引言
在 STM32F4 高性能系列中,Cortex-M4 内核集成了可选的 D-Cache(数据缓存)和 I-Cache,用于加速对慢速存储器(如外部 SDRAM)的访问。然而,D-Cache 的引入也带来了一个经典陷阱:当 DMA 控制器直接访问内存时,CPU 和 DMA 看到的数据可能不一致,导致数据错乱、校验失败等随机故障。本文从底层原理出发,手把手教你通过 MPU(内存保护单元)配置来规避此问题。
1. D-Cache 与 DMA 数据一致性问题的根源
1.1 D-Cache 的工作机制
D-Cache 是 CPU 与主存之间的高速缓存,以“行”(Line)为单位(STM32F4 为 32 字节)存储数据。当 CPU 读取内存时,若命中 Cache 则直接返回,否则从主存加载整行到 Cache;当 CPU 写入时,采用 write-back(写回)策略:数据先写入 Cache,标记为脏(Dirty),仅在缓存行被替换或显式 Clean 时才写回主存。
1.2 冲突场景
- DMA 读取,CPU 写入:CPU 将数据写入 Cache(尚未写回主存),DMA 从主存读取旧数据,导致 DMA 拿到过期数据。
- DMA 写入,CPU 读取:DMA 将新数据写入主存,但 CPU 的 Cache 中仍保留旧缓存行,CPU 读回旧数据。
这两种情况都会破坏数据一致性,尤其在网络、音频、图像等高频 DMA 传输中极易触发。
2. 解决方案概览
解决思路有两种:
- 彻底禁用 D-Cache:简单粗暴,但牺牲性能,不推荐用于大块数据场景。
- 通过 MPU 配置内存区域的 Cache 策略:将 DMA 涉及的缓冲区设置为 非缓存(Non-cacheable) 或 写透(Write-through),既保证一致性,又保留部分缓存加速。
3. 实战配置:使用 MPU 设置非缓存区域
3.1 硬件环境
- MCU:STM32F407(Cortex-M4,带 D-Cache)
- 外部 SDRAM:起始地址 0xC0000000,大小 1MB
- DMA:用于 ADC 采集数据到 SDRAM 缓冲区
3.2 MPU 配置步骤
- 使能 MPU:设置 MPU_CTRL 寄存器,使能 MPU 和默认内存映射。
- 配置区域:选择要设置为非缓存的区域(如 SDRAM 的某段),设置起始地址、大小、访问权限和 Cache 策略。
- 使能区域:将区域使能位置 1。
3.3 代码实现(基于 HAL 库)
#include "stm32f4xx_hal.h"
void MPU_Config_NonCacheable(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 SDRAM 区域(0xC0000000,大小 1MB)为非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_1MB;
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; // 共享,便于 DMA
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);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// 在 main 函数中调用
int main(void)
{
HAL_Init();
SystemClock_Config();
// 配置 MPU 前,确保 D-Cache 已使能(可选)
SCB_EnableDCache();
MPU_Config_NonCacheable();
// 初始化 DMA 和 SDRAM...
// 之后 DMA 与 CPU 访问 0xC0000000 区域时,数据直接读写主存,无缓存干扰
while(1)
{
// 主循环
}
}
3.4 关键点说明
-
IsBufferable 和 IsCacheable:两者都设为
NOT,即完全非缓存,CPU 每次读写都直接访问主存,保证一致性。 - IsShareable:设为共享,允许 DMA 等其他总线主设备访问,避免总线仲裁问题。
- MPU_TEX_LEVEL0:配合 TEX 字段,实现特定的内存属性,此处为常规外设内存。
4. 进阶:使用 Write-Through 策略平衡性能
如果不想完全放弃缓存,可将区域设置为 Write-Through(写透)策略:CPU 写操作同时更新 Cache 和主存,读操作仍可缓存。这样 DMA 读取时总能拿到最新数据,而 CPU 读性能得以保留。
配置修改如下:
// 修改 MPU 配置中的缓存属性
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; // 允许缓存
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // TEX=1, C=1, B=0 表示 Write-Through
注意:Write-Through 下,CPU 写操作会直接写主存,但读操作仍可能命中旧缓存行(若 DMA 写入了新数据)。因此,仅适用于 CPU 写、DMA 读的场景。对于 DMA 写、CPU 读,仍需非缓存或手动 Clean/Invalidate。
5. 手动维护 Cache 的备用方案
若不想配置 MPU,也可在 DMA 传输前后手动操作 Cache:
-
DMA 读取前:调用
SCB_CleanDCache()将 CPU 写入的数据写回主存。 -
DMA 写入后:调用
SCB_InvalidateDCache()使 CPU 缓存行失效,强制重新从主存读取。
但这种方法需要精确控制时机,容易遗漏,且影响性能,仅适合小数据量。
6. 注意事项与避坑指南
- MPU 配置必须在使能 D-Cache 之前或之后? 建议先配置 MPU 再使能 D-Cache,避免配置期间缓存干扰。
- 区域大小必须对齐:MPU 区域大小必须是 2 的幂次,且起始地址对齐到区域大小。例如 1MB 区域,起始地址低 20 位必须为 0。
- 默认内存映射:STM32F4 的默认内存映射中,外部 RAM 区域(0xC0000000-0xDFFFFFFF)默认是 Cacheable 的,因此必须显式覆盖。
- 调试时:如果出现随机数据错误,优先检查 MPU 配置是否生效,可通过读取 MPU_RBAR 和 MPU_RASR 寄存器验证。
- 多缓冲区:为不同用途的缓冲区分配不同 MPU 区域,例如 ADC 缓冲区非缓存,显示缓冲区可 Write-Through。
7. 总结
D-Cache 与 DMA 的数据一致性是 STM32F4 开发中的经典陷阱,但通过合理配置 MPU,可以精准控制内存区域的缓存策略,既保证数据正确性,又兼顾性能。本文提供了两种配置方案和完整代码,开发者可根据实际场景选择。记住:在嵌入式世界里,缓存不是免费的午餐,必须明确管理。
希望本文能帮你避开这个“坑”,让你的 DMA 传输稳定可靠。如果你有更多实战经验,欢迎交流!