一、为什么 H7 上 DMA 会“莫名其妙”出错?

STM32H7 采用 Cortex-M7 内核,带 16KB D-Cache 和 16KB I-Cache。开启 D-Cache 后,CPU 访问 SRAM 会先查缓存,命中则直接读缓存,不访问实际内存。而 DMA 是独立于 CPU 的总线主设备,它直接读写物理 SRAM,不经过 Cache。

这就导致两个经典问题:

  • DMA 写入内存后,CPU 读到的仍是 Cache 中的旧数据(读旧值)。
  • CPU 写入数据还在 Cache 中未回写,DMA 启动后读到的是内存中的旧数据(写丢失)。

根本原因:CPU 视角的数据在 Cache,DMA 视角的数据在 SRAM,两者不同步。

二、Cache 行对齐:最容易忽略的坑

Cortex-M7 的 D-Cache 行大小为 32 字节。Clean/Invalidate 操作以“行”为单位,若缓冲区起始地址或长度未按 32 字节对齐,会误伤相邻数据。

  • Invalidate 的风险:若缓冲区尾部与相邻变量共享同一 Cache 行,Invalidate 会丢弃该行中尚未回写的 CPU 数据,导致相邻变量值丢失。
  • Clean 的风险:若缓冲区头部与相邻变量共享一行,Clean 会把无关数据也写回内存,通常无害但可能覆盖 DMA 刚写入的内容。

避坑准则:

  • DMA 缓冲区必须 32 字节对齐,长度建议为 32 的整数倍。
  • 使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。
  • 若无法对齐,可将缓冲区放在单独的 MPU 区域并配置为 Write-Through 或 Non-Cacheable。
// 推荐:32 字节对齐的 DMA 缓冲区
#define DMA_BUF_SIZE  256
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];

三、Clean 与 Invalidate 的正确语义

  • Clean(清理):将 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读 的场景(如发送)。
  • Invalidate(无效化):丢弃 Cache 中的内容,强制下次读取从 SRAM 加载。用于 DMA 写、CPU 读 的场景(如接收)。
  • Clean+Invalidate:先写回再丢弃,用于双向场景或不确定状态时。

关键原则:操作顺序不能错。

  • 发送前:CPU 填好数据 → SCB_CleanDCache_by_Addr() → 启动 DMA。
  • 接收后:DMA 完成 → SCB_InvalidateDCache_by_Addr() → CPU 读数据。

注意:Invalidate 必须在 DMA 完成之后、CPU 读取之前执行。若在 DMA 传输过程中 Invalidate,可能丢弃 DMA 刚写入但尚未完成的数据。

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

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

#include "stm32h7xx.h"

#define RX_BUF_SIZE  128
__attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t rx_flag = 0;
volatile uint16_t rx_len = 0;

void uart_dma_init(void)
{
    // 1. 配置 UART 和 DMA(略去底层寄存器,可用 HAL)
    // 2. 启动 DMA 接收
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);
}

// DMA 空闲回调(HAL 中为 HAL_UARTEx_RxEventCallback)
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1) {
        rx_len = Size;
        // 关键:DMA 已写入 SRAM,但 CPU Cache 中可能是旧数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
        rx_flag = 1;
    }
}

void process_rx_data(void)
{
    if (rx_flag) {
        rx_flag = 0;
        // 此时读取 rx_buf 才是 DMA 写入的最新数据
        for (uint16_t i = 0; i < rx_len; i++) {
            // 处理 rx_buf[i]
        }
        // 重新启动接收前,确保缓冲区状态一致
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buf, RX_BUF_SIZE);
    }
}

// 发送示例
void uart_send_dma(uint8_t *data, uint16_t len)
{
    // 确保数据在 SRAM 中最新
    SCB_CleanDCache_by_Addr((uint32_t *)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

五、MPU 配置:一劳永逸的方案

若不想每次手动 Clean/Invalidate,可用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through。

  • Non-Cacheable:CPU 直接访问 SRAM,无一致性问题,但性能略低。
  • Write-Through:写操作同时更新 Cache 和 SRAM,读仍走 Cache,适合读多写少。
// MPU 配置示例(将 0x30000000 开始的 64KB 配置为 Non-Cacheable)
void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    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_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

六、注意事项与避坑清单

  • 对齐是前提:所有 DMA 缓冲区必须 32 字节对齐,长度建议 32 的倍数。
  • 顺序不能反:发送先 Clean,接收后 Invalidate,切勿在 DMA 传输中操作 Cache。
  • Invalidate 有风险:若缓冲区与其它变量共享 Cache 行,Invalidate 会丢数据,优先用对齐或 MPU。
  • 多缓冲区要分别处理:每个 DMA 缓冲区独立 Clean/Invalidate,不能只处理一个。
  • 中断中调用:SCB_*DCache_by_Addr 可在中断中调用,但注意耗时,避免影响实时性。
  • 调试时关闭 Cache:若问题诡异,先关 D-Cache 验证是否为一致性问题。
  • HAL 库的坑:部分 HAL 版本在 DMA 收发中未自动处理 Cache,需手动添加。

掌握以上要点,STM32H7 的 D-Cache 与 DMA 就能稳定协同,既享受高主频性能,又避免数据错乱。