一、为什么 H7 上 DMA 会“莫名其妙”出错?

STM32H7 主频高达 480MHz,为了匹配 CPU 与存储器速度,Cortex-M7 内核配备了 L1 D-Cache(数据缓存)。开启后,CPU 读写数据可能只操作 Cache,而不立即写回 SRAM。DMA 作为独立外设,直接访问物理内存,不经过 Cache。

这就导致两类经典问题:

  • DMA 读(外设→内存):DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 里的旧数据。
  • DMA 写(内存→外设):CPU 修改了数据,但还在 Cache 中未写回 SRAM,DMA 发送的是旧数据。

解决思路只有两条:要么让该内存区域不经过 Cache(MPU 配置为 Device/Strongly Ordered 或 Write-Through),要么在恰当时机手动 clean/invalidate。

二、MPU 配置:最容易踩的三个坑

坑 1:忘记配置 MPU,直接开 DCache

默认情况下,所有内存都是 Cacheable Write-Back。DMA 缓冲区若未特殊处理,必然出错。

坑 2:把 DMA 缓冲区配成 Write-Back 却只做 invalidate

Write-Back 模式下,CPU 写数据可能还在 Cache 中。若只 invalidate,会丢弃未写回的数据,导致 DMA 发送旧内容。正确做法是 先 clean 再 invalidate,或直接配置为 Write-Through。

坑 3:MPU 区域大小与对齐错误

MPU 要求区域起始地址按区域大小对齐,且最小 32 字节。例如 64KB 区域,基址必须是 64KB 对齐。否则配置失败,行为不可预测。

推荐配置:将 DMA 缓冲区所在 SRAM 区域配置为 Non-Cacheable 或 Write-Through,可省去手动维护,但会损失部分性能。若追求性能,则保持 Write-Back,并严格手动 clean/invalidate。

// MPU 配置示例:将 0x30000000 开始的 64KB 配置为 Non-Cacheable
void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    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);
}

三、clean 与 invalidate 的正确调用时机

假设 DMA 缓冲区仍为 Cacheable Write-Back,必须遵循以下规则:

  • 外设→内存(DMA 读):DMA 启动前 invalidate 缓冲区,避免 Cache 中旧数据干扰;DMA 完成后再次 invalidate,确保 CPU 读到新数据。
  • 内存→外设(DMA 写):DMA 启动前 clean 缓冲区,确保数据写回 SRAM;DMA 完成后无需操作。

注意:invalidate 会丢弃未写回的数据,因此 绝不能对 CPU 刚写过但未 clean 的缓冲区直接 invalidate。

// 发送前 clean
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));

// 接收前 invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_UART_Receive_DMA(&huart1, rx_buf, sizeof(rx_buf));
// 接收完成回调中再次 invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
    // 处理数据
}

四、完整示例:UART DMA 收发与 Cache 维护

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

void UART_DMA_Init(void)
{
    // 填充发送数据
    for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;
    // 启动接收
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

void UART_SendData(void)
{
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, BUF_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
    // 此时 rx_buf 数据有效,可安全读取
}

五、注意事项与避坑清单

  • 缓冲区对齐:使用 __attribute__((aligned(32))) 确保 32 字节对齐,否则 clean/invalidate 可能影响相邻数据。
  • 大小取整:clean/invalidate 的长度建议按 32 字节向上取整,避免遗漏。
  • 避免频繁操作:clean/invalidate 本身有开销,可批量处理或使用 Non-Cacheable 区域。
  • 中断与 DMA 竞争:在 DMA 完成中断中 invalidate 前,确保 CPU 未访问该缓冲区。
  • 多缓冲区场景:若使用双缓冲,每个缓冲区都需独立维护。
  • 调试技巧:若数据错乱,先关闭 DCache 验证是否为一致性问题,再逐步开启。

掌握 MPU 配置与 clean/invalidate 的时机,你就能在 STM32H7 上既享受 DCache 的性能,又保证 DMA 数据传输的可靠性。