一、为什么 Cache 与 DMA 会“打架”?

STM32H7 内置 16KB I-Cache 和 16KB D-Cache,CPU 访问数据时优先走 Cache,而 DMA 直接访问物理内存(SRAM/SDRAM)。当 CPU 写数据到 Cache 但尚未写回内存时,DMA 读取的是旧数据;反之,DMA 更新了内存,CPU 却可能从 Cache 读到旧值。这就是Cache 一致性问题。

典型场景:

  • ADC 通过 DMA 采集数据到数组,CPU 读数组发现全是 0 或旧值。
  • CPU 填充发送缓冲区后启动 DMA 发送,对方收到乱码。

二、解决思路:MPU 配置 + Cache 维护操作

2.1 MPU 配置:为 DMA 缓冲区设置正确属性

最根本的方法是使用 MPU 将 DMA 缓冲区配置为 Non-CacheableWrite-Through,避免 Cache 介入。推荐 Non-Cacheable,简单可靠。

#include "stm32h7xx_hal.h"

// 定义 DMA 缓冲区,必须 32 字节对齐
__attribute__((aligned(32))) uint8_t dma_buffer[1024];

void MPU_Config(void)
{
    HAL_MPU_Disable();

    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 配置 DMA 缓冲区所在区域为 Non-Cacheable
    MPU_InitStruct.Enable           = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress      = (uint32_t)dma_buffer;
    MPU_InitStruct.Size             = MPU_REGION_SIZE_1KB; // 根据实际大小调整
    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);
}

注意:MPU 区域大小必须是 2 的幂,且基地址按大小对齐。若缓冲区跨区域,需拆分多个 MPU 区域。

2.2 如果必须使用 Cache:Clean/Invalidate 操作

有时为了性能,DMA 缓冲区仍配置为 Cacheable,此时必须手动维护一致性。

  • CPU 写 → DMA 读:在启动 DMA 前执行 SCB_CleanDCache_by_Addr(),将 Cache 数据写回内存。
  • DMA 写 → CPU 读:在 DMA 完成后执行 SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧数据。
// 发送前:清理 D-Cache
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer));
HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));

// 接收完成回调中:无效化 D-Cache
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    // 现在可以安全读取 rx_buffer
}

三、完整实战示例:UART DMA 收发

以下代码展示在 Cacheable 内存下,如何正确使用 Clean/Invalidate。

#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

void UART_DMA_Init(void)
{
    // 填充发送数据
    for (int i = 0; i < BUF_SIZE; i++) tx_buf[i] = i;

    // 1. 清理 D-Cache,确保内存中数据最新
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, BUF_SIZE);

    // 2. 启动 DMA 发送
    HAL_UART_Transmit_DMA(&huart1, tx_buf, BUF_SIZE);

    // 3. 启动 DMA 接收
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

// 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 4. 无效化 D-Cache,丢弃旧数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);

    // 5. 安全读取
    for (int i = 0; i < BUF_SIZE; i++) {
        printf("%02X ", rx_buf[i]);
    }
}

四、避坑指南:常见错误与注意事项

  • 地址未对齐:Clean/Invalidate 操作要求地址 32 字节对齐,长度也建议 32 字节倍数。否则可能触发 HardFault 或遗漏数据。
  • Invalidate 前未 Clean:若缓冲区部分数据在 Cache 中已修改但未写回,直接 Invalidate 会丢失修改。正确顺序:先 Clean 再 Invalidate。
  • DMA 传输中操作 Cache:DMA 正在访问内存时执行 Clean/Invalidate 会导致数据错乱。务必在 DMA 完成中断或传输启动前操作。
  • MPU 配置遗漏:忘记配置 MPU 或区域大小不匹配,导致 Cache 属性未生效。建议用 SCB_GetCacheLevel() 确认。
  • 多缓冲区共享 Cache 行:若两个变量位于同一 32 字节 Cache 行,Invalidate 一个可能影响另一个。务必按 32 字节对齐隔离。
  • 中断优先级:DMA 完成中断优先级应低于或等于调用 Clean/Invalidate 的任务,避免竞态。

五、总结

STM32H7 的 Cache 与 DMA 一致性没有银弹。推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单且零开销。若必须使用 Cache,则严格遵守“写前 Clean,读后 Invalidate”原则,并注意对齐与操作时机。掌握这些技巧,你的嵌入式系统将既快又稳。