一、为什么 H7 上 Cache 和 DMA 会打架?
STM32H7 的 Cortex-M7 内核带有一级数据缓存(D-Cache),默认写回(Write-Back)、写分配(Write-Allocate)。CPU 访问内存时,数据可能只停留在 Cache 里,并没有真正写到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理内存,不经过 Cache。
这就导致两个经典问题:
- CPU 写、DMA 读:CPU 把数据写入缓冲区,数据还在 Cache 中(脏行),DMA 从 SRAM 读到的是旧数据。
- DMA 写、CPU 读:DMA 把新数据搬到 SRAM,但 CPU 读同一地址时命中了 Cache 里的旧行,读到旧数据。
解决思路只有两个:要么让该区域不走 Cache(MPU 配置为 Device/Strongly-Ordered 或 Non-Cacheable),要么在恰当时机手动维护 Cache(Clean / Invalidate)。
二、Clean 与 Invalidate 到底做了什么?
- Clean(清理):把 Cache 中的脏行写回 SRAM。用于“CPU 写、DMA 读”之前,确保 SRAM 拿到最新数据。
- Invalidate(无效化):把 Cache 中的行标记为无效。下次 CPU 读取时会重新从 SRAM 加载。用于“DMA 写、CPU 读”之前,丢弃旧缓存。
- Clean & Invalidate:两者都做,常用于双向缓冲区或不确定方向的场景。
注意:Invalidate 是危险操作。如果 Cache 中有尚未 Clean 的脏数据,直接 Invalidate 会永久丢失这些修改。所以顺序很关键。
三、正确时机:一张表说清楚
| 场景 | 操作 | 时机 | |------|------|------| | CPU 填充发送缓冲区 → 启动 DMA 发送 | Clean | 启动 DMA 之前 | | DMA 接收完成 → CPU 读取接收缓冲区 | Invalidate | DMA 完成中断里,读取数据之前 | | 双向缓冲区(如 SPI 全双工) | Clean & Invalidate | 启动 DMA 前 Clean,完成中断里 Invalidate |
核心口诀:“发之前 Clean,收之后 Invalidate”。
四、MPU 配置:更省心的方案
如果不想每次手动维护,可以把 DMA 缓冲区所在 SRAM 区域配置为 Non-Cacheable。以 STM32H7 的 AXI SRAM(0x24000000)为例,使用 HAL 的 MPU 配置:
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
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_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
这样该区域所有访问都直达 SRAM,无需手动 Clean/Invalidate。代价是失去缓存加速,适合 DMA 频繁的大缓冲区。
五、完整代码示例:ADC + DMA 采集
下面以 ADC1 使用 DMA 循环采集为例,演示手动维护 Cache 的标准写法。缓冲区放在可缓存区域(如 DTCM 以外的 AXI SRAM)。
#include "stm32h7xx_hal.h"
#define ADC_BUF_LEN 256
/* 必须 32 字节对齐,Cache 行大小为 32 字节 */
aligned(32) static uint16_t adc_buf[ADC_BUF_LEN];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void ADC_DMA_Init(void)
{
/* 启动 DMA 前:若缓冲区有 CPU 预填数据,先 Clean */
SCB_CleanDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);
}
/* DMA 传输完成回调 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/* 读取前:Invalidate,丢弃 Cache 中的旧数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));
/* 此时 adc_buf 中才是 DMA 搬来的最新数据 */
ProcessAdcData(adc_buf, ADC_BUF_LEN);
}
如果使用 HAL 的 HAL_DMAEx_... 或 LL 库,原理相同,只是 API 名称不同。
六、必须注意的坑
-
地址与长度必须 32 字节对齐:Cortex-M7 的 Cache 行是 32 字节。
SCB_InvalidateDCache_by_Addr内部会按行处理,但若缓冲区跨行且未对齐,可能误伤相邻数据。 -
缓冲区不要和栈、其他变量共享 Cache 行:否则 Invalidate 会丢掉邻居的脏数据。用
aligned(32)并单独定义。 - Invalidate 前确保没有脏数据:如果 CPU 刚写过该缓冲区,先 Clean 再 Invalidate,或直接用 Clean & Invalidate。
- DTCM 不走 Cache:STM32H7 的 DTCM(0x20000000)默认不经过 Cache,DMA 也无法访问 DTCM。DMA 缓冲区应放在 AXI SRAM 或 SRAM1/2/3。
- 中断里操作要快:Clean/Invalidate 是逐行操作,大缓冲区会耗时,必要时用 MDMA 或分块处理。
- 不要迷信“自动维护”:H7 没有硬件缓存一致性,一切靠软件或 MPU。
七、总结
STM32H7 的 Cache 与 DMA 一致性,本质是“谁先看到谁”的时序问题。记住三句话:
- CPU 写、DMA 读 → 启动 DMA 前 Clean;
- DMA 写、CPU 读 → DMA 完成后 Invalidate;
- 拿不准就配 MPU 为 Non-Cacheable,用性能换省心。
把 Clean/Invalidate 放在正确的步骤,你的 H7 项目就能既跑得快,又不出玄学 Bug。