一、为什么 Cache 会让 DMA 数据“出错”?
STM32H7 的 Cortex-M7 内核带有 L1 D-Cache(数据缓存),默认写回(Write-Back)、写分配(Write-Allocate)策略。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 RAM。而 DMA 控制器直接访问物理内存,不经过 Cache。
这就导致两类经典问题:
- CPU 写,DMA 读:CPU 修改了缓冲区数据,但数据还在 Cache 里(脏行),DMA 从 RAM 读到的是旧数据。
- DMA 写,CPU 读:DMA 把新数据写入 RAM,但 CPU 读的是 Cache 中的旧副本(若该行之前被缓存过)。
解决思路:在 DMA 传输前后,对缓冲区执行 Clean(将 Cache 脏行写回 RAM)或 Invalidate(丢弃 Cache 行,强制从 RAM 重新加载)。
二、Clean 与 Invalidate 的正确使用场景
| 方向 | 操作 | 时机 | |------|------|------| | CPU 写 → DMA 读 | Clean(写回) | DMA 启动前 | | DMA 写 → CPU 读 | Invalidate(无效化) | DMA 完成后 | | DMA 双向读写 | Clean + Invalidate | 启动前 Clean,完成后 Invalidate |
关键原则:
- 对同一缓冲区,不要同时存在 CPU 和 DMA 的并发访问,必须用软件同步(如等待传输完成标志)。
- Invalidate 会丢弃未写回的数据,若缓冲区有 CPU 未 Clean 的脏数据,直接 Invalidate 将导致数据丢失。
- 缓冲区地址和大小必须按 Cache 行(32 字节)对齐,否则 Clean/Invalidate 可能影响相邻变量。
三、完整代码示例(以 ADC + DMA 为例)
假设使用 ADC1 连续扫描,DMA 循环模式将数据搬运到 adc_buf[256],CPU 定期读取。
#include "stm32h7xx.h"
#define BUF_SIZE 256
// 按 32 字节对齐,且放在非 Cache 区域或普通 RAM 均可
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];
void cache_clean(uint32_t addr, uint32_t size) {
uint32_t start = addr & ~0x1F; // 向下对齐到 32 字节
uint32_t end = (addr + size + 31) & ~0x1F; // 向上对齐
SCB_CleanDCache_by_Addr((uint32_t *)start, end - start);
}
void cache_invalidate(uint32_t addr, uint32_t size) {
uint32_t start = addr & ~0x1F;
uint32_t end = (addr + size + 31) & ~0x1F;
SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start);
}
void adc_dma_init(void) {
// 此处省略 ADC、DMA、GPIO 初始化代码
// 启动 DMA 前,若 CPU 曾写过 adc_buf,需 Clean
cache_clean((uint32_t)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, BUF_SIZE);
}
void process_adc_data(void) {
// 等待 DMA 传输完成(实际项目用标志或回调)
while (!dma_done_flag);
dma_done_flag = 0;
// DMA 写入了新数据,CPU 读取前必须 Invalidate
cache_invalidate((uint32_t)adc_buf, sizeof(adc_buf));
for (int i = 0; i < BUF_SIZE; i++) {
// 此时读取的是 RAM 中的最新数据
uint16_t val = adc_buf[i];
// ... 处理
}
}
四、常见踩坑与排查方法
坑 1:缓冲区未对齐,误伤相邻变量
若 adc_buf 未按 32 字节对齐,Clean/Invalidate 会覆盖相邻内存,导致其他变量被意外修改或丢失。
排查:检查缓冲区地址 % 32 == 0,使用 __attribute__((aligned(32))) 强制对齐。
坑 2:Invalidate 前未 Clean,丢失 CPU 写入
例如 CPU 先填充了发送缓冲区,然后启动 DMA 发送,但忘记 Clean,DMA 发出旧数据;或者 DMA 接收后,CPU 直接 Invalidate,把之前未 Clean 的配置数据丢弃。
排查:明确数据流向,遵循“写前 Clean,读后 Invalidate”原则。
坑 3:DMA 传输中 CPU 访问缓冲区
DMA 正在写 RAM 时,CPU 若访问同一缓冲区,可能触发 Cache 与 DMA 的竞争,数据不可预测。
排查:使用 DMA 完成中断或标志,确保传输结束后再操作缓冲区。
坑 4:多缓冲区或链表模式下的遗漏
使用 DMA 双缓冲或链表时,容易忘记对每个缓冲区分别做 Cache 维护。
排查:为每个缓冲区单独调用 Clean/Invalidate,或使用 MPU 将缓冲区配置为 Write-Through/Non-Cacheable。
坑 5:MPU 配置不当
若将 DMA 缓冲区所在区域配置为 Non-Cacheable,则无需 Clean/Invalidate,但会降低 CPU 访问性能。若配置为 Write-Back 却忘记维护,则出现一致性问题。
排查:检查 MPU 区域属性,确保与软件维护策略一致。
五、系统化排查步骤
- 确认现象:数据错位、旧值、随机跳变,且与 DMA 相关。
- 检查对齐:缓冲区地址和大小是否 32 字节对齐。
- 检查操作顺序:DMA 启动前是否 Clean?完成后是否 Invalidate?
- 检查并发:DMA 传输期间 CPU 是否访问了缓冲区?
- 检查 MPU:缓冲区区域是否被错误缓存?
- 使用调试手段:在 Clean/Invalidate 前后读取内存,对比 Cache 与 RAM 内容;或暂时将缓冲区设为 Non-Cacheable 验证问题是否消失。
六、总结
STM32H7 的 Cache 与 DMA 数据一致性并非无解,核心是理解“CPU 视角”与“DMA 视角”的差异,并严格遵循 Clean/Invalidate 的使用时机。牢记三点:对齐、同步、按需维护。在复杂项目中,可结合 MPU 将 DMA 缓冲区设为 Non-Cacheable 以简化设计,但需权衡性能。掌握这些技巧,即可让 H7 的高性能与 DMA 的高效传输完美共存。