一、为什么 H7 上 DMA 会“读到旧数据”?
STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存),默认写回(Write-Back)、写分配(Write-Allocate)。CPU 写内存时,数据可能只停留在 Cache 里,并没有真正落到 SRAM;而 DMA 是直接访问 SRAM 的“物理内存”,它看不到 Cache。于是出现两种经典错误:
- CPU 写、DMA 读:CPU 刚写入的缓冲区还在 Cache 中,DMA 启动后从 SRAM 读到旧值。
- DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本。
解决思路只有两条:发送前 Clean(把 Cache 写回内存),接收后 Invalidate(让 Cache 失效,强制从内存重读)。
二、关键 API 与 Cache Line 概念
CMSIS 提供两个核心函数(需包含 core_cm7.h):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
- Clean:将指定地址范围的 Cache 行写回 SRAM。
- Invalidate:将指定地址范围的 Cache 行标记为无效,下次读取从 SRAM 加载。
注意 Cache Line = 32 字节。操作地址必须 32 字节对齐,长度建议向上取整到 32 的倍数,否则可能误伤相邻数据。
三、配置步骤(以 ADC + DMA 为例)
- 在 SRAM 中定义 DMA 缓冲区,并做 32 字节对齐。
- 启动 DMA 前,若 CPU 写过该缓冲区,调用
SCB_CleanDCache_by_Addr。 - DMA 传输完成中断中,调用
SCB_InvalidateDCache_by_Addr再读数据。 - 若使用
__attribute__((section))放到 DTCM,则无需 Cache 操作(DTCM 不经过 Cache)。
四、完整代码示例
#include "stm32h7xx_hal.h"
#include "core_cm7.h"
#define BUF_LEN 256
#define CACHE_LINE 32
/* 32 字节对齐的 DMA 缓冲区 */
aligned_32 uint16_t adc_buf[BUF_LEN];
volatile uint8_t dma_done = 0;
/* 向上取整到 Cache Line 的辅助宏 */
#define ALIGN_UP(x) (((x) + CACHE_LINE - 1) & ~(CACHE_LINE - 1))
void adc_dma_start(void)
{
/* 若 CPU 预填充过缓冲区,先 Clean 保证 SRAM 数据最新 */
SCB_CleanDCache_by_Addr((uint32_t *)adc_buf, ALIGN_UP(sizeof(adc_buf)));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, BUF_LEN);
}
/* DMA 传输完成回调 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/* 先让 Cache 失效,确保 CPU 读到 DMA 写入的新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, ALIGN_UP(sizeof(adc_buf)));
dma_done = 1;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_DMA_Init();
MX_ADC1_Init();
adc_dma_start();
while (1) {
if (dma_done) {
dma_done = 0;
uint32_t sum = 0;
for (int i = 0; i < BUF_LEN; i++) {
sum += adc_buf[i]; /* 此时数据已一致 */
}
/* 处理 sum ... */
}
}
}
五、踩坑记录与注意事项
-
地址未对齐:
SCB_CleanDCache_by_Addr内部按 32 字节行操作,传入非对齐地址会触发断言或误清相邻变量。务必使用aligned_32或手动对齐。 -
长度未取整:长度不是 32 的倍数时,最后一个 Cache Line 可能未被处理,导致部分数据不一致。用
ALIGN_UP宏统一处理。 - Clean 与 Invalidate 顺序:发送方向先 Clean 再启动 DMA;接收方向先等 DMA 完成再 Invalidate。顺序颠倒会丢数据。
- Invalidate 前必须确保 DMA 已停止:若 DMA 仍在写,Invalidate 可能丢弃刚写入的数据。建议在传输完成中断中操作。
- 多缓冲区/双缓冲:乒乓缓冲时,每个缓冲区都要独立做 Cache 维护,不能只处理首地址。
- MPU 配置:可将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,从根源避免一致性问题,但会牺牲部分性能。
- DTCM 陷阱:DTCM 不经过 D-Cache,放这里无需 Clean/Invalidate,但 DMA 能否访问取决于总线矩阵,需查参考手册。
- 性能开销:频繁 Clean/Invalidate 会拖慢系统,建议缓冲区尽量大、操作次数尽量少。
六、小结
STM32H7 的 D-Cache 是性能利器,也是 DMA 数据一致性的“隐形杀手”。牢记 “CPU 写后 Clean,DMA 写后 Invalidate”,并严格保证地址与长度按 32 字节对齐,就能避开绝大多数数据错乱问题。若项目对实时性要求极高,可结合 MPU 将 DMA 区域设为 Non-Cacheable,用空间换稳定。