一、为什么 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 区域属性,确保与软件维护策略一致。

五、系统化排查步骤

  1. 确认现象:数据错位、旧值、随机跳变,且与 DMA 相关。
  2. 检查对齐:缓冲区地址和大小是否 32 字节对齐。
  3. 检查操作顺序:DMA 启动前是否 Clean?完成后是否 Invalidate?
  4. 检查并发:DMA 传输期间 CPU 是否访问了缓冲区?
  5. 检查 MPU:缓冲区区域是否被错误缓存?
  6. 使用调试手段:在 Clean/Invalidate 前后读取内存,对比 Cache 与 RAM 内容;或暂时将缓冲区设为 Non-Cacheable 验证问题是否消失。

六、总结

STM32H7 的 Cache 与 DMA 数据一致性并非无解,核心是理解“CPU 视角”与“DMA 视角”的差异,并严格遵循 Clean/Invalidate 的使用时机。牢记三点:对齐、同步、按需维护。在复杂项目中,可结合 MPU 将 DMA 缓冲区设为 Non-Cacheable 以简化设计,但需权衡性能。掌握这些技巧,即可让 H7 的高性能与 DMA 的高效传输完美共存。