引言:一次诡异的 DMA 传输失败
某工业控制项目中,我们使用 STM32F429 驱动外部 SDRAM 作为显示缓冲区,并通过 DMA 将图像数据从 SDRAM 搬运到 LCD 控制器。代码运行一段时间后,屏幕出现随机花屏,调试发现 DMA 读取的数据与 CPU 写入的数据不一致,甚至出现“旧数据”残留。排查良久,最终定位到罪魁祸首——D-Cache 未正确维护。
原理剖析:D-Cache 与 SDRAM 的一致性鸿沟
为什么 D-Cache 会带来问题?
STM32F4 系列(如 F429、F407)内置 4KB 或 8KB 的 D-Cache,用于加速 CPU 对内存的访问。当 CPU 写入 SDRAM 时,数据可能暂存在 Cache 中,并未立即写回 SDRAM;同样,DMA 外设直接访问 SDRAM 时,绕过 Cache,导致 CPU 和 DMA 看到的数据视图不一致。
- 写操作:CPU 写 Cache,SDRAM 中仍是旧数据(Write-back 策略)。
- 读操作:CPU 读 Cache,可能命中已失效的旧数据(Stale data)。
关键概念:Clean 与 Invalidate
- Clean (Cache Clean):将 Cache 中已修改的数据写回 SDRAM,确保外设可见。
- Invalidate (Cache Invalidate):将 Cache 中的行标记为无效,后续读取强制从 SDRAM 加载最新数据。
配置步骤:开启 D-Cache 与 SDRAM 初始化
1. 使能 D-Cache
在系统初始化代码中,调用 SCB_EnableDCache() 使能 D-Cache。注意:必须在所有外设(如 DMA、LTDC)初始化之前使能,否则可能出现不可预知行为。
void SystemInit_Cache(void) {
SCB_EnableDCache();
// 可选:配置 Cache 策略,如 Write-back, Write-allocate
// SCB->CCR |= SCB_CCR_DWTC_Msk; // 使能 Write-Through 模式(谨慎使用)
}
2. SDRAM 初始化与 MPU 配置
SDRAM 通常映射在外部存储器区域(如 Bank1 0xC0000000)。为了正确管理 Cache,建议为 SDRAM 区域配置 MPU(Memory Protection Unit),设置其属性为“Normal, Write-back, Write-allocate”,以便使用标准 Cache 维护指令。
void MPU_Config_SDRAM(void) {
MPU_Region_InitTypeDef MPU_InitStruct;
// 禁用 MPU 进行配置
HAL_MPU_Disable();
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; // 根据 SDRAM 大小调整
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // Normal memory, Write-back, Write-allocate
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_CONTROL_HRD_MEM_MAP);
}
数据一致性维护策略:Clean & Invalidate 实战
场景一:CPU 写 SDRAM,DMA 读取(如显示缓冲区)
在 CPU 完成写入后,必须执行 Cache Clean,确保数据写回 SDRAM,再启动 DMA 传输。
// 假设 buffer_addr 是 SDRAM 中的地址,buffer_size 是字节数
void CPU_Write_To_SDRAM(uint32_t buffer_addr, uint8_t *data, uint32_t size) {
// 1. CPU 写入数据到 SDRAM(通过普通指针)
memcpy((void*)buffer_addr, data, size);
// 2. Clean D-Cache,将写入的数据写回 SDRAM
SCB_CleanDCache_by_Addr((uint32_t*)buffer_addr, size);
// 3. 现在可以安全启动 DMA 读取
Start_DMA_Read(buffer_addr, size);
}
场景二:DMA 写 SDRAM,CPU 读取(如摄像头采集)
DMA 将数据写入 SDRAM 后,CPU 读取前必须 Invalidate 对应 Cache 行,避免读到旧数据。
void DMA_Write_To_SDRAM(uint32_t buffer_addr, uint32_t size) {
// 1. 启动 DMA 写入 SDRAM
Start_DMA_Write(buffer_addr, size);
// 等待 DMA 完成(如中断或轮询)
Wait_DMA_Complete();
// 2. Invalidate D-Cache,使 CPU 强制从 SDRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_addr, size);
// 3. 现在 CPU 可以安全读取数据
Process_Data((uint8_t*)buffer_addr, size);
}
场景三:双缓冲交替使用
在双缓冲场景中,每个缓冲区在切换前都需要执行相应的 Clean 或 Invalidate 操作,确保数据同步。
#define BUFFER_SIZE 1024
uint32_t buffer_A = 0xC0000000;
uint32_t buffer_B = 0xC0001000;
void Switch_Buffer(uint8_t use_A) {
uint32_t active_buf = use_A ? buffer_A : buffer_B;
uint32_t inactive_buf = use_A ? buffer_B : buffer_A;
// 先 Clean 即将被 DMA 读取的缓冲区(假设 CPU 已写入数据)
SCB_CleanDCache_by_Addr((uint32_t*)active_buf, BUFFER_SIZE);
// 启动 DMA 从 active_buf 读取
Start_DMA_Read(active_buf, BUFFER_SIZE);
// 同时,对 inactive_buf 进行 Invalidate,准备接收新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)inactive_buf, BUFFER_SIZE);
}
注意事项与常见陷阱
- 地址对齐:Clean/Invalidate 操作要求地址按 32 字节对齐(Cache line 大小)。若缓冲区未对齐,需手动调整或使用更大的对齐区域。
-
大小参数:
SCB_CleanDCache_by_Addr的 size 参数必须是 32 字节的倍数,否则可能遗漏部分行。 - 频繁操作开销:每次 Clean/Invalidate 都有性能开销,应尽量减少调用次数,例如在批量操作后统一维护。
- DMA 与 Cache 并发:确保 DMA 传输期间不要对同一区域执行 Cache 操作,否则可能导致数据错乱。建议使用信号量或关中断保护。
- MPU 配置错误:若 SDRAM 区域未配置为 Cacheable,则 Clean/Invalidate 无效,但性能下降;若配置为 Write-Through,则无需 Clean,但性能可能受损。
-
使用 HAL 库注意:HAL 库提供
HAL_DCACHE_Clean()等函数,但它们是全局操作,效率较低,建议使用 CMSIS 提供的地址级函数。
总结
D-Cache 与 SDRAM 的数据一致性是 STM32F4 开发中不可忽视的细节。通过理解 Cache 工作原理,合理配置 MPU,并严格遵循“写后 Clean,读前 Invalidate”的原则,即可避免大多数缓存一致性问题。记住:在嵌入式世界里,缓存不是免费的午餐,它需要你用正确的策略来“买单”。