一、问题现象:DMA 数据错乱
最近在 STM32H743 上调试一个 ADC 连续采样 + DMA 搬运到内存缓冲区的功能。代码逻辑很简单:ADC 规则通道连续转换,DMA 循环模式将数据搬到 adc_buffer[256],主循环读取缓冲区做处理。但实际运行时,缓冲区数据经常出现“半新半旧”或完全错乱,而单步调试时又正常。
初步怀疑:DMA 配置错误?ADC 时钟太快?内存对齐?排查一圈后,最终定位到 MPU(Memory Protection Unit)配置错误导致 Cache 与 DMA 不一致。
二、原理:Cortex-M7 的 Cache 与 DMA 冲突
STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache)和指令 Cache(I-Cache)。Cache 的存在是为了弥补 CPU 与主存之间的速度差,但 DMA 直接访问物理内存,不经过 Cache。
- 写操作:CPU 写数据到缓冲区,数据可能只停留在 Cache 中,未写回主存。此时 DMA 从主存读取,得到的是旧数据。
- 读操作:DMA 将新数据写入主存,但 CPU 读取时若 Cache 中已有旧数据副本,则读到旧值。
这就是典型的 Cache 一致性(Coherency)问题。解决方法通常有三种:
- 将 DMA 缓冲区所在内存区域配置为 Write-Through(写透) 或 Non-Cacheable(不可缓存)。
- 在 DMA 传输前后手动执行 Cache 清理(Clean) 和 无效化(Invalidate) 操作。
- 使用 MPU 将特定区域设为 Non-Cacheable,让 CPU 和 DMA 都直接访问主存。
本例中,我原本想用 MPU 将 0x24000000(AXI SRAM)区域设为 Non-Cacheable,但配置时属性写错,导致该区域仍然是 Cacheable,从而引发数据错乱。
三、排查过程:从现象到根因
- 确认 DMA 配置:检查 DMA 流、通道、优先级、数据宽度、循环模式,均正确。
- 检查内存对齐:缓冲区 32 字节对齐,符合 Cache 行大小(32 字节)。
-
关闭 D-Cache 测试:在
main()开头调用SCB_DisableDCache(),数据立即正常。这证实了 Cache 一致性问题是元凶。 -
检查 MPU 配置:发现 MPU 区域属性中
TEX=0, C=1, B=0,即 Write-Back, Write-Allocate,这是可缓存的。而我本意是 Non-Cacheable(应设TEX=1, C=0, B=0)。 - 修正 MPU 配置:重新配置区域属性为 Non-Cacheable,问题消失。
四、MPU 配置步骤与完整代码
以下是基于 STM32H7 HAL 库的 MPU 配置示例,将 AXI SRAM 的 0x24000000 起始的 512KB 区域设为 Non-Cacheable。
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 设置 AXI SRAM 区域为 Non-Cacheable
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.BaseAddress = 0x24000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_512KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // TEX=1
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_NOT_CACHEABLE; // C=0
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; // B=0
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 必须在使能 Cache 前配置
SCB_EnableICache();
SCB_EnableDCache();
// 后续初始化 ADC、DMA 等
// ...
while (1) { }
}
关键点:
-
MPU_TEX_LEVEL1+NOT_CACHEABLE+NOT_BUFFERABLE组合得到 Non-Cacheable 属性。 - MPU 配置必须在
SCB_EnableDCache()之前完成,否则可能不生效。 - 如果使用 FreeRTOS 等 RTOS,需确保任务堆栈和 DMA 缓冲区不在同一可缓存区域。
五、替代方案:手动 Cache 维护
如果不想将整个区域设为 Non-Cacheable,可以在 DMA 传输前后手动维护 Cache:
// DMA 发送前:将 CPU 写入的数据从 Cache 写回主存
SCB_CleanDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));
// DMA 接收后:无效化 Cache,使 CPU 重新从主存读取
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));
注意:SCB_InvalidateDCache_by_Addr 的地址必须 32 字节对齐,长度也建议 32 字节对齐,否则可能误伤相邻数据。
六、注意事项与避坑指南
-
MPU 区域属性:
TEX=1, C=0, B=0才是 Non-Cacheable;TEX=0, C=1, B=0是 Write-Back,仍会缓存。 - Cache 行大小:Cortex-M7 的 Cache 行是 32 字节,DMA 缓冲区地址和长度最好按 32 字节对齐。
- 多区域重叠:MPU 区域不能重叠,否则行为未定义。
- DMA 访问权限:确保 MPU 区域允许 DMA 访问(通常 Full Access)。
- 调试技巧:遇到疑似 Cache 问题,先关闭 D-Cache 验证,再逐步开启并配置 MPU。
- RTOS 环境:任务切换时可能触发 Cache 维护,需仔细设计内存布局。
七、总结
STM32H7 的 Cache 大幅提升性能,但也给 DMA 带来了数据一致性挑战。通过 MPU 将 DMA 缓冲区设为 Non-Cacheable 是最简单可靠的方案,但必须正确配置 TEX、C、B 位。本次排查历时两天,最终发现是 MPU 属性写错,希望本文能帮你少走弯路。记住:Cache 与 DMA 共存时,一致性永远是第一优先级。