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 的步骤:
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; - 关闭 D-Cache 并清理:
SCB_DisableDCache(); - 配置 MPU 区域:设置基地址、大小、属性为
MPU_ACCESS_NOT_CACHEABLE。 - 使能 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 就能和谐共处,让你的高性能应用稳定运行。