DMA 数据乱了?别只怪时序,看看 Cache 一致性与内存屏障
摘要:在 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 |
|
把 Cache 里的脏数据强行写回 RAM |
|
Invalidate |
|
把 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,你才能真正驾驭高端芯片。
《嵌入式底层硬核分析》系列正式完结。
原创文章,转载请注明出处!
更多推荐
所有评论(0)