一、为什么 Cache 会成为 DMA 的“猪队友”?
STM32H7 基于 Cortex-M7 内核,带 16KB I-Cache 和 16KB D-Cache。CPU 访问内存时,若命中 Cache 则直接读写缓存,不会立即同步到物理内存(SRAM/SDRAM)。而 DMA 是“老实人”,它只认物理内存地址,完全绕过 Cache。
这就导致两个经典问题:
- DMA 读(外设→内存):DMA 把新数据写入物理内存,但 CPU 读到的却是 Cache 里的旧数据。
- DMA 写(内存→外设):CPU 修改了 Cache 中的数据,但尚未写回物理内存,DMA 却从物理内存取走了旧数据。
结果就是数据错乱、校验失败,甚至因访问未对齐地址触发 HardFault。
二、Cache 操作函数:Clean、Invalidate 与 Clean+Invalidate
CMSIS 提供了三个关键函数(定义在 core_cm7.h):
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 中“脏”数据写回物理内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从物理内存重新加载。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再丢弃,用于彻底同步。
核心原则:
-
DMA 读之前(外设→内存):CPU 不应有未写回的数据,且需让 Cache 失效,以便读到 DMA 写入的新数据。通常调用
Invalidate。 -
DMA 写之前(内存→外设):必须确保 CPU 的修改已写回物理内存,调用
Clean。 -
DMA 传输完成后:若 CPU 要读取 DMA 写入的数据,需再次
Invalidate(或CleanInvalidate)。
三、正确调用时机与代码示例
假设使用 uint8_t dma_buffer[1024] __attribute__((aligned(32))),且 DMA 配置为内存递增、外设到内存。
3.1 外设→内存(DMA 读)
// 启动 DMA 前:使 Cache 失效,避免 CPU 旧数据干扰
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
HAL_DMA_Start(&hdma, (uint32_t)&periph_reg, (uint32_t)dma_buffer, sizeof(dma_buffer));
// 等待 DMA 完成(中断或轮询)
while (HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);
// DMA 完成后:再次失效,确保 CPU 读到新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
// 现在可以安全读取 dma_buffer
3.2 内存→外设(DMA 写)
// 准备数据
memset(dma_buffer, 0xAA, sizeof(dma_buffer));
// 启动 DMA 前:将 Cache 写回物理内存
SCB_CleanDCache_by_Addr((uint32_t*)dma_buffer, sizeof(dma_buffer));
HAL_DMA_Start(&hdma, (uint32_t)dma_buffer, (uint32_t)&periph_reg, sizeof(dma_buffer));
// 等待完成...
四、MPU 配置:更优雅的解决方案
频繁手动 Clean/Invalidate 不仅代码臃肿,还容易遗漏。STM32H7 的 MPU 可以将特定内存区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题。
4.1 配置步骤
- 使能 MPU:
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); - 定义区域:例如将
0x30000000(SRAM1)开始的 64KB 设为 Non-Cacheable。 - 设置属性:
MPU_ACCESS_NOT_CACHEABLE、MPU_ACCESS_NOT_BUFFERABLE、MPU_ACCESS_SHAREABLE。 - 加载并启用。
4.2 代码示例
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
之后将 DMA 缓冲区放在该区域(如 __attribute__((section(".dma_buffer")))),即可免去手动 Cache 维护。
五、注意事项与避坑指南
-
地址对齐:Cache 操作要求地址 32 字节对齐,否则可能触发 HardFault。使用
__attribute__((aligned(32)))。 - 大小对齐:操作长度也建议 32 字节对齐,不足时向上取整。
- 不要过度 Invalidate:Invalidate 会丢弃未写回的数据,若在 DMA 写之前误用,将导致数据丢失。
- 中断与 DMA 竞争:在 DMA 完成中断中调用 Cache 操作时,确保没有其他主设备访问同一区域。
- MPU 区域重叠:多个 MPU 区域重叠时,编号高的优先,需仔细规划。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,仅对 DMA 缓冲区使用,不要全局关闭 Cache。
六、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典深坑。掌握 Clean/Invalidate 的调用时机是基础,而合理使用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through 则是更稳健的工程实践。建议在项目初期就规划好内存布局,避免后期调试时被“幽灵数据”折磨。