STM32H7 D-Cache与DMA冲突实战:数据错乱排查与MPU配置详解

一、问题现象:DMA传输数据错乱

在 STM32H7 项目中使用 DMA 搬运 ADC 采样数据或串口收发时,常遇到以下现象:

  • DMA 目标缓冲区数据部分正确、部分为旧值或随机值
  • 串口发送的数据与预期不符,接收数据丢失
  • 关闭 D-Cache 后问题消失,开启后复现

根本原因:Cortex-M7 的 D-Cache 与 DMA 控制器访问内存时缺乏一致性。

二、原理剖析:为什么会产生冲突

2.1 D-Cache 的工作机制

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 写数据时先写 Cache,标记为脏(Dirty),稍后才写回主存;CPU 读数据时若 Cache 未命中,则从主存加载到 Cache。

2.2 DMA 与 Cache 的不一致

DMA 控制器直接访问主存(SRAM),不经过 Cache。因此:

  • CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中未写回,DMA 读到的是旧数据。
  • DMA 写、CPU 读:DMA 将新数据写入主存,但 CPU 读到的可能是 Cache 中的旧副本。

2.3 典型场景

// 错误示例:DMA 发送,CPU 填充数据
uint8_t tx_buf[256];
void send_data(void) {
    for (int i = 0; i < 256; i++) tx_buf[i] = i;
    HAL_UART_Transmit_DMA(&huart1, tx_buf, 256); // DMA 可能读到旧数据
}

三、排查步骤

  1. 确认问题是否与 Cache 相关:在 main() 开头调用 SCB_DisableDCache(),若问题消失,则确认为 Cache 一致性导致。
  2. 定位 DMA 缓冲区地址:检查缓冲区是否位于可 Cache 区域(如 DTCM 默认不经过 Cache,AXI SRAM 经过 Cache)。
  3. 检查 MPU 配置:确认是否对 DMA 缓冲区所在区域配置了正确的内存属性。
  4. 使用调试器观察:在 DMA 传输前后查看内存与 Cache 内容,验证数据是否同步。

四、解决方案:MPU 配置与 Cache 维护

4.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable

通过 MPU 将 DMA 使用的内存区域设置为 Non-Cacheable,一劳永逸。

步骤:

  1. 在链接脚本中为 DMA 缓冲区分配独立段,例如 .dma_buffer。
  2. 配置 MPU 区域,属性设为 Normal, Non-Cacheable。
  3. 使能 MPU 和 D-Cache。

链接脚本示例(STM32H7 的 AXI SRAM 区域):

.dma_buffer (NOLOAD) : {
    . = ALIGN(32);
    *(.dma_buffer)
    . = ALIGN(32);
} > RAM_D1

C 代码定义缓冲区:

__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_tx_buf[256];

MPU 配置函数:

#include "stm32h7xx_hal.h"

void MPU_Config(void) {
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 DMA 缓冲区区域为 Non-Cacheable
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x24000000; // AXI SRAM 起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

4.2 方案二:使用 Cache 维护操作

若缓冲区仍需 Cache 加速,可在 DMA 传输前后手动维护 Cache。

发送前(CPU 写,DMA 读):

SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, sizeof(dma_tx_buf));
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, sizeof(dma_tx_buf));

接收后(DMA 写,CPU 读):

// DMA 接收完成中断中
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf));
// 之后 CPU 才能安全读取 dma_rx_buf

注意: SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度也建议按 32 字节对齐,否则可能误无效相邻数据。

4.3 方案三:使用 DTCM 内存

STM32H7 的 DTCM(Data Tightly-Coupled Memory)默认不经过 Cache,可直接用于 DMA 缓冲区。但 DTCM 容量有限(通常 128KB),且 DMA 无法访问所有外设(如某些 DMA 不能访问 DTCM)。

五、完整示例:ADC + DMA + D-Cache 正确配置

#include "stm32h7xx_hal.h"

#define ADC_BUF_SIZE 128

__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_dma_buf[ADC_BUF_SIZE];

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void System_Init(void) {
    HAL_Init();
    MPU_Config();          // 先配置 MPU
    SCB_EnableICache();    // 使能 I-Cache
    SCB_EnableDCache();    // 使能 D-Cache
    // ... 时钟、外设初始化
}

void ADC_DMA_Start(void) {
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_BUF_SIZE);
}

// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 若缓冲区配置为 Non-Cacheable,无需维护 Cache
    // 若为 Cacheable,需调用:
    // SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, sizeof(adc_dma_buf));

    // 安全读取数据
    uint32_t sum = 0;
    for (int i = 0; i < ADC_BUF_SIZE; i++) {
        sum += adc_dma_buf[i];
    }
}

六、注意事项与最佳实践

  • MPU 配置必须在使能 D-Cache 之前完成,否则可能触发异常。
  • DMA 缓冲区地址和长度尽量 32 字节对齐,避免 Cache 维护操作影响相邻数据。
  • 避免在 DMA 传输过程中 CPU 访问同一缓冲区,否则仍可能产生竞争。
  • 使用 SCB_CleanDCache_by_Addr 而非 SCB_CleanInvalidateDCache,后者会无效全部 Cache,影响性能。
  • 对于频繁小数据 DMA,优先使用 Non-Cacheable 区域,减少维护开销。
  • 调试时可用 __DSB() 和 __ISB() 确保内存操作顺序。

七、总结

STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。理解 Cache 一致性原理,通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,或合理使用 Cache 维护函数,即可彻底解决数据错乱问题。推荐在项目初期就规划好内存布局,将 DMA 缓冲区统一放置于 Non-Cacheable 区域,既保证性能又避免隐患。