STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、MPU 配置与 Clean/Invalidate 的踩坑排查

STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。但开启 D-Cache 后,DMA 传输的数据经常出现“随机错乱”——明明外设收到了数据,内存里却是旧值;或者内存更新了,DMA 却发送了旧数据。这背后是 Cache 一致性 问题。本文从原理到实战,帮你彻底填平这个坑。

一、为什么 D-Cache 会与 DMA 冲突?

Cortex-M7 的 D-Cache 位于 CPU 和主存之间。当 CPU 写数据时,若命中 Cache,数据只写入 Cache,主存并未更新(写回策略)。此时若 DMA 从主存读取,就会拿到旧数据。反之,DMA 将新数据写入主存,但 CPU 读时命中 Cache 中的旧数据,同样出错。

关键点:DMA 不经过 Cache,直接访问主存。因此必须手动维护一致性。

二、地址对齐:容易被忽视的硬性要求

ARM 规定,对 Cache 的维护操作(Clean/Invalidate)必须基于 Cache Line 对齐。Cortex-M7 的 D-Cache Line 为 32 字节。

  • 若缓冲区起始地址或长度不是 32 字节对齐,Clean/Invalidate 会波及相邻数据,导致其他变量被意外清除或写回。
  • 解决方案:使用 __attribute__((aligned(32))) 强制对齐,并确保长度是 32 的整数倍。
// 定义 DMA 缓冲区,32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[256];

三、MPU 配置:将 DMA 缓冲区设为 Non-Cacheable

最省心的方案是通过 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable,这样 CPU 和 DMA 都直接访问主存,无需手动维护。

以 STM32H7 为例,配置 MPU 的步骤:

  1. 使能 MPU:SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
  2. 关闭 D-Cache 并清理:SCB_DisableDCache();
  3. 配置 MPU 区域:设置基地址、大小、属性为 MPU_ACCESS_NOT_CACHEABLE。
  4. 使能 MPU 和 D-Cache。
void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 地址
    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);
}

注意:MPU 区域大小和基地址必须对齐(例如 64KB 区域基地址需 64KB 对齐)。

四、手动 Clean/Invalidate:灵活但需谨慎

若无法使用 MPU,则需在 DMA 传输前后手动操作 Cache。

  • 发送前(CPU 写,DMA 读):调用 SCB_CleanDCache_by_Addr(),将 Cache 数据写回主存。
  • 接收后(DMA 写,CPU 读):调用 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 旧数据,强制从主存加载。
// 发送前清理
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
HAL_UART_Transmit_DMA(&huart1, dma_buffer, sizeof(dma_buffer));

// 接收后无效化
HAL_UART_Receive_DMA(&huart1, dma_buffer, sizeof(dma_buffer));
// 等待传输完成...
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));

踩坑提醒:

  • 不要对同一缓冲区同时 Clean 和 Invalidate,顺序错误会丢数据。
  • 若缓冲区被多个 DMA 描述符使用,需逐段维护。
  • 调用前确保地址和长度 32 字节对齐。

五、典型排查案例

现象:ADC 连续采样 DMA 传输,数据偶尔跳变。 排查:发现 ADC 缓冲区未对齐,且未做 Invalidate。DMA 写入新数据后,CPU 读到了 Cache 中的旧值。 解决:将缓冲区改为 32 字节对齐,并在 DMA 完成中断中调用 SCB_InvalidateDCache_by_Addr()。

现象:UART DMA 发送字符串,接收端收到乱码。 排查:发送前未 Clean,Cache 中修改的字符串未写回主存,DMA 发送了旧内容。 解决:发送前调用 SCB_CleanDCache_by_Addr()。

六、最佳实践总结

  • 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
  • 若手动维护,务必保证 32 字节对齐,并严格区分 Clean 和 Invalidate 的使用场景。
  • 在 DMA 传输完成中断中处理 Invalidate,避免过早操作。
  • 使用 SCB_InvalidateDCache_by_Addr 时,注意不要影响其他变量。
  • 调试时可通过关闭 D-Cache 验证是否为一致性问题。

掌握这些要点,STM32H7 的 D-Cache 与 DMA 就能和谐共处,让你的高性能应用稳定运行。