为什么 DMA 一开 D-Cache 就翻车?
STM32H7 的 Cortex-M7 内核带 16KB D-Cache 和 I-Cache。开启 D-Cache 后,CPU 读写内存会先经过 Cache,而 DMA 是直接访问物理内存(SRAM)的“旁路”通道。两者看到的数据可能不一致:
- CPU 写数据 → 只更新了 Cache,SRAM 还是旧值,DMA 发送出去的是旧数据。
- DMA 接收数据 → 只写了 SRAM,Cache 里还是旧值,CPU 读到的还是旧数据。
这就是经典的 Cache 一致性问题。解决手段只有两个:Clean(写回) 和 Invalidate(无效化)。
Clean 与 Invalidate 的时机
先记住一句话:Clean 是“把 Cache 里的脏数据写回 SRAM”,Invalidate 是“把 Cache 里的数据标记为无效,下次读时从 SRAM 重新加载”。
| 场景 | 操作 | 时机 | |------|------|------| | CPU 写缓冲区,DMA 发送 | Clean | DMA 启动前 | | DMA 接收数据,CPU 读取 | Invalidate | DMA 完成后 | | DMA 接收数据,CPU 读取 | Clean(可选) | DMA 启动前,防止脏数据覆盖 |
关键坑点:
- Invalidate 必须在 DMA 完成后、CPU 读取前执行,否则读到旧数据。
- Invalidate 前如果 CPU 写过该区域,必须先 Clean,否则未写回的数据会被丢弃。
- 不要对正在被 DMA 写入的区域执行 Invalidate,否则可能丢数据。
正确使用 Cache 维护函数
CMSIS 提供了两个函数:
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
注意 dsize 是字节数,且地址必须 32 字节对齐(Cache Line 大小)。缓冲区定义时务必对齐:
__attribute__((aligned(32))) uint8_t dma_buf[1024];
MPU 配置:让 DMA 缓冲区绕过 Cache
更优雅的方案是用 MPU 把 DMA 缓冲区配置为 Non-Cacheable,这样根本不需要手动维护 Cache。
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_LEVEL1;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
把 DMA 缓冲区放到 0x30000000(D2 SRAM)并配置为 Non-Cacheable,DMA 和 CPU 都直接访问物理内存,彻底避免一致性问题。
完整实战:ADC + DMA 采集
以 ADC1 连续采集 1024 个点为例,缓冲区放在 D2 SRAM 并配置 MPU:
#include "stm32h7xx_hal.h"
#define ADC_BUF_SIZE 1024
__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_buf[ADC_BUF_SIZE];
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
void ADC_DMA_Init(void)
{
// DMA 配置
hdma_adc1.Instance = DMA1_Stream0;
hdma_adc1.Init.Request = DMA_REQUEST_ADC1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
// ADC 配置(略,按 CubeMX 生成即可)
HAL_ADC_Init(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);
}
// 读取数据前,若缓冲区是 Cacheable,必须 Invalidate
void ADC_ReadBuffer(uint16_t *out, uint32_t len)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, ADC_BUF_SIZE * 2);
memcpy(out, adc_buf, len * 2);
}
链接脚本中定义 .dma_buffer 段到 D2 SRAM:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D2
避坑清单
- 缓冲区地址和长度必须 32 字节对齐,否则维护函数可能越界或无效。
- Invalidate 前先 Clean,避免丢弃 CPU 未写回的数据。
- 不要在 DMA 传输过程中操作 Cache,否则数据竞争。
-
MPU 配置后要调用
SCB_EnableDCache(),且顺序不能乱。 -
使用
__attribute__((aligned(32)))或链接脚本对齐,别依赖编译器默认。 - 调试时若出现 HardFault,先检查 MPU 区域是否与其它外设地址冲突。
小结
D-Cache 与 DMA 的一致性问题本质是“两个主设备看到的内存视图不同”。解决方案要么手动 Clean/Invalidate,要么用 MPU 把 DMA 缓冲区设为 Non-Cacheable。前者灵活但易错,后者一劳永逸但牺牲该区域的 Cache 性能。实际项目中,建议把 DMA 缓冲区统一放到 D2 SRAM 并配置 Non-Cacheable,既简单又可靠。