STM32H7 的 D-Cache 与 DMA 一致性坑:Clean/Invalidate 顺序写错会怎样

STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。但 M7 带有的 D-Cache 在提升性能的同时,也给 DMA 传输带来了严重的数据一致性挑战。很多开发者第一次在 H7 上使用 DMA 时,都会遇到“数据明明发了却不对”“接收缓冲区全是旧值”甚至 HardFault 的问题。根源往往在于 Clean/Invalidate 的顺序写反了

一、为什么会有 D-Cache 一致性问题?

Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据可能只存在于 Cache 中,并未写回 SRAM;同样,DMA 直接读写 SRAM,CPU 却可能从 Cache 中读到旧数据。

  • CPU 写 → DMA 读:CPU 写入的数据可能还在 Cache 里,DMA 从 SRAM 读到的就是旧值。
  • DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 的 Cache 里可能还缓存着该地址的旧数据,CPU 读到的就是旧值。

解决手段就是 Cache 维护操作:

  • Clean:将 Cache 中已修改的数据写回 SRAM。
  • Invalidate:将 Cache 中的对应行标记为无效,下次读取时从 SRAM 重新加载。

二、顺序写错会怎样?

场景 1:发送前只 Invalidate 不 Clean

// 错误示范:发送前只 Invalidate
SCB_InvalidateDCache_by_Addr(tx_buf, len);
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&uart->TDR, len);

CPU 刚写入 tx_buf 的新数据还在 Cache 中,Invalidate 直接丢弃了这些脏数据,DMA 从 SRAM 读到的是旧内容。结果:发送出去的是旧数据。

场景 2:接收后只 Clean 不 Invalidate

// 错误示范:接收完成后只 Clean
HAL_DMA_Start(&hdma, (uint32_t)&uart->RDR, (uint32_t)rx_buf, len);
// ... 等待传输完成 ...
SCB_CleanDCache_by_Addr(rx_buf, len);
// 读取 rx_buf

DMA 已经把新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着该地址的旧行。Clean 只是把 Cache 写回 SRAM,并不会让 CPU 重新从 SRAM 加载。结果:CPU 读到旧数据,甚至可能用旧数据覆盖 DMA 刚写入的新数据。

场景 3:Invalidate 范围未对齐

Cortex-M7 的 Cache 行大小为 32 字节。如果 Invalidate 的地址或长度不是 32 字节对齐,SCB_InvalidateDCache_by_Addr 会操作包含该地址的整个 Cache 行。若该行中还有其它变量,这些变量的最新值可能被丢弃,导致莫名其妙的变量值被改

三、正确的配置步骤

1. 内存属性配置

将 DMA 缓冲区放在 Non-Cacheable 区域是最简单的方案,但会损失性能。若追求性能,应将缓冲区放在 Cacheable 区域,并严格使用维护操作。

// 将 DMA 缓冲区对齐到 32 字节
__attribute__((aligned(32))) uint8_t tx_buf[256];
__attribute__((aligned(32))) uint8_t rx_buf[256];

2. 发送方向(CPU 写 → DMA 读)

先 Clean,再启动 DMA。

// 1. CPU 填充数据
memcpy(tx_buf, data, len);
// 2. Clean:确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
// 3. 启动 DMA
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&uart->TDR, len);

3. 接收方向(DMA 写 → CPU 读)

先启动 DMA,传输完成后再 Invalidate。

// 1. 启动 DMA 前,若缓冲区可能被 CPU 访问过,先 Clean 避免脏数据
SCB_CleanDCache_by_Addr((uint32_t*)rx_buf, len);
// 2. 启动 DMA
HAL_DMA_Start(&hdma, (uint32_t)&uart->RDR, (uint32_t)rx_buf, len);
// 3. 等待传输完成(中断或轮询)
while (DMA_TransferComplete == 0) {}
// 4. Invalidate:丢弃 Cache 中的旧行,强制从 SRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
// 5. 读取数据
process(rx_buf, len);

4. 完整示例:UART DMA 收发

#include "stm32h7xx_hal.h"

__attribute__((aligned(32))) uint8_t tx_buf[64];
__attribute__((aligned(32))) uint8_t rx_buf[64];
volatile uint8_t dma_rx_done = 0;

void uart_dma_send(uint8_t *data, uint16_t len) {
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
    HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buf,
                  (uint32_t)&USART1->TDR, len);
}

void uart_dma_receive_start(uint16_t len) {
    SCB_CleanDCache_by_Addr((uint32_t*)rx_buf, len);
    HAL_DMA_Start_IT(&hdma_usart1_rx, (uint32_t)&USART1->RDR,
                     (uint32_t)rx_buf, len);
}

void DMA1_Stream0_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, 64);
    dma_rx_done = 1;
}

四、注意事项

  • 地址和长度必须 32 字节对齐,否则会误伤相邻变量。
  • Clean 和 Invalidate 不能互相替代:Clean 是写回,Invalidate 是丢弃。
  • 接收方向启动 DMA 前建议先 Clean,防止 Cache 中的脏数据在 DMA 传输期间被意外写回,覆盖 DMA 写入的新数据。
  • MPU 配置 Non-Cacheable 区域可彻底避免一致性问题,但会降低 CPU 访问性能,适合对性能不敏感的缓冲区。
  • 使用 SCB_InvalidateDCache_by_Addr 后,CPU 不能再访问该缓冲区中未对齐的部分,否则可能读到错误数据。
  • 调试时若发现 DMA 数据错乱,优先检查 Cache 维护操作顺序,这是 H7 上最常见的坑。

掌握 Clean/Invalidate 的正确顺序,才能让 STM32H7 的 D-Cache 与 DMA 和谐共处,既享受高性能,又保证数据正确。