一、为什么 D-Cache 和 DMA 会打架?
STM32H7 主频高达 480MHz,为了匹配 CPU 速度,芯片内置了 L1 D-Cache(数据缓存)。CPU 访问内存时,若命中 Cache 则直接读写缓存,不会立即同步到实际 RAM。而 DMA 是独立于 CPU 的外设,它直接访问物理内存(SRAM/SDRAM),完全绕过 Cache。
这就导致两个典型问题:
- DMA 读取时:CPU 刚写入的数据还在 Cache 里(Write-Back 模式),物理内存中仍是旧值,DMA 发送出去的是旧数据。
- DMA 写入时:DMA 把新数据写入物理内存,但 CPU 之前读过的旧数据还在 Cache 中,CPU 再次读取时命中 Cache,拿到的是旧数据。
根本矛盾:同一块内存存在两个副本(Cache 与物理内存),且没有硬件自动同步机制。
二、解决思路:让 DMA 缓冲区“绕过”Cache
常见方案有三种:
- 使用
SCB_CleanDCache_by_Addr()/SCB_InvalidateDCache_by_Addr()手动维护一致性。 - 将 DMA 缓冲区放在默认非缓存区域(如 DTCM,但 H7 的 DTCM 不能被大多数 DMA 访问)。
- 通过 MPU 将指定内存区域配置为 Non-Cacheable,一劳永逸。
方案 3 最稳定,适合频繁 DMA 传输的场景。下面详细讲解 MPU 配置的正确姿势。
三、MPU 配置 Non-Cacheable 区域步骤
3.1 确定内存区域
假设我们使用 AXI SRAM(0x24000000)中的一段 32KB 空间作为 DMA 缓冲区。注意:不要与栈、堆或已使用的区域重叠。
3.2 配置 MPU 属性
MPU 以 Region 为单位管理,每个 Region 需设置基地址、大小、访问权限和内存属性。关键属性:
- TEX = 1, C = 0, B = 0:Normal memory, Outer and Inner Non-cacheable。
- AP = 0b011:Full access(根据需求调整)。
- XN = 0:允许执行(通常禁止执行更安全,可设 1)。
3.3 完整代码示例
#include "stm32h7xx.h"
#define DMA_BUFFER_ADDR 0x24000000UL
#define DMA_BUFFER_SIZE 0x8000UL // 32KB
void MPU_Config_NonCacheable(void)
{
HAL_MPU_Disable(); // 配置前必须关闭 MPU
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 选择 Region 0(0~15 任选,不冲突即可)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = DMA_BUFFER_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // TEX = 1
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
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);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:
HAL_MPU_ConfigRegion内部会处理 TEX/C/B 的组合,只需正确赋值IsCacheable和IsBufferable即可。
3.4 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config_NonCacheable(); // 必须在使能 Cache 之前调用
SCB_EnableICache();
SCB_EnableDCache();
// 后续初始化 DMA 并使用 DMA_BUFFER_ADDR 作为缓冲区
...
}
四、注意事项与常见坑
-
顺序至关重要:MPU 配置必须在
SCB_EnableDCache()之前完成,否则 Cache 已生效,再改 MPU 可能引发不可预知行为。 - Region 重叠:多个 Region 重叠时,编号高的优先级高。确保 Non-Cacheable 区域没有被其他 Region 覆盖。
- 大小与对齐:Region 大小必须是 2 的幂,且基地址按大小对齐。例如 32KB 区域基地址必须 32KB 对齐。
- DTCM 不能用于 DMA:H7 的 DTCM 地址为 0x20000000,大部分 DMA 无法访问,不要试图将其配为 Non-Cacheable 给 DMA 用。
- 多缓冲区场景:若多个 DMA 缓冲区分散,可合并到一个连续区域,或使用多个 MPU Region。
- 性能权衡:Non-Cacheable 区域 CPU 访问会变慢,只把 DMA 缓冲区放进去,其他代码/数据仍保持 Cacheable。
-
调试技巧:用
SCB_InvalidateDCache_by_Addr()配合测试,若数据一致则说明 MPU 配置成功。
五、验证方法
写一个简单测试:CPU 向 Non-Cacheable 缓冲区写入递增数据,启动 DMA 发送到另一块内存,比较结果。若完全一致,则配置正确。也可通过查看 MPU 寄存器 MPU->RBAR 和 MPU->RASR 确认属性。
六、总结
STM32H7 的 D-Cache 与 DMA 冲突是嵌入式开发中的经典陷阱。通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable,可以从根源上避免数据一致性问题,且无需在每次传输前后手动清理/无效化 Cache。牢记配置顺序、区域对齐和权限设置,你就能安心享受 H7 的高性能与 DMA 的高效率。