一、为什么 D-Cache 会让 DMA 数据“错乱”?

STM32H7 的 Cortex-M7 内核带 16KB D-Cache,CPU 访问数据时优先走 Cache,而 DMA 直接访问物理内存(SRAM)。当两者访问同一块缓冲区时,就会出现:

  • CPU 写数据后未回写:DMA 读到的是旧值(Cache 未 Clean)。
  • DMA 写入新数据后 CPU 读旧值:Cache 中仍是旧副本(Cache 未 Invalidate)。
  • Cache 行对齐问题:Invalidate 操作以 32 字节行为单位,若缓冲区未对齐,会误伤相邻变量。

因此,必须遵循 “谁写谁 Clean,谁读谁 Invalidate” 的原则,并保证缓冲区按 Cache 行对齐。

二、Cache 行对齐:32 字节是硬性要求

Cortex-M7 的 D-Cache 行大小为 32 字节SCB_InvalidateDCache_by_Addr 等函数内部按行操作,若地址未对齐,会:

  • 向上取整到行首,向下取整到行尾,导致相邻数据被意外失效或回写。
  • 若相邻变量是 DMA 正在使用的,可能引发数据竞争甚至 HardFault。

正确做法

  • 使用 __attribute__((aligned(32)))ALIGN_32BYTES 宏。
  • 缓冲区大小最好是 32 的整数倍,避免末尾部分行被误操作。
#define DMA_BUF_SIZE  128
__attribute__((aligned(32))) uint8_t dma_buf[DMA_BUF_SIZE];

三、Clean 与 Invalidate 的时序

3.1 发送方向(CPU → 外设)

  1. CPU 填充缓冲区。
  2. Clean D-Cache(将最新数据写回 SRAM)。
  3. 启动 DMA 发送。
  4. DMA 完成后,若 CPU 要修改缓冲区,需再次 Clean 或直接操作非缓存区。

3.2 接收方向(外设 → CPU)

  1. 启动 DMA 接收前,Invalidate D-Cache(丢弃旧副本,避免 CPU 读到脏数据)。
  2. DMA 完成后,再次 Invalidate(确保 CPU 读到 DMA 写入的新数据)。
  3. 注意:Invalidate 前必须确保 CPU 没有未回写的脏数据,否则会丢失。

关键点:Invalidate 会丢弃 Cache 中未回写的数据,因此若 CPU 刚写过该区域,必须先 Clean 再 Invalidate。

四、完整示例:ADC + DMA 采集

以下代码基于 STM32H743,使用 ADC1 + DMA1_Stream0,采集 128 个样本。

#include "stm32h7xx.h"

#define ADC_BUF_SIZE  128
__attribute__((aligned(32))) static uint16_t adc_buf[ADC_BUF_SIZE];
volatile uint8_t adc_done = 0;

void ADC_DMA_Init(void)
{
    // 1. 使能时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;

    // 2. 配置 DMA1_Stream0
    DMA1_Stream0->CR &= ~DMA_SxCR_EN;
    while (DMA1_Stream0->CR & DMA_SxCR_EN);
    DMA1_Stream0->PAR  = (uint32_t)&ADC1->DR;
    DMA1_Stream0->M0AR = (uint32_t)adc_buf;
    DMA1_Stream0->NDTR = ADC_BUF_SIZE;
    DMA1_Stream0->CR   = DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 |
                         DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC |
                         DMA_SxCR_CIRC | DMA_SxCR_TCIE;
    DMA1_Stream0->FCR  = 0;

    // 3. 配置 ADC1
    ADC1->CFGR = ADC_CFGR_DMAEN | ADC_CFGR_DMACFG;
    ADC1->SQR1 = (ADC_BUF_SIZE - 1) << ADC_SQR1_L_Pos;
    ADC1->SQR3 = 0; // 通道 0
    ADC1->CR   = ADC_CR_ADEN;
    while (!(ADC1->ISR & ADC_ISR_ADRDY));
    ADC1->CR  |= ADC_CR_ADSTART;

    // 4. 启动 DMA
    DMA1_Stream0->CR |= DMA_SxCR_EN;

    // 5. 使能中断
    NVIC_EnableIRQ(DMA1_Stream0_IRQn);
}

void DMA1_Stream0_IRQHandler(void)
{
    if (DMA1->LISR & DMA_LISR_TCIF0) {
        DMA1->LIFCR = DMA_LIFCR_CTCIF0;

        // 关键:DMA 写入后,Invalidate D-Cache
        SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));

        adc_done = 1;
    }
}

// 主循环中处理数据
void Process_ADC(void)
{
    if (adc_done) {
        adc_done = 0;
        // 此时 adc_buf 中已是 DMA 写入的最新数据
        uint32_t sum = 0;
        for (int i = 0; i < ADC_BUF_SIZE; i++) {
            sum += adc_buf[i];
        }
        // ...
    }
}

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

若不想每次 DMA 都手动 Clean/Invalidate,可用 MPU 将 DMA 缓冲区配置为 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; // 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);
}

将 DMA 缓冲区放在该区域,即可免去手动维护 Cache 的麻烦。

六、注意事项与排查清单

  • 对齐:缓冲区必须 32 字节对齐,大小建议为 32 的倍数。
  • 时序:发送前 Clean,接收前 Invalidate,接收后再次 Invalidate。
  • 原子性:Clean/Invalidate 操作本身可能被中断打断,建议在临界区或 DMA 完成中断中执行。
  • 多缓冲区:若使用双缓冲,每个缓冲区都要独立维护 Cache。
  • 调试:若出现 HardFault,检查是否因 Invalidate 误伤了栈或全局变量。
  • 性能:频繁 Clean/Invalidate 会降低性能,可考虑 MPU 非缓存区或使用 SCB_CleanInvalidateDCache_by_Addr

遵循以上原则,STM32H7 的 D-Cache 与 DMA 就能和谐共处,既发挥高性能,又保证数据一致性。