STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时序与常见踩坑

STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,并带有 16KB 的 D-Cache 和 I-Cache。开启 D-Cache 后,CPU 访问 SRAM 的命中率大幅提升,但 DMA 直接访问物理内存,不经过 Cache。如果 CPU 先写数据到 Cache 而未写回 SRAM,DMA 读到的就是旧数据;反之 DMA 写入 SRAM 后,CPU 可能仍从 Cache 读到旧值。这就是经典的 Cache 一致性问题。

一、为什么会有缓存一致性问题

Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 写数据时,若 Cache 命中,只更新 Cache 行并标记为脏(Dirty),不会立即写回 SRAM。DMA 是总线主设备,直接访问 SRAM,因此看不到 Cache 中的新数据。

  • CPU 写 → DMA 读:需要先 Clean(写回)Cache,把脏数据刷到 SRAM,再启动 DMA。
  • DMA 写 → CPU 读:DMA 完成后,需要 Invalidate(无效化)对应 Cache 行,让 CPU 下次读取时从 SRAM 重新加载。

二、地址对齐与 Cache 行

Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以 Cache 行为单位,因此缓冲区地址和长度必须按 32 字节对齐,否则会误伤相邻数据。

  • 缓冲区起始地址建议 32 字节对齐:__attribute__((aligned(32)))。
  • 缓冲区长度建议向上取整到 32 字节的整数倍。
  • 若缓冲区跨越两个 Cache 行且未对齐,Invalidate 可能丢弃相邻变量尚未写回的数据,导致“莫名其妙”的变量被清零。
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) static uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) static uint8_t dma_rx_buf[DMA_BUF_SIZE];

三、Clean 与 Invalidate 的时序

3.1 CPU 发送数据(TX)

  1. CPU 填充 dma_tx_buf。
  2. 调用 SCB_CleanDCache_by_Addr() 将脏行写回 SRAM。
  3. 启动 DMA 传输。
  4. 等待 DMA 完成(中断或轮询)。

3.2 CPU 接收数据(RX)

  1. 启动 DMA 接收前,先 Invalidate 对应 Cache 行,避免 Cache 中的旧数据干扰。
  2. 等待 DMA 完成。
  3. 再次 Invalidate,确保 CPU 读到 DMA 写入的新数据。

注意:Invalidate 必须在 DMA 启动前和完成后各做一次,且期间 CPU 不要访问该缓冲区。

四、完整代码示例

以下以 UART DMA 发送为例,展示 Clean 操作。

#include "stm32h7xx_hal.h"

extern UART_HandleTypeDef huart1;

#define TX_SIZE 128
__attribute__((aligned(32))) static uint8_t tx_buf[TX_SIZE];

void uart_dma_send(uint8_t *data, uint16_t len)
{
    /* 1. 拷贝数据到对齐缓冲区 */
    memcpy(tx_buf, data, len);

    /* 2. Clean D-Cache,将 tx_buf 写回 SRAM */
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_SIZE);

    /* 3. 启动 DMA 发送 */
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

接收方向示例:

#define RX_SIZE 128
__attribute__((aligned(32))) static uint8_t rx_buf[RX_SIZE];
volatile uint8_t rx_done = 0;

void uart_dma_start_rx(void)
{
    /* 启动前 Invalidate,丢弃 Cache 中旧数据 */
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
    HAL_UART_Receive_DMA(&huart1, rx_buf, RX_SIZE);
}

/* DMA 完成回调 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        /* 完成后再次 Invalidate,确保读到新数据 */
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_SIZE);
        rx_done = 1;
    }
}

五、MPU 配置:更优雅的方案

如果不想每次手动 Clean/Invalidate,可以用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through。这样 CPU 和 DMA 看到的内存始终一致,代价是访问速度略降。

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = 0x30000000; /* D2 SRAM 示例 */
    MPU_InitStruct.Size             = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable     = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable      = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsShareable      = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number           = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField     = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec      = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

六、常见踩坑清单

  • 缓冲区未 32 字节对齐:Invalidate 会误伤相邻变量,表现为变量随机被清零。
  • Clean/Invalidate 长度不是 32 的倍数:同样会误伤相邻 Cache 行。
  • RX 启动前忘记 Invalidate:Cache 中的旧数据可能被 DMA 覆盖后仍被 CPU 读到。
  • DMA 传输中 CPU 访问缓冲区:Cache 与 DMA 竞争,数据不可预测。
  • 使用 SCB_InvalidateDCache() 全局无效化:会丢弃所有脏数据,导致程序崩溃,应使用 by_Addr 版本。
  • 栈上变量做 DMA 缓冲区:栈地址通常未对齐,且生命周期不可控。
  • 忘记配置 MPU 或未开启 Cache:性能上不去,但一致性反而“正常”,容易掩盖问题。

七、总结

STM32H7 的 D-Cache 是性能利器,但 DMA 一致性必须手动处理。核心原则:TX 前 Clean,RX 前后 Invalidate,缓冲区 32 字节对齐,传输中 CPU 不碰缓冲区。如果嫌麻烦,用 MPU 把 DMA 区域设为 Non-Cacheable 是最省心的方案。掌握这些,你就能在 H7 上放心地跑高速 DMA 了。