摘要:在 STM32H7 上,DMA 采集的数据 CPU 读出来是乱码,或者 CPU 写的数据 DMA 发不出去?不是 DMA 配置错了,而是 Cache 一致性(Cache Coherency)​ 问题。本文解析 Cache 的工作原理及 Clean/Invalidate 操作。


一、问题现象(玄学现场)

场景 1:摄像头通过 DMA 传图到 RAM,CPU 读取图片数据,发现一半是旧的。

场景 2:CPU 更新了发送缓冲区,DMA 发出去的却是上一次的数据。

场景 3:加了 printf调试后,DMA 突然正常了。

这些现象的共同点是:数据在“路上”丢了,而路就是 Cache


二、原理分析:CPU 眼中的“假内存”

1. 物理模型:Cache 是什么?

Cache 是 CPU 和 DDR/AXI-RAM 之间的高速缓冲池

CPU <-> Cache <-> RAM <-> DMA

2. 核心痛点:三个视角,三种真相

当 CPU 写数据到 RAM 时:

角色

看到的数据

CPU

新数据(在 Cache 里)

RAM

旧数据(还没来得及写回去)

DMA

旧数据(读的是 RAM)

结果:DMA 发送了旧数据。

当 DMA 写数据到 RAM 时:

角色

看到的数据

DMA

新数据(写入 RAM)

RAM

新数据

CPU

旧数据(Cache 命中,没去 RAM 读)

结果:CPU 读到了旧数据。

3. 反直觉真相:Cache 策略

  • Write-Back:CPU 写数据只写到 Cache,不立即写回 RAM(快,但有风险)。

  • Write-Through:CPU 写数据同时写 Cache 和 RAM(慢,但安全)。

  • Read Allocate:读缺失时,分配 Cache 行。

默认配置:大多数高端 MCU 默认开启 Write-Back + Read Allocate,这是 Bug 的根源。


三、工程级解决方案

方案 1:Clean(清洗)与 Invalidate(作废)

这是解决 Cache 问题的两个核心操作。

操作

英文名

作用

Clean

SCB_CleanDCache_by_Addr()

把 Cache 里的脏数据强行写回 RAM

Invalidate

SCB_InvalidateDCache_by_Addr()

把 Cache 里的数据标记为无效,下次必须从 RAM 读

2. 实战代码:DMA 发送(CPU -> DMA)

流程:CPU 准备好数据 -> Clean Cache -> 启动 DMA。

// 1. CPU 填充数据
Prepare_Tx_Data(tx_buffer);

// 2. Clean Cache(关键步骤)
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, BUFFER_SIZE);

// 3. 启动 DMA
HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE);

3. 实战代码:DMA 接收(DMA -> CPU)

流程:启动 DMA -> 等待完成 -> Invalidate Cache -> CPU 读取。

// 1. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);

// 2. 等待 DMA 传输完成(中断或轮询)

// 3. Invalidate Cache(关键步骤)
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);

// 4. CPU 读取数据
Process_Rx_Data(rx_buffer);

4. 内存屏障(Memory Barrier)

防止编译器或 CPU 乱序执行。

__DSB(); // 数据同步屏障:确保所有内存访问完成
__ISB(); // 指令同步屏障:清空流水线

用法

SCB_CleanDCache_by_Addr(...);
__DSB(); // 确保 Clean 操作完成后再启动 DMA
HAL_DMA_Start(...);

四、进阶:MPU(内存保护单元)配置

如果不想每次都手动 Clean/Invalidate,可以用 MPU 把 DMA 缓冲区设置为 Non-Cacheable(不可缓存)

MPU_Region_InitTypeDef MPU_InitStruct = {0};

MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24000000; // DMA Buffer 地址
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // 禁止 Cache
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;   // 禁止 Cache
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;

HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);

优点:一劳永逸,代码最简洁。

缺点:该区域的读写速度会变慢(因为没有 Cache 加速)。


五、总结 Checklist

  • [ ] 是否意识到 DMA 和 CPU 看到的 RAM 内容可能不一致?

  • [ ] 发送前是否执行了 SCB_CleanDCache_by_Addr()

  • [ ] 接收后是否执行了 SCB_InvalidateDCache_by_Addr()

  • [ ] 是否配置了 MPU 将 DMA 缓冲区设为 Non-Cacheable?

  • [ ] 启动 DMA 前是否加了 __DSB()屏障?


六、写在最后(关注我,少走弯路)

我是 gqqsherry666,一个拒绝调包、专注底层逻辑的嵌入式架构师。

不带 Cache 的 MCU 是自行车,带了 Cache 的是赛车。

赛车很快,但一旦失控(Cache 不一致),翻车也最惨烈。

懂得驾驭 Cache,你才能真正驾驭高端芯片。

《嵌入式底层硬核分析》系列正式完结。


原创文章,转载请注明出处!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐