STM32H7 的 D-Cache 与 DMA 冲突排查:从数据错乱到 MPU 配置的完整定位路径
一、现象:数据错乱的典型表现
在 STM32H7 上使用 DMA 传输时,你可能会遇到:
- 串口 DMA 接收的数据偶尔出现前几个字节正确、后续全为 0 或乱码;
- ADC 连续采样 DMA 搬运到数组,但读取到的值跳变剧烈,与真实电压不符;
- SDMMC 读写文件时数据校验失败,但降低主频或关闭 D-Cache 后正常;
- 以太网 DMA 描述符被 CPU 修改后,DMA 却读到旧值。
这些现象的共同点是:CPU 访问的内存与 DMA 访问的内存不一致。根源在于 STM32H7 的 Cortex-M7 内核带有 D-Cache,而 DMA 直接访问物理内存,不经过 Cache。
二、原理:D-Cache 与 DMA 为何冲突
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 写数据时,先写入 Cache,标记为脏(Dirty),并不立即写回 SRAM;CPU 读数据时,若 Cache 未命中,则从 SRAM 加载到 Cache。
DMA 控制器直接读写 SRAM,完全绕过 D-Cache。因此:
- CPU 写、DMA 读:CPU 写入的数据可能还在 Cache 中(脏行),DMA 从 SRAM 读到的是旧数据。
- DMA 写、CPU 读:DMA 将新数据写入 SRAM,但 CPU 的 Cache 中可能还保留着旧数据(干净行或脏行),CPU 读到旧值。
此外,Cache 行大小为 32 字节。若 DMA 缓冲区未按 32 字节对齐,Cache 维护操作可能影响相邻变量,导致更难排查的随机错误。
三、排查路径:从现象到根因
步骤 1:确认是否与 Cache 相关
临时关闭 D-Cache 测试:
// 在 main 初始化前调用
SCB_DisableDCache();
若问题消失,则基本确定是 Cache 一致性问题。注意:关闭 D-Cache 会大幅降低性能,仅用于验证。
步骤 2:检查 DMA 缓冲区地址与对齐
- 缓冲区是否位于 DTCM RAM?DTCM 默认不经过 Cache,但部分 DMA 无法访问 DTCM(如 MDMA、部分外设 DMA)。
- 缓冲区是否 32 字节对齐?使用
__attribute__((aligned(32)))。 - 是否与其他变量共享 Cache 行?避免将 DMA 缓冲区与普通变量放在同一 32 字节区域。
步骤 3:选择正确的 Cache 维护策略
根据数据传输方向:
-
CPU 写 → DMA 读:在启动 DMA 前执行
SCB_CleanDCache_by_Addr(),将脏行写回 SRAM。 -
DMA 写 → CPU 读:在 DMA 完成后执行
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧行,强制从 SRAM 重新加载。 -
双向:先 Clean 再 Invalidate,或使用
SCB_CleanInvalidateDCache_by_Addr()。
注意:Invalidate 操作若缓冲区有未写回的脏数据,会丢失数据。因此顺序很重要。
步骤 4:使用 MPU 配置内存属性
更优雅的方案是利用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,从而无需手动维护 Cache。
推荐配置:
- 将 DMA 缓冲区集中放在一个专用区域(如
0x30000000开始的 SRAM 区域); - 用 MPU 将该区域设为 Normal, Non-Cacheable 或 Normal, Write-Through, No Write Allocate;
- 确保该区域不与其他 Cacheable 数据重叠。
四、完整代码示例
以下示例展示 ADC + DMA 连续采样,使用 MPU 将缓冲区设为 Non-Cacheable。
#include "stm32h7xx.h"
#define ADC_DMA_BUFFER_SIZE 256
// 将缓冲区放在 D2 SRAM 区域,并 32 字节对齐
__attribute__((section(".dma_buffer"), aligned(32)))
uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
// MPU 配置:将 .dma_buffer 区域设为 Non-Cacheable
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 获取缓冲区地址,按 32 字节对齐向下取整
uint32_t base_addr = (uint32_t)adc_dma_buffer & ~0x1F;
// 区域大小至少覆盖缓冲区,设为 4KB 对齐
uint32_t region_size = 4096;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = base_addr;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
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_buffer 段到 D2 SRAM
// 例如:
// .dma_buffer (NOLOAD) : ALIGN(32) { *(.dma_buffer) } > RAM_D2
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
SCB_EnableICache();
SCB_EnableDCache();
// 初始化 ADC 和 DMA,目标地址为 adc_dma_buffer
// ...
while (1) {
// 由于 MPU 已设为 Non-Cacheable,CPU 直接读取 SRAM,无需手动维护 Cache
uint16_t value = adc_dma_buffer[0];
// 处理数据...
}
}
若不想用 MPU,也可手动维护 Cache:
// DMA 传输前,CPU 写数据到缓冲区后
SCB_CleanDCache_by_Addr((uint32_t*)adc_dma_buffer, sizeof(adc_dma_buffer));
// DMA 传输完成后,CPU 读数据前
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buffer, sizeof(adc_dma_buffer));
五、注意事项
- 对齐:DMA 缓冲区必须 32 字节对齐,否则 Cache 维护会波及相邻数据。
- 区域隔离:不要将 DMA 缓冲区与普通变量放在同一 32 字节 Cache 行内。
- DTCM 限制:DTCM 虽不经过 Cache,但部分 DMA(如 MDMA、以太网 DMA)无法访问 DTCM,需查阅参考手册。
- Invalidate 风险:若缓冲区有未写回的脏数据,Invalidate 会丢弃它们,务必先 Clean。
- 多缓冲区:若使用多个 DMA 缓冲区,确保每个都按 32 字节对齐且不重叠。
- 性能权衡:Non-Cacheable 区域会降低 CPU 访问速度,适合 DMA 频繁访问的缓冲区;若 CPU 也频繁读取,可考虑 Write-Through 模式。
-
调试技巧:使用
SCB_GetDCacheSize()等函数确认 Cache 状态;在调试器中查看内存时,注意 Cache 可能未写回,可手动 Clean 后再查看。
通过以上路径,你可以系统性地定位并解决 STM32H7 上 D-Cache 与 DMA 的冲突问题,让高性能与数据一致性兼得。