一、为什么 STM32H7 的 Cache 会让 DMA 出错?
STM32H7 内置 16KB 的 L1 Cache(I-Cache 和 D-Cache),CPU 访问内存时会优先命中 Cache,从而大幅提升性能。但 DMA 控制器是直接访问物理内存(SRAM、SDRAM 等),并不经过 Cache。这就导致两个经典问题:
- 写丢失:CPU 写数据到 Cache 后,DMA 从内存读到的仍是旧数据。
- 读脏:DMA 将新数据写入内存,但 CPU 读到的却是 Cache 中的旧副本。
更麻烦的是,STM32H7 的 Cache 是写回(Write-Back) 策略,且支持写分配(Write-Allocate),使得问题更加隐蔽。
二、核心原理:Cache 与 DMA 的三种一致性场景
| 方向 | 风险 | 正确操作 | |------|------|----------| | CPU 写 → DMA 读 | Cache 中数据未回写 | 发送前 Clean | | DMA 写 → CPU 读 | Cache 中旧数据未失效 | 接收后 Invalidate | | CPU 读写同一缓冲区 | 读写顺序错乱 | 先 Clean 再 Invalidate |
关键点:Clean 是把 Cache 数据写回内存,Invalidate 是丢弃 Cache 数据并从内存重新加载。顺序错误会导致数据被覆盖或丢失。
三、MPU 配置:为 DMA 缓冲区划定“安全区”
最稳妥的做法是通过 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,从而避免手动维护一致性。
3.1 MPU 配置步骤
- 使能 MPU:
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;并调用ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk); - 定义区域基址、大小(必须是 2 的幂,最小 32 字节)。
- 设置属性:
ARM_MPU_ATTR(ARM_MPU_AP_FULL, 0)表示 Non-Cacheable。 - 加载区域并启用。
3.2 代码示例:将 0x24000000 开始的 64KB 设为 Non-Cacheable
#include "cmsis_compiler.h"
void MPU_Config(void)
{
ARM_MPU_Disable();
/* 区域 0:DMA 缓冲区,Non-Cacheable, Non-Shareable */
ARM_MPU_SetRegion(
0, // 区域编号
0x24000000, // 基址 (AXI SRAM)
ARM_MPU_RASR(0, // 禁止指令访问
ARM_MPU_AP_FULL, // 全权限
0, // TEX=0
0, // C=0, B=0 -> Non-Cacheable
0, // S=0
0, // 子区域禁用
ARM_MPU_REGION_SIZE_64KB)
);
ARM_MPU_Enable(MPU_CTRL_PRIVDEFENA_Msk);
__DSB();
__ISB();
}
注意:若使用 SDRAM 作为 DMA 缓冲区,同样需将其配置为 Non-Cacheable,或使用 Write-Through 模式并配合手动 Clean。
四、手动维护一致性:Clean 与 Invalidate 的正确用法
当无法使用 MPU 时(例如缓冲区与 CPU 频繁交互),必须手动调用 CMSIS 提供的函数:
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
4.1 发送场景(CPU → DMA)
// 填充数据到缓冲区
fill_buffer(tx_buf, LEN);
// 关键:将 Cache 写回内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, LEN);
// 启动 DMA 发送
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, LEN);
4.2 接收场景(DMA → CPU)
// 启动 DMA 接收(注意:接收前先 Invalidate,避免 Cache 中旧数据干扰)
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, LEN);
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)rx_buf, LEN);
// 等待 DMA 完成(中断或轮询)
while (!dma_done);
// 关键:使 Cache 失效,强制 CPU 从内存读取新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, LEN);
// 此时 CPU 读取 rx_buf 才是 DMA 写入的新数据
process_data(rx_buf, LEN);
4.3 双向场景(CPU 与 DMA 交替读写)
// 先 Clean 确保旧数据写回,再 Invalidate 丢弃 Cache 副本
SCB_CleanInvalidateDCache_by_Addr((uint32_t*)buf, LEN);
五、完整排查路径与注意事项
5.1 排查清单
- 确认缓冲区地址:是否落在 Cacheable 区域?
- 检查 MPU 配置:DMA 缓冲区是否已设为 Non-Cacheable?
- 检查 Clean/Invalidate 调用:发送前是否 Clean?接收后是否 Invalidate?
- 检查对齐:地址和长度是否 32 字节对齐?CMSIS 函数要求 32 字节对齐,否则可能误伤相邻数据。
- 检查 DMA 传输完成标志:必须在 DMA 完成后再 Invalidate,否则会读到部分数据。
- 检查中断优先级:DMA 中断与 Cache 操作可能被高优先级中断打断,导致顺序错乱。
5.2 常见陷阱
- Invalidate 前未 Clean:如果 CPU 曾写过该缓冲区,Invalidate 会丢弃未回写的数据。
- 缓冲区跨 Cache 行:长度不是 32 字节倍数时,Invalidate 可能影响相邻变量。
- DMA 双缓冲模式:两个缓冲区都要独立维护一致性。
-
使用
__DSB():在 Clean/Invalidate 后、启动 DMA 前插入__DSB()确保操作完成。
5.3 推荐实践
- 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
- 若必须使用 Cacheable 内存,将缓冲区按 32 字节对齐,并封装统一的
dma_send()/dma_recv()函数,内部自动处理 Clean/Invalidate。 - 在调试时,可临时关闭 D-Cache 验证问题是否由 Cache 引起。
六、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典“暗坑”。理解 Clean 与 Invalidate 的语义、合理配置 MPU、严格遵循操作顺序,才能确保数据正确。记住:发送前 Clean,接收后 Invalidate,双向操作先 Clean 再 Invalidate。掌握这些,你就能在 H7 的高性能与数据一致性之间游刃有余。