STM32H7 的 D-Cache 与 DMA 数据一致性:从 Cache 维护 API 到实际踩坑排查

STM32H7 系列凭借 Cortex-M7 内核和 480MHz 主频,成为高性能嵌入式应用的首选。但开启 D-Cache 后,DMA 传输的数据经常出现“随机错乱”,让不少开发者头疼。本文从原理到实践,帮你彻底理清 D-Cache 与 DMA 的数据一致性问题。

一、为什么 Cache 和 DMA 会打架?

Cortex-M7 的 D-Cache 位于 CPU 和 AXI 总线之间。CPU 访问内存时,数据会被缓存到 D-Cache 中;而 DMA 控制器直接访问物理内存(SRAM/SDRAM),不经过 Cache。

这就导致两种典型问题:

  • DMA 写入内存,CPU 读不到新数据:CPU 读的是 Cache 里的旧值,DMA 写的新数据还在内存里。
  • CPU 写入内存,DMA 读到旧数据:CPU 写的数据还在 Cache 中(Write-Back 模式),尚未写回内存,DMA 启动后读到的是内存中的旧内容。

注意:STM32H7 的 D-Cache 默认是 Write-Back, Write-Allocate 模式,写操作不会立即同步到内存。

二、Cache 维护 API 详解

CMSIS 提供了核心函数(需包含 core_cm7.h):

void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
  • Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写 → DMA 读 场景。
  • Invalidate:丢弃 Cache 内容,强制下次从内存重新加载。用于 DMA 写 → CPU 读 场景。
  • CleanInvalidate:先写回再丢弃,用于双向传输。

关键点:addr 必须 32 字节对齐,dsize 建议按 32 字节倍数传递,否则可能误伤相邻数据。

三、实战:ADC + DMA 采集数据

以 ADC1 连续扫描 + DMA 循环传输为例,缓冲区定义在 0x30000000(D2 SRAM,DMA 可访问)。

#include "stm32h7xx_hal.h"

#define ADC_BUF_LEN  256
// 必须 32 字节对齐,且放在 DMA 可访问区域
__attribute__((section(".RAM_D2"), aligned(32)))
uint16_t adc_buf[ADC_BUF_LEN];

void ADC_DMA_Init(void)
{
    // ... ADC 和 DMA 初始化代码省略 ...
    // 启动前:确保内存中无脏数据(可选)
    SCB_CleanInvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_LEN);
}

// 处理数据前调用:丢弃 Cache,读取 DMA 新数据
void Process_ADC_Data(void)
{
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
    for (int i = 0; i < ADC_BUF_LEN; i++) {
        // 此时读取的是 DMA 写入的最新值
        float voltage = adc_buf[i] * 3.3f / 65535.0f;
        // ...
    }
}

发送方向(CPU 填充数据 → DMA 发送):

void UART_Send_DMA(uint8_t *data, uint16_t len)
{
    // 填充数据到 data 缓冲区
    // ...
    // 写回内存,确保 DMA 读到最新数据
    SCB_CleanDCache_by_Addr((uint32_t*)data, len);
    HAL_UART_Transmit_DMA(&huart1, data, len);
}

四、踩坑排查指南

坑 1:缓冲区未对齐

SCB_*DCache_by_Addr 要求地址 32 字节对齐。若未对齐,函数可能不生效或误清相邻数据。

解决:使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。

坑 2:DMA 缓冲区放在 DTCM

STM32H7 的 DTCM(0x20000000)无法被 DMA 访问。若缓冲区误放此处,DMA 传输直接失败或数据全 0。

解决:将 DMA 缓冲区放到 D1/D2/D3 SRAM,如 0x24000000、0x30000000。

坑 3:Invalidate 时机错误导致数据丢失

若在 DMA 传输过程中调用 Invalidate,可能丢弃 CPU 刚写入但未 Clean 的数据。

解决:严格遵循“传输前 Clean,传输后 Invalidate”的顺序。

坑 4:多缓冲区或 Cache 行边界问题

当缓冲区长度不是 32 字节倍数时,Invalidate 可能影响相邻变量。

解决:手动将长度向上取整到 32 的倍数,或使用 MPU 将 DMA 区域配置为 Non-Cacheable。

坑 5:忘记配置 MPU

最彻底的方案是用 MPU 将 DMA 缓冲区所在区域设为 Non-Cacheable,一劳永逸。

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

五、总结

  • D-Cache 与 DMA 冲突的本质是 CPU 视角与内存视角不一致。
  • 牢记口诀:CPU 写后 Clean,DMA 写后 Invalidate。
  • 地址 32 字节对齐、缓冲区放对 SRAM 区域是基本要求。
  • 对性能要求不高的 DMA 缓冲区,优先用 MPU 设为 Non-Cacheable,省心省力。
  • 调试时可用 SCB_InvalidateDCache() 全局失效 Cache 快速验证问题是否由 Cache 引起。

掌握这些要点,STM32H7 的 D-Cache 与 DMA 就能和谐共处,让你的高性能应用稳定运行。