# 引言:D-Cache 的双刃剑 STM32F4 系列(如 STM32F407、F429)内置了 4KB 或 8KB 的 D-Cache,用于加速对片外 SDRAM 的访问。然而,Cache 的引入带来了数据一致性问题:当 CPU 和 DMA(或外设)同时访问同一内存区域时,缓存中的副本可能与 SDRAM 中的实际数据不一致,导致程序行为异常。本文将从底层原理出发,剖析问题根源,并给出硬件层(MPU 配置)和软件层的完整解决方案。 # 1. 问题根源:Cache 与 DMA 的“各自为政” ## 1.1 Cache 的工作原理 D-Cache 以“行”(Line)为单位管理数据,典型行大小为 32 字节。当 CPU 读取 SDRAM 地址时,Cache 会加载整个行到缓存中;后续访问命中缓存则直接返回,无需访问外部存储器。写操作则可能采用“写回”(Write-back)策略:数据先写入缓存,标记为脏(Dirty),延迟到缓存行被替换时才写回 SDRAM。 ## 1.2 一致性冲突场景 - **场景 A:CPU 写,DMA 读** CPU 修改了 SDRAM 中的数据(数据留在 Cache 中),DMA 外设直接读取 SDRAM 物理地址,拿到的是旧数据。 - **场景 B:DMA 写,CPU 读** DMA 从外设接收数据写入 SDRAM,CPU 读取时命中 Cache 中的旧副本,看不到新数据。 - **场景 C:Cache 行替换导致部分更新** 若 CPU 只修改了某行的前 16 字节,而 DMA 修改了该行的后 16 字节,当 Cache 行被替换时,后 16 字节的修改可能被覆盖。 # 2. 踩坑实录:一个典型的显示花屏案例 假设使用 STM32F429 驱动 LCD,显存位于外部 SDRAM,通过 LTDC 控制器直接读取显存刷新屏幕。CPU 负责在显存中绘制图形,但绘制后屏幕出现随机花屏。 **分析过程:** 1. 绘制函数使用 CPU 写入显存,数据可能残留在 D-Cache 中。 2. LTDC 是 DMA 外设,直接读取 SDRAM 物理地址,未命中 Cache。 3. 结果:LTDC 读到的是未更新的旧数据,或部分新数据(若 Cache 行被部分替换)。 **解决思路:** 在 CPU 绘制完成后,必须将相关 Cache 行写回(Clean)SDRAM,并确保后续读取时无效化(Invalidate)缓存。 # 3. 硬件层解决方案:MPU 配置与内存属性 ## 3.1 MPU 的作用 Memory Protection Unit (MPU) 可以配置内存区域的缓存策略,例如将 SDRAM 区域设置为“写通”(Write-through)或“非缓存”(Non-cacheable),从而避免一致性问题的发生。但完全禁用缓存会损失性能,因此推荐使用“写回 + 读分配”策略,并配合软件维护。 ## 3.2 配置步骤 1. **使能 MPU 和 D-Cache**(在 SystemInit 或 main 函数开头): ```c SCB_EnableDCache(); MPU_Region_Init(); ``` 2. **配置 SDRAM 区域为“写回,读分配”**: ```c void MPU_Region_Init(void) { MPU_Region_InitTypeDef MPU_InitStruct; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; // SDRAM 基地址 MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; 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.Number = MPU_REGION_NUMBER0; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` 3. **注意:** 若使用“写回”策略,仍需软件维护一致性;若使用“写通”策略,CPU 写操作会立即更新 SDRAM,但读操作仍可能命中缓存,DMA 写后需 Invalidate。 # 4. 软件层解决方案:Clean 与 Invalidate 操作 ## 4.1 核心 API - `SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize)`:将指定地址范围的 Cache 行写回 SDRAM。 - `SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize)`:使指定地址范围的 Cache 行失效,下次读取时从 SDRAM 重新加载。 **注意:** 地址必须按 32 字节对齐,大小向上取整到 32 的倍数。 ## 4.2 封装函数 ```c // 对齐到 32 字节边界 #define CACHE_LINE_SIZE 32 void Cache_Clean(uint32_t addr, uint32_t size) { uint32_t start = addr & ~(CACHE_LINE_SIZE - 1); uint32_t end = (addr + size + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1); SCB_CleanDCache_by_Addr((uint32_t*)start, end - start); } void Cache_Invalidate(uint32_t addr, uint32_t size) { uint32_t start = addr & ~(CACHE_LINE_SIZE - 1); uint32_t end = (addr + size + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1); SCB_InvalidateDCache_by_Addr((uint32_t*)start, end - start); } ``` ## 4.3 使用示例:CPU 绘制后刷新显示 ```c // 绘制矩形到显存 void Draw_Rect(uint32_t *fb, int x, int y, int w, int h, uint32_t color) { for (int i = y; i < y+h; i++) { for (int j = x; j < x+w; j++) { fb[i*LCD_WIDTH + j] = color; } } // 写回整个绘制区域 uint32_t start_addr = (uint32_t)&fb[y*LCD_WIDTH + x]; uint32_t size = w * h * 4; // 每个像素 4 字节 Cache_Clean(start_addr, size); } // DMA 接收数据到 SDRAM 缓冲区后,CPU 读取前 void Process_DMA_Buffer(uint32_t *buf, uint32_t len) { Cache_Invalidate((uint32_t)buf, len * 4); // 现在可以安全读取 buf 中的数据 } ``` # 5. 进阶技巧与注意事项 - **内存屏障**:在 Clean/Invalidate 操作后,建议添加 `__DSB()`(数据同步屏障)确保操作完成。 ```c SCB_CleanDCache_by_Addr(...); __DSB(); ``` - **缓冲区对齐**:定义 DMA 缓冲区时,使用 `__attribute__((aligned(32)))` 确保起始地址对齐,避免跨行操作。 - **性能权衡**:频繁 Clean/Invalidate 会降低性能,建议按块操作,而非逐字节。 - **多核/中断场景**:在中断服务程序中若访问共享缓冲区,同样需要一致性维护。 - **调试技巧**:若出现随机性数据错误,先检查 Cache 配置,再检查 DMA 描述符是否配置了正确的内存区域。 # 6. 总结 D-Cache 与 SDRAM 的数据一致性是 STM32F4 开发中的常见陷阱,但通过理解 Cache 行机制、合理配置 MPU、并正确使用 Clean/Invalidate 操作,可以彻底解决。硬件层方案提供基础保障,软件层维护确保实时一致性。建议在项目初期就规划好内存区域的缓存策略,避免后期大规模修改。希望本文能帮你少走弯路,让嵌入式开发更顺畅。