一、为什么 DMA 与 Cache 会“打架”?
STM32H7 搭载 Cortex-M7,主频可达 480MHz,内部集成 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。D-Cache 的存在让 CPU 访问频繁使用的数据时无需每次都读 AXI SRAM,性能提升显著。但 DMA 控制器直接访问物理内存,不经过 Cache。
这就产生两个经典问题:
- CPU 写、DMA 读:CPU 把数据写入缓冲区,数据可能还在 Cache 里(Write-Back 模式),DMA 从物理内存读到的却是旧数据。
- DMA 写、CPU 读:DMA 把新数据写入物理内存,但 CPU 读到的却是 Cache 中的旧副本。
根本原因:同一块内存存在两个“视图”——CPU 看到的是 Cache,DMA 看到的是物理内存。
二、解决思路:MPU 配置 + Cache 维护
有两种主流方案:
- 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through:CPU 访问直接穿透到物理内存,无需维护操作,但会损失部分性能。
- 保持 Cacheable,在 DMA 传输前后手动维护 Cache:性能最好,但代码复杂,容易遗漏。
实际工程中常混合使用:小数据量、高频传输用 Non-Cacheable;大数据量、低频传输用 Cache 维护。
三、MPU 配置实战
STM32H7 的 MPU 有 16 个区域,每个区域可独立设置属性。下面以 AXI SRAM 中一块 32KB 的 DMA 缓冲区为例,配置为 Non-Cacheable。
#include "stm32h7xx.h"
#define DMA_BUF_ADDR 0x24000000UL // AXI SRAM 起始
#define DMA_BUF_SIZE 0x8000UL // 32KB
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 区域 0:背景区域,Normal Cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x00000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 区域 1:DMA 缓冲区,Non-Cacheable */
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.BaseAddress = DMA_BUF_ADDR;
MPU_InitStruct.Size = MPU_REGION_SIZE_32KB;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
关键点:
- 区域编号越小优先级越高,区域 1 覆盖区域 0 的对应地址。
-
TEX=1, C=0, B=0对应 Normal Non-Cacheable。 - 若希望 Write-Through 模式,设
TEX=0, C=1, B=0。 - 配置后需调用
SCB_EnableDCache()使能 D-Cache。
四、Cache 维护操作
若缓冲区保持 Cacheable,必须在 DMA 传输前后调用 CMSIS 提供的函数。
/* CPU 写数据 -> DMA 读:先 Clean,把 Cache 写回内存 */
void DMA_SendData(uint8_t *buf, uint32_t len)
{
SCB_CleanDCache_by_Addr((uint32_t *)buf, len);
HAL_DMA_Start(&hdma, (uint32_t)buf, (uint32_t)&periph_addr, len);
}
/* DMA 写数据 -> CPU 读:先 Invalidate,丢弃 Cache 旧副本 */
void DMA_ReceiveData(uint8_t *buf, uint32_t len)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
HAL_DMA_Start(&hdma, (uint32_t)&periph_addr, (uint32_t)buf, len);
/* 等待传输完成... */
SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
}
地址对齐要求:SCB_CleanDCache_by_Addr 的地址必须是 32 字节对齐,长度建议向上取整到 32 字节倍数,否则可能误伤相邻数据。
#define ALIGN_32(x) (((x) + 31U) & ~31U)
void Safe_Clean(void *addr, uint32_t len)
{
uint32_t aligned_addr = (uint32_t)addr & ~31U;
uint32_t aligned_len = ALIGN_32((uint32_t)addr + len - aligned_addr);
SCB_CleanDCache_by_Addr((uint32_t *)aligned_addr, aligned_len);
}
五、完整示例:ADC + DMA 采集
#define ADC_BUF_LEN 256
__attribute__((aligned(32))) static uint16_t adc_buf[ADC_BUF_LEN];
void ADC_DMA_Init(void)
{
/* 假设 MPU 已把 adc_buf 所在区域设为 Non-Cacheable */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);
}
/* DMA 完成回调中处理数据 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/* 若为 Cacheable,此处需 Invalidate */
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));
ProcessData(adc_buf, ADC_BUF_LEN);
}
六、注意事项与避坑指南
- 不要对同一区域既 Invalidate 又 Clean:顺序错误会导致数据丢失。写方向先 Clean,读方向先 Invalidate。
- Invalidate 前确保 DMA 已停止:否则可能丢弃 DMA 刚写入但未完成的数据。
- 多缓冲区(Double Buffer)要分别维护:每个缓冲区独立调用维护函数。
- ETH、SDMMC、USB 等外设 DMA 同样适用:原理一致,别只盯着 ADC。
- 调试时关闭 D-Cache 可快速定位问题:若关闭后正常,基本可确定是一致性问题。
- MPU 配置要在使能 Cache 之前完成:否则属性不生效。
-
使用
__attribute__((aligned(32)))保证缓冲区对齐:避免维护操作越界。
七、总结
STM32H7 的 Cache 与 DMA 一致性问题是高性能应用的必修课。核心原则:要么让 DMA 缓冲区不经过 Cache(MPU 配置),要么在传输前后正确维护 Cache(Clean/Invalidate)。理解两者适用场景,结合对齐与顺序要求,就能在享受 H7 高性能的同时,保证数据万无一失。