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

STM32H7 的 Cortex-M7 内核带有一级 D-Cache(通常 16KB),CPU 访问数据时先查 Cache,命中则直接读写 Cache,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,完全绕过 Cache。

这就导致两个经典问题:

  • DMA 写入 SRAM 后,CPU 读到的却是 Cache 里的旧数据(读脏数据)。
  • CPU 写入的数据还在 Cache 中,DMA 却从 SRAM 读到了旧值(写丢失)。

解决思路只有两条:要么把 DMA 缓冲区放到非 Cache 区域(如 DTCM),要么在恰当时机手动维护 Cache 一致性。本文聚焦后者。

二、Cache Line 与地址对齐:一切的基础

Cortex-M7 的 D-Cache 以 Cache Line 为单位操作,STM32H7 的 Cache Line 固定为 32 字节。

关键规则:

  • Clean(清理):把 Cache 中的脏数据写回 SRAM。
  • Invalidate(无效化):丢弃 Cache 内容,下次读时从 SRAM 重新加载。
  • 这两个操作的最小粒度都是 一整条 Cache Line(32 字节),无法只操作其中几个字节。

因此,如果 DMA 缓冲区没有按 32 字节对齐,或者长度不是 32 的整数倍,Invalidate 时就会误伤相邻变量,导致其他数据被意外丢弃。

正确做法:

  • 缓冲区起始地址按 32 字节对齐。
  • 缓冲区大小补齐到 32 字节的整数倍。
  • 推荐使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。
#define ALIGN_32BYTES __attribute__((aligned(32)))

// DMA 接收缓冲区,强制 32 字节对齐
ALIGN_32BYTES static uint8_t dma_rx_buf[256];

// 如果长度不是 32 的倍数,向上取整
#define CACHE_LINE_SIZE  32
#define ALIGN_UP(x)      (((x) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1))

三、Clean 与 Invalidate 的使用时机

根据数据流向,选择不同操作:

  • CPU 写 → DMA 读(发送):CPU 写完数据后,执行 Clean,把数据刷到 SRAM,再启动 DMA。
  • DMA 写 → CPU 读(接收):DMA 完成中断里,先 Invalidate,丢弃 Cache 旧内容,再让 CPU 读。
  • 双向缓冲区:先 Invalidate 再 Clean,或分区域处理,避免误伤。

HAL 库提供了封装函数,位于 stm32h7xx_hal.h:

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

注意:dsize 是字节数,函数内部会自动按 Cache Line 对齐处理,但地址本身必须 32 字节对齐,否则行为未定义。

四、完整实战代码:UART DMA 接收

以 UART 空闲中断 + DMA 接收为例,展示完整流程。

#include "stm32h7xx_hal.h"

#define RX_BUF_SIZE  256
ALIGN_32BYTES static uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t rx_flag = 0;

void uart_dma_init(UART_HandleTypeDef *huart, DMA_HandleTypeDef *hdma)
{
    // 启动 DMA 接收,长度按 32 字节对齐
    HAL_UART_Receive_DMA(huart, rx_buf, ALIGN_UP(RX_BUF_SIZE));
    // 使能空闲中断
    __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
}

// UART 空闲中断回调(在 stm32h7xx_it.c 中调用)
void UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
    if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
        __HAL_UART_CLEAR_IDLEFLAG(huart);

        // 1. 停止 DMA,获取已接收长度
        HAL_UART_DMAStop(huart);
        uint16_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);

        // 2. 关键:无效化 Cache,保证 CPU 读到 DMA 写入的新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, ALIGN_UP(RX_BUF_SIZE));

        // 3. 此时 rx_buf 中的数据才是有效的
        process_data(rx_buf, len);

        // 4. 重新启动 DMA 接收
        HAL_UART_Receive_DMA(huart, rx_buf, ALIGN_UP(RX_BUF_SIZE));
        __HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
    }
}

// 发送示例:CPU 写 → DMA 读
void uart_send_dma(UART_HandleTypeDef *huart, uint8_t *data, uint16_t len)
{
    // 确保数据已刷入 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)data, ALIGN_UP(len));
    HAL_UART_Transmit_DMA(huart, data, len);
}

五、避坑要点总结

  • 地址必须 32 字节对齐:用 ALIGN_32BYTES 修饰所有 DMA 缓冲区,否则 Invalidate 会破坏相邻变量。
  • 长度补齐到 32 的整数倍:ALIGN_UP 宏必不可少,尤其在小缓冲区场景。
  • Invalidate 前先停止 DMA:DMA 还在写时执行 Invalidate,可能丢数据或读到半包。
  • 不要对同一区域反复 Invalidate:若 CPU 刚写过该区域,Invalidate 会丢弃未刷回的数据,应先 Clean。
  • 优先使用 DTCM:DTCM 不经过 Cache,天然一致,适合小容量高频 DMA 缓冲区。
  • MPU 配置非 Cache 区域:对大片 DMA 缓冲区,可用 MPU 将其设为 Non-Cacheable,一劳永逸。
  • 调试时关闭 Cache 对比:若数据错乱,先关 D-Cache 验证是否为一致性问题。

六、结语

D-Cache 与 DMA 的一致性问题是 STM32H7 开发中的高频坑点,核心就三句话:对齐、时机、粒度。只要缓冲区按 32 字节对齐、在正确的时机执行 Clean/Invalidate、并注意 Cache Line 粒度,就能稳定可靠地让 DMA 与 CPU 协同工作。建议将上述封装成通用宏或函数,在项目中统一使用,避免重复踩坑。