STM32H7 的 DCache 与 DMA 数据一致性:Clean/Invalidate 时机与踩坑复盘

STM32H7 凭借 480MHz 的 Cortex-M7 和 16KB D-Cache,性能远超 F4/F7。但很多开发者第一次用 DMA 就遇到“数据对不上”“偶发硬件错误”,根源往往在 D-Cache 与 DMA 的数据一致性。本文从原理到代码,帮你彻底理清。

一、为什么 DMA 和 D-Cache 会打架?

Cortex-M7 的 D-Cache 位于 CPU 和总线矩阵之间。CPU 读写内存时,若命中 Cache,实际只操作 Cache 行,不会立刻写回 SRAM。而 DMA 是总线上的独立主设备,它直接访问 SRAM,完全绕过 Cache。

于是出现两种典型问题:

  • CPU 写、DMA 读:CPU 把数据写进 Cache 但未回写 SRAM,DMA 从 SRAM 读到旧数据。
  • DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本。

解决思路只有两条:Clean(把 Cache 脏行写回 SRAM)和 Invalidate(丢弃 Cache 行,强制下次从 SRAM 重读)。

二、Clean 与 Invalidate 的正确时机

记住一个口诀:发送前 Clean,接收后 Invalidate

1. CPU 发送数据给 DMA(内存 → 外设)

DMA 要读 SRAM,必须保证 SRAM 里是最新数据。所以启动 DMA 前,对发送缓冲区执行 Clean

SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, tx_len);
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, tx_len);

2. DMA 接收数据给 CPU(外设 → 内存)

DMA 写 SRAM 后,CPU 的 Cache 里可能还是旧数据。所以 DMA 传输完成中断里,对接收缓冲区执行 Invalidate

void DMA_RxCompleteCallback(void) {
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);
    // 现在可以安全读取 rx_buf
}

3. 双向传输(如 SPI 全双工)

发送前 Clean TX,接收后 Invalidate RX,两者独立操作。

三、完整代码示例:UART DMA 收发

以下代码基于 STM32H743,使用 HAL 库,演示安全的数据收发。

#include "stm32h7xx_hal.h"

#define BUF_SIZE 64
// 注意:缓冲区必须 32 字节对齐,且大小为 32 字节整数倍
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;

// 发送函数:先 Clean,再启动 DMA
void UART_Send_DMA(uint8_t *data, uint16_t len) {
    memcpy(tx_buf, data, len);
    // 关键:Clean 发送缓冲区,确保 SRAM 数据最新
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

// DMA 接收完成回调:先 Invalidate,再处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 关键:Invalidate 接收缓冲区,丢弃 Cache 旧副本
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
        // 此时读取 rx_buf 才是 DMA 写入的新数据
        Process_Data(rx_buf, BUF_SIZE);
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
    }
}

// 初始化时确保 MPU 配置正确(见注意事项)
void Cache_Init(void) {
    // 使能 D-Cache 和 I-Cache
    SCB_EnableICache();
    SCB_EnableDCache();
}

四、踩坑复盘:那些年我们掉过的坑

坑 1:缓冲区未对齐,Clean/Invalidate 越界

SCB_CleanDCache_by_Addr 按 32 字节 Cache 行操作。若缓冲区地址未 32 字节对齐,或长度不是 32 的倍数,会误伤相邻数据,导致其他变量被清掉或回写。

对策:所有 DMA 缓冲区用 __attribute__((aligned(32))),长度向上取整到 32 的倍数。

坑 2:在 DMA 传输过程中 Invalidate

有人图省事,在 DMA 还在写 SRAM 时就 Invalidate,结果 Cache 行被丢弃,但 DMA 后续写入的数据又可能被 CPU 读走旧值,甚至触发总线错误。

对策:Invalidate 必须在 DMA 完成中断里执行,确保传输已结束。

坑 3:忘记配置 MPU,Cache 策略错误

STM32H7 默认 Cache 策略是 Write-Back, Write-Allocate。对于 DMA 缓冲区,最好通过 MPU 配置为 Write-Through 或 Non-Cacheable,减少手动维护。但注意:Non-Cacheable 会降低 CPU 访问性能。

对策:对频繁 DMA 的大缓冲区,用 MPU 设为 Non-Cacheable;小缓冲区用 Clean/Invalidate 手动维护。

坑 4:只 Clean 不 Invalidate,或反之

发送时只 Clean 是对的,但接收时若只 Clean 不 Invalidate,CPU 仍读 Cache 旧值。必须严格按方向操作。

坑 5:中断里调用 Cache 维护函数耗时过长

SCB_InvalidateDCache_by_Addr 会遍历 Cache 行,若缓冲区很大(如几十 KB),在中断里执行可能影响实时性。

对策:大缓冲区建议用 MPU 配置为 Non-Cacheable,或把 Cache 维护放到主循环中处理。

五、总结

STM32H7 的 D-Cache 与 DMA 一致性并不复杂,核心就是:发送前 Clean,接收后 Invalidate,缓冲区对齐,MPU 辅助。理解 Cache 行和总线行为,就能写出既高效又稳定的代码。下次遇到 DMA 数据错乱,先检查 Cache 维护时机,多半能秒杀问题。