一、为什么 Cache 和 DMA 会打架?
STM32H7 基于 Cortex-M7 内核,带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。开启 D-Cache 后,CPU 访问内存时可能直接命中 Cache,而不会立即写回主存(SRAM)。DMA 则直接访问主存,不经过 Cache。
这就导致两个经典问题:
- DMA 读取时数据陈旧:CPU 修改了缓冲区数据,但只更新了 Cache,未写回 SRAM。DMA 从 SRAM 读取,拿到的是旧数据。
- DMA 写入后 CPU 读到旧数据:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中仍保留着该地址的旧内容。CPU 读取时命中 Cache,得到旧数据。
此外,Cache 的写回操作可能以 Cache Line(32 字节) 为单位,若 DMA 缓冲区未按 Cache Line 对齐,Clean/Invalidate 操作可能误伤相邻数据,引发更难排查的错误。
二、解决思路:MPU 配置 + 手动维护
2.1 方案一:将 DMA 缓冲区配置为 Non-Cacheable
通过 MPU(Memory Protection Unit)将 DMA 使用的内存区域设置为 Non-Cacheable, Non-Shareable,这样 CPU 访问该区域时绕过 Cache,从根本上避免一致性问题。
优点:一劳永逸,无需手动 Clean/Invalidate。
缺点:CPU 访问该区域速度下降,适合对性能不敏感的数据缓冲区。
2.2 方案二:保持 Cacheable,手动维护一致性
在 DMA 传输前后调用 SCB_CleanDCache_by_Addr() 和 SCB_InvalidateDCache_by_Addr():
- 发送前(CPU 写,DMA 读):Clean,将 Cache 数据写回 SRAM。
- 接收后(DMA 写,CPU 读):Invalidate,丢弃 Cache 中的旧数据,强制从 SRAM 重新加载。
优点:CPU 访问缓冲区仍享受 Cache 加速。
缺点:需精确控制,且缓冲区必须按 Cache Line 对齐。
三、MPU 配置步骤与代码
以将 0x30000000 开始的 64KB 区域(D2 SRAM)配置为 Non-Cacheable 为例。
3.1 使能 MPU 并配置 Region
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 配置 Region 0:D2 SRAM 64KB 为 Non-Cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
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;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:
IsCacheable = NOT_CACHEABLE且IsBufferable = NOT_BUFFERABLE时,该区域为 Strongly Ordered 或 Device 内存,具体取决于 TEX 等位。对于普通 SRAM,推荐 TEX=0, C=0, B=0,即 Non-Cacheable Normal Memory。
3.2 在 main 中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 前配置
SCB_EnableICache();
SCB_EnableDCache();
// ... 其他初始化
}
四、手动 Clean/Invalidate 的正确姿势
若选择方案二,需注意以下要点:
4.1 缓冲区对齐
DMA 缓冲区必须按 32 字节(Cache Line 大小) 对齐,且长度最好是 32 的整数倍。
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_buf[BUF_SIZE];
4.2 发送前 Clean
// CPU 填充数据后,DMA 发送前
memcpy(dma_buf, src, BUF_SIZE);
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, dma_buf, BUF_SIZE);
4.3 接收后 Invalidate
// DMA 接收完成回调中
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
// 此时 CPU 读取 dma_buf 才能拿到 DMA 写入的新数据
process_data(dma_buf, BUF_SIZE);
}
4.4 关键陷阱:Invalidate 前先 Clean
如果缓冲区在 DMA 接收期间被 CPU 修改过(例如部分写入),直接 Invalidate 会丢弃这些修改。正确做法是先 Clean 再 Invalidate,或确保 CPU 不碰该区域。
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, BUF_SIZE);
五、注意事项与常见坑
- MPU 配置必须在使能 Cache 之前完成,否则配置不生效。
- DMA 描述符、链表等结构体若被 DMA 访问,也应放在 Non-Cacheable 区域或手动维护。
- 多缓冲区(Ping-Pong) 时,每个缓冲区都要独立 Clean/Invalidate,不能只操作首地址。
- Cache Line 对齐:若缓冲区未对齐,Clean/Invalidate 可能影响相邻变量,导致数据损坏。
-
使用
SCB_InvalidateDCache_by_Addr时地址必须 32 字节对齐,否则行为未定义。 - 调试时:若发现 DMA 数据错乱,首先检查 Cache 配置和 Clean/Invalidate 调用时机。
- 性能权衡:Non-Cacheable 区域访问速度约为 Cacheable 的 1/3~1/2,对高速数据流建议用方案二。
六、总结
STM32H7 的 Cache 与 DMA 一致性问题是嵌入式开发中的经典难题。核心原则:要么让 DMA 缓冲区绕过 Cache(MPU 配置),要么在 DMA 传输前后正确 Clean/Invalidate。无论哪种方案,都要保证缓冲区按 Cache Line 对齐,并注意操作顺序。掌握这些姿势,你就能在享受 H7 高性能的同时,避开数据一致性的坑。