STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 误用导致的偶发数据错乱定位方法

一、为什么 H7 上 DMA 会“偶发”出错

STM32H7 的 Cortex-M7 内核带有一级 D-Cache(通常 16KB)。CPU 访问 SRAM 时,数据可能只落在 Cache 中,并未写回物理内存;同理,DMA 直接改写物理内存后,CPU 读到的却可能是 Cache 里的旧值。

  • CPU 写 → DMA 读:CPU 写入的数据还在 Cache 里,DMA 从 SRAM 读到旧数据。
  • DMA 写 → CPU 读:DMA 已更新 SRAM,但 CPU 命中了 Cache 中的旧数据。

这类问题往往表现为“偶发”“概率性”“重启后正常”,极难定位。根因通常是 Clean/Invalidate 的时机或范围用错。

二、Clean 与 Invalidate 的正确语义

  • Clean(清理):把 Cache 中“脏”数据写回物理内存。用于 CPU 写、DMA 读 之前。
  • Invalidate(无效化):丢弃 Cache 中的旧数据,强制下次从内存重新加载。用于 DMA 写、CPU 读 之后。
  • Clean+Invalidate:既写回又丢弃,用于缓冲区被双向使用或地址范围可能重叠时。

关键原则:操作方向决定用哪个。写反了,问题只会更隐蔽。

三、典型误用场景

  1. 发送前只 Invalidate 不 Clean:CPU 刚填好的发送缓冲区被 Invalidate 丢弃,DMA 发出旧数据。
  2. 接收后只 Clean 不 Invalidate:CPU 仍读到 Cache 旧值,DMA 新数据被覆盖。
  3. 按字节地址而非 32 字节对齐操作:H7 的 Cache 行是 32 字节,非对齐范围会波及相邻变量,造成“无辜”数据被破坏。
  4. DMA 传输过程中 CPU 访问缓冲区:传输未完成就 Clean/Invalidate,导致数据撕裂。

四、系统化定位方法

  • 第一步:确认缓冲区是否在 Cacheable 区域。H7 的 AXI SRAM(0x24000000)默认 Cacheable,DTCM(0x20000000)不经过 Cache。
  • 第二步:用 MPU 将 DMA 缓冲区配置为 Non-Cacheable,作为对照实验。若问题消失,基本锁定 Cache 一致性。
  • 第三步:检查 Clean/Invalidate 的调用时机与方向,对照第二节原则。
  • 第四步:检查地址与长度是否 32 字节对齐,必要时用 SCB_InvalidateDCache_by_Addr 并向上取整。
  • 第五步:在 DMA 完成中断里加内存屏障 __DSB(),确保操作顺序。

五、完整代码示例

以下以 UART DMA 接收为例,展示正确用法。

#include "stm32h7xx_hal.h"

#define RX_BUF_SIZE  256
/* 必须 32 字节对齐,且长度按 32 字节向上取整 */
__attribute__((aligned(32))) uint8_t rxBuf[RX_BUF_SIZE];

/* 启动 DMA 接收前:Invalidate,丢弃旧 Cache,让 DMA 写入内存 */
void UART_DMA_StartReceive(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuf, RX_BUF_SIZE);
    __DSB();  /* 确保 Invalidate 完成后再启动 DMA */
    HAL_UART_Receive_DMA(huart, rxBuf, RX_BUF_SIZE);
}

/* DMA 接收完成回调:Invalidate,让 CPU 读到 DMA 写入的新数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuf, RX_BUF_SIZE);
    __DSB();
    /* 此时 CPU 读取 rxBuf 才是 DMA 写入的真实数据 */
    ProcessData(rxBuf, RX_BUF_SIZE);
}

/* DMA 发送:CPU 填好数据后 Clean,确保 DMA 读到最新数据 */
__attribute__((aligned(32))) uint8_t txBuf[RX_BUF_SIZE];

void UART_DMA_Send(UART_HandleTypeDef *huart)
{
    FillTxData(txBuf, RX_BUF_SIZE);
    SCB_CleanDCache_by_Addr((uint32_t *)txBuf, RX_BUF_SIZE);
    __DSB();
    HAL_UART_Transmit_DMA(huart, txBuf, RX_BUF_SIZE);
}

若缓冲区被双向使用,使用 SCB_CleanInvalidateDCache_by_Addr()。

六、注意事项

  • 长度必须 32 字节对齐:H7 Cache 行 32 字节,非对齐会误伤相邻数据。
  • 地址必须 32 字节对齐:用 __attribute__((aligned(32))) 或 ALIGN_32BYTES。
  • DMA 传输期间禁止 CPU 访问缓冲区,否则 Clean/Invalidate 会破坏进行中的传输。
  • DTCM 不经过 Cache,把 DMA 缓冲区放 DTCM 可彻底规避,但 DTCM 容量有限且部分外设无法访问。
  • MPU 配置 Non-Cacheable 是更稳妥的方案,代价是 CPU 访问变慢。
  • 调试时先关 D-Cache 验证,确认问题是否由 Cache 引起,再逐步开启。

七、小结

STM32H7 的 D-Cache 与 DMA 一致性问题的核心是:谁写谁读,决定 Clean 还是 Invalidate。牢记“CPU 写→Clean,DMA 写→Invalidate”,并保证 32 字节对齐与正确的内存屏障,就能把这类“偶发数据错乱”从玄学变成可定位、可复现的工程问题。