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

STM32H7 系列凭借 Cortex-M7 内核和 L1 Cache 实现了高主频下的零等待执行,但 DCache 的引入也让 DMA 传输变得“危机四伏”。如果你曾遇到 DMA 发送的数据错乱、接收数据被覆盖,或程序在开启 DCache 后莫名 HardFault,那大概率是踩中了缓存一致性的坑。本文从原理到实践,帮你彻底理清。

一、为什么 DCache 会与 DMA 冲突?

Cortex-M7 的 L1 DCache 是**写回(Write-Back)**策略:CPU 写数据时只更新 Cache,不立即写回 SRAM;读数据时若 Cache 未命中,则从 SRAM 加载并缓存。

DMA 则直接访问 SRAM(物理内存),不经过 Cache。这就导致:

  • CPU 写 → DMA 读:数据可能还在 Cache 中,DMA 读到的是 SRAM 里的旧值。
  • DMA 写 → CPU 读:DMA 更新了 SRAM,但 CPU 读到的却是 Cache 中的旧数据。

解决思路只有两个:Clean(清理)Invalidate(无效化)

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

二、地址对齐:32 字节的硬性要求

Cortex-M7 的 Cache Line 为 32 字节。Clean/Invalidate 操作以 Cache Line 为单位,因此操作地址必须 32 字节对齐,长度也建议为 32 字节的整数倍

若缓冲区未对齐,Clean 或 Invalidate 会波及相邻数据,导致“误伤”。例如,一个 20 字节的缓冲区位于地址 0x20000010,Invalidate 时会覆盖 0x20000000~0x2000001F 整个 Cache Line,可能丢弃相邻变量的最新值。

解决方案

  • 使用 __attribute__((aligned(32))) 强制 32 字节对齐。
  • 缓冲区大小向上取整到 32 的倍数。
  • 或者使用 MPU 将 DMA 缓冲区配置为 Write-Through/Non-Cacheable,但会牺牲性能。
// 推荐:32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];

三、Clean/Invalidate 的正确时机

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

在启动 DMA 传输之前,对发送缓冲区执行 Clean 操作,确保数据已写回 SRAM。

SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, sizeof(dma_tx_buf));
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf, (uint32_t)&periph, len);

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

在 DMA 传输完成之后,对接收缓冲区执行 Invalidate 操作,丢弃 Cache 中的旧数据。

// 在 DMA 完成中断或轮询等待完成后
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
// 此时 CPU 读取 dma_rx_buf 才能得到 DMA 写入的新数据

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

发送前 Clean TX 缓冲区,接收完成后 Invalidate RX 缓冲区。注意:若 TX 和 RX 是同一缓冲区,需先 Clean 再启动,完成后 Invalidate。

四、完整代码示例(以 UART DMA 接收为例)

#include "stm32h7xx.h"

#define RX_BUF_SIZE  128
__attribute__((aligned(32))) uint8_t uart_rx_buf[RX_BUF_SIZE];

void uart_dma_init(void) {
    // 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, uart_rx_buf, RX_BUF_SIZE);
}

// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 关键:Invalidate 接收缓冲区,使 CPU 能读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, RX_BUF_SIZE);
        // 此时可以安全处理 uart_rx_buf 中的数据
        process_data(uart_rx_buf, RX_BUF_SIZE);
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, uart_rx_buf, RX_BUF_SIZE);
    }
}

// 发送函数
void uart_send_dma(uint8_t *data, uint16_t len) {
    // 确保长度和地址对齐(简化处理,实际应确保缓冲区对齐)
    SCB_CleanDCache_by_Addr((uint32_t *)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

五、踩坑复盘与注意事项

  • 坑 1:忘记 Invalidate 导致接收数据“不变”。DMA 明明收到了新数据,但 CPU 读到的还是上一次的旧值。原因就是 Cache 未失效。
  • 坑 2:缓冲区未对齐导致相邻变量被破坏。Invalidate 操作波及相邻 Cache Line,把其他变量的最新值丢弃。务必使用 aligned(32)
  • 坑 3:在 DMA 传输过程中 Invalidate。若 DMA 正在写缓冲区,此时 Invalidate 可能丢弃已写入的部分数据。必须在 DMA 完成后再操作。
  • 坑 4:Clean 和 Invalidate 顺序颠倒。发送前应 Clean,接收后应 Invalidate,切勿混淆。
  • 坑 5:使用 SCB_InvalidateDCache() 全量失效。这会清空整个 DCache,严重影响性能,且可能影响其他任务。应使用 SCB_InvalidateDCache_by_Addr() 按地址操作。
  • 注意:若使用 STM32CubeMX 生成的代码,默认可能未开启 DCache。需在 main() 中调用 SCB_EnableDCache(),并确保 MPU 配置正确。
  • 替代方案:对于频繁小数据量传输,可将 DMA 缓冲区配置为 Non-Cacheable(通过 MPU),避免手动维护一致性,但会降低 CPU 访问速度。

六、总结

STM32H7 的 DCache 与 DMA 共存的核心就是对齐、Clean、Invalidate三要素。牢记:

  • 缓冲区 32 字节对齐;
  • 发送前 Clean,接收后 Invalidate;
  • 操作地址和长度按 Cache Line 对齐;
  • 在 DMA 完成后再 Invalidate。

掌握这些,你就能在 H7 上既享受 Cache 的性能,又保证 DMA 的数据一致性。