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

Cortex-M7 的 D-Cache 是**写回(write-back)**策略:CPU 写数据时只更新 Cache,不立即写回 SRAM。而 DMA 直接访问物理 SRAM,它看不到 Cache 里的新数据。

  • CPU 写、DMA 读:数据还在 Cache 里,DMA 读到旧值。
  • DMA 写、CPU 读:DMA 更新了 SRAM,但 CPU 读的是 Cache 里的旧副本。

解决思路只有两个:要么让该区域不走 Cache(MPU 配置为 Device/Strongly Ordered),要么在恰当时机手动 clean/invalidate

二、MPU 配置:给 DMA 缓冲区划一块“安全区”

最稳妥的做法是把 DMA 缓冲区所在 SRAM 区域通过 MPU 配置为 Non-cacheable。以 STM32H743 的 0x30000000(D2 SRAM)为例:

#include "stm32h7xx.h"

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_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
  • IsCacheable = NOT_CACHEABLE:CPU 访问直接读写 SRAM,与 DMA 一致。
  • IsShareable = SHAREABLE:保证多总线主控(CPU、DMA)之间的一致性。
  • 注意:MPU 配置必须在 main() 最开头、使能 Cache 之前调用。

三、clean 与 invalidate 的时机

如果不想牺牲性能(比如大块数据搬运),可以让缓冲区走 Cache,但必须手动维护一致性。CMSIS 提供两个函数:

  • SCB_CleanDCache_by_Addr(addr, size):把 Cache 里的脏数据写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,下次读从 SRAM 取。

核心口诀:

  • CPU 写 → DMA 读:先 Clean,再启动 DMA。
  • DMA 写 → CPU 读:DMA 完成后先 Invalidate,再读数据。
  • 地址必须 32 字节对齐,长度建议按 32 字节向上取整。
#define BUF_SIZE  256
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];

// CPU 准备数据发给 DMA 前
SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, BUF_SIZE * 2);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);

// DMA 完成回调中
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, BUF_SIZE * 2);
    // 现在可以安全读取 adc_buf
}

四、完整实战:ADC + DMA + D-Cache

#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

#define ADC_BUF_LEN  128
__attribute__((aligned(32))) uint16_t adc_dma_buf[ADC_BUF_LEN];

void SystemClock_Config(void);
void MX_DMA_Init(void);
void MX_ADC1_Init(void);

int main(void)
{
    HAL_Init();
    MPU_Config();                 // 先配 MPU
    SCB_EnableICache();
    SCB_EnableDCache();           // 再开 Cache
    SystemClock_Config();
    MX_DMA_Init();
    MX_ADC1_Init();

    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_BUF_LEN);

    while (1)
    {
        HAL_Delay(500);
        // 若 MPU 已配 Non-cacheable,可直接读;否则需先 Invalidate
        SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, ADC_BUF_LEN * 2);
        uint32_t sum = 0;
        for (int i = 0; i < ADC_BUF_LEN; i++)
            sum += adc_dma_buf[i];
        uint16_t avg = sum / ADC_BUF_LEN;
        (void)avg;
    }
}

五、注意事项与避坑指南

  • 对齐是硬性要求SCB_*DCache_by_Addr 的地址和长度必须 32 字节对齐,否则触发 HardFault。
  • 不要对同一区域重复 Invalidate:若 CPU 刚写过该区域且未 Clean,Invalidate 会丢失数据。
  • MPU 与手动维护二选一:混用容易出错,建议 DMA 缓冲区统一走 Non-cacheable 区域。
  • DMA 描述符也要注意:若使用链表模式,描述符所在内存同样需 Non-cacheable 或手动 Clean。
  • 调试时先关 Cache:定位诡异问题时,先 SCB_DisableDCache() 排除缓存因素。

掌握 MPU 配置与 clean/invalidate 时机,就能在 STM32H7 上既享受 D-Cache 的高性能,又保证 DMA 数据万无一失。