STM32H7 的 D-Cache 与 DMA 一致性坑:用 MPU 配置 Non-Cacheable 区域的正确姿势

一、为什么 D-Cache 会和 DMA 打架?

STM32H7 的 Cortex-M7 内核带有一级 D-Cache(数据缓存),默认写回策略。CPU 访问内存时,数据可能只停留在 Cache 里,并未真正写入 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接访问物理内存,看不到 Cache 内容。

典型场景:

  • CPU 准备好发送缓冲区,调用 HAL_UART_Transmit_DMA(),但数据还在 Cache 中,DMA 读到的是旧数据。
  • DMA 接收数据到内存,CPU 读到的却是 Cache 中的旧值。

结果就是:数据错乱、偶发死机、调试时一切正常,发布后随机崩溃。

二、三种解决思路对比

  1. 手动维护 Cache:发送前 SCB_CleanDCache_by_Addr(),接收后 SCB_InvalidateDCache_by_Addr()。
    • 缺点:调用频繁,地址必须 32 字节对齐,容易漏掉或重复失效。
  2. 将 DMA 缓冲区放到非缓存区:通过 MPU 配置某段 SRAM 为 Non-Cacheable。
    • 优点:一劳永逸,CPU 和 DMA 看到同一份数据,无需手动维护。
  3. 使用 DTCM RAM:DTCM 默认不经过 Cache,但容量有限(通常 128KB),且部分 DMA 无法访问。

推荐方案:MPU 配置 Non-Cacheable 区域,兼顾性能与安全。

三、MPU 配置步骤(以 STM32H743 为例)

3.1 确定内存区域

假设使用 D2 SRAM(地址 0x30000000,大小 128KB)作为 DMA 缓冲区。

3.2 配置 MPU Region

MPU 共有 16 个 Region,每个 Region 需设置基地址、大小、属性和使能。关键属性:

  • TEX = 0b001, C = 0, B = 0 → Normal, Non-Cacheable
  • AP = 0b011 → 全访问
  • XN = 0 → 允许执行(可选)

3.3 完整代码示例

#include "stm32h7xx.h"

#define DMA_BUFFER_ADDR   0x30000000UL
#define DMA_BUFFER_SIZE   0x20000UL   // 128KB

void MPU_Config_NonCacheable(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 Region 0 为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.BaseAddress      = DMA_BUFFER_ADDR;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_128KB;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL1;  // TEX = 0b001
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;  // C = 0
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE; // B = 0

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

3.4 在 main 中调用

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config_NonCacheable();  // 必须在使能 D-Cache 之前调用
    SCB_EnableICache();
    SCB_EnableDCache();

    // 后续 DMA 缓冲区使用 DMA_BUFFER_ADDR 起始的地址
    uint8_t *dma_buf = (uint8_t *)DMA_BUFFER_ADDR;
    // ...
}

四、注意事项与常见坑

  • 顺序不能错:先配置 MPU,再使能 D-Cache。否则已缓存的数据可能未失效。
  • 地址对齐:Region 基地址必须按大小对齐,例如 128KB 区域基地址低 17 位必须为 0。
  • 大小限制:MPU Region 大小必须是 2 的幂,且最小 32 字节。
  • 不要与 DTCM 冲突:DTCM 地址 0x20000000 默认不缓存,无需重复配置。
  • DMA 描述符也要注意:如果使用链表式 DMA,描述符本身也应放在非缓存区。
  • 多 Region 重叠:高编号 Region 优先级更高,注意规划。
  • 调试时:可临时关闭 D-Cache 验证问题,但正式版务必开启。

五、验证方法

  1. 在 DMA 传输前后打印缓冲区数据,确认一致。
  2. 使用 SCB_InvalidateDCache_by_Addr() 对比测试,观察差异。
  3. 用逻辑分析仪或示波器抓取总线波形,确认 DMA 实际访问。

六、总结

STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典坑。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据不一致,且无需在每次传输时手动维护 Cache。记住:MPU 配置在前,D-Cache 使能在后,地址对齐要检查,Region 规划要合理。掌握这一招,你的 H7 项目将更加稳定可靠。