一、问题现象:偶发数据错位,重启就好?

在 STM32H7 上使用 DMA 搬运 ADC 采样、串口收发或外设数据时,常遇到:

  • 数据偶尔错位、丢包,概率性出现,重启后可能消失;
  • 调试器单步运行正常,全速运行就出错;
  • 关闭 D-Cache 后一切正常,开启后问题复现。

这类现象几乎都指向 D-Cache 与 DMA 的一致性问题。Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的,CPU 访问的是 Cache,而 DMA 直接访问物理内存(SRAM)。两者看到的数据可能不一致。

二、原理:为什么 Cache 会让 DMA 数据“错位”

2.1 写回与写分配

  • 写回:CPU 写数据只更新 Cache,不立即写回 SRAM,直到该 Cache Line 被替换或手动 Clean。
  • 写分配:CPU 写未命中时,先把该行从 SRAM 读入 Cache,再修改。

2.2 DMA 与 Cache 的冲突场景

  • 发送方向(CPU 写 → DMA 读):CPU 把数据写入缓冲区,数据可能还在 Cache 中未写回 SRAM,DMA 从 SRAM 读到的就是旧数据。
  • 接收方向(DMA 写 → CPU 读):DMA 把新数据写入 SRAM,但 CPU 的 Cache 中可能还缓存着该地址的旧数据,CPU 读到的仍是旧值。

2.3 Clean 与 Invalidate 的正确语义

  • Clean(清理):把 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读 之前。
  • Invalidate(无效化):丢弃 Cache 中的内容,强制下次从 SRAM 重新读取。用于 DMA 写、CPU 读 之前。
  • Clean+Invalidate:既写回又无效化,用于双向缓冲区或不确定场景。

关键:Invalidate 会丢弃未写回的数据!如果 CPU 刚写过缓冲区就 Invalidate,数据会丢失,这正是“误用”的根源。

三、排查步骤:定位一致性 bug

  1. 确认 D-Cache 是否开启:读取 SCB->CCR 的 DC 位,或检查 SCB_EnableDCache() 是否被调用。
  2. 确认 DMA 缓冲区地址与大小:是否按 32 字节对齐(Cache Line 大小),大小是否为 32 字节整数倍。
  3. 检查操作顺序:
    • 发送前是否对缓冲区做了 SCB_CleanDCache_by_Addr()?
    • 接收后是否做了 SCB_InvalidateDCache_by_Addr()?
    • 是否在 CPU 写之后、DMA 启动之前错误地调用了 Invalidate?
  4. 用调试器观察 SRAM 与 Cache:在 DMA 启动前后分别查看物理内存,对比 CPU 读到的值。
  5. 临时关闭 D-Cache 验证:若问题消失,基本可确认是一致性问题。

四、正确配置与完整代码示例

4.1 缓冲区定义:对齐到 Cache Line

#include "stm32h7xx.h"

#define BUF_SIZE  256
#define CACHE_LINE 32

/* 按 32 字节对齐,避免跨 Cache Line 操作 */
__attribute__((aligned(CACHE_LINE))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t dma_rx_buf[BUF_SIZE];

4.2 发送方向:CPU 写 → Clean → 启动 DMA

void dma_send(uint8_t *data, uint16_t len)
{
    /* 1. CPU 填充数据到缓冲区 */
    memcpy(dma_tx_buf, data, len);

    /* 2. 将 Cache 中已修改的数据写回 SRAM */
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);

    /* 3. 启动 DMA 传输 */
    HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)dma_tx_buf,
                  (uint32_t)&USART1->TDR, len);
}

4.3 接收方向:DMA 完成 → Invalidate → CPU 读

/* DMA 接收完成回调 */
void dma_rx_complete_callback(uint16_t len)
{
    /* 1. 无效化 Cache,丢弃旧数据,强制从 SRAM 重新读取 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, len);

    /* 2. 此时 CPU 读取 dma_rx_buf 才能拿到 DMA 写入的新数据 */
    process_data(dma_rx_buf, len);
}

4.4 双向缓冲区:Clean + Invalidate

void dma_bidirectional(uint16_t len)
{
    /* 发送前:Clean 确保 SRAM 数据最新 */
    SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
    HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf,
                  (uint32_t)dma_rx_buf, len);

    /* 等待传输完成... */

    /* 接收后:Invalidate 确保 CPU 读到新数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, len);
}

五、注意事项与避坑指南

  • 地址与长度必须 32 字节对齐:SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 要求地址 32 字节对齐,长度建议为 32 的整数倍,否则可能影响相邻数据。
  • Invalidate 前确保 CPU 不再写该缓冲区:否则未写回的数据会丢失,导致“数据错位”。
  • 不要在 DMA 传输过程中操作 Cache:传输中 Clean/Invalidate 可能破坏正在传输的数据。
  • 使用 MPU 配置 Non-Cacheable 区域:对于频繁 DMA 的缓冲区,可将其配置为 Non-Cacheable,从根本上避免一致性问题,但会牺牲性能。
  • DMA 描述符与缓冲区分离:若使用链表式 DMA,描述符本身也需注意 Cache 一致性。
  • 中断优先级与 Cache 操作:Cache 维护操作本身不是原子的,避免在中断中频繁调用。
  • 调试时关闭优化:高优化等级可能重排指令,掩盖或放大问题,排查时建议先降优化。

六、总结

STM32H7 的 D-Cache 是性能利器,也是 DMA 一致性的“暗礁”。核心原则只有一条:CPU 写、DMA 读之前 Clean;DMA 写、CPU 读之前 Invalidate。牢记操作顺序、对齐要求,并在必要时用 MPU 将 DMA 缓冲区设为 Non-Cacheable,就能彻底告别偶发数据错位。遇到诡异问题时,先问自己:Cache 维护做对了吗?