# 引言:一次诡异的 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)初始化之前使能,否则可能出现不可预知行为。 ```c 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 维护指令。 ```c 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 传输。 ```c // 假设 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 行,避免读到旧数据。 ```c 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 操作,确保数据同步。 ```c #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”的原则,即可避免大多数缓存一致性问题。记住:在嵌入式世界里,缓存不是免费的午餐,它需要你用正确的策略来“买单”。