# 引言 在 STM32F4 系列(如 STM32F407、F429 等)上,D-Cache(数据缓存)能显著提升 CPU 访问内部 SRAM 的速度,但当外部 SDRAM 介入时,D-Cache 与 DMA 外设之间的数据一致性便成为隐患。典型场景:LCD 显存位于 SDRAM,CPU 写入后 DMA 读取,若未正确处理缓存,屏幕会出现随机花屏;或以太网 DMA 接收数据时,CPU 读到的是陈旧缓存。本文从原理出发,提供三种实用排查手段,助你快速定位问题。 # D-Cache 与 SDRAM 一致性问题的根源 STM32F4 的 D-Cache 是 4 路组关联、总大小 4KB(部分型号 8KB),以 32 字节为一行。当 CPU 访问 SDRAM 时,数据会被缓存到 D-Cache 中。若 DMA 外设直接访问 SDRAM(绕过 CPU),则可能产生两种冲突: - **写一致性**:CPU 修改缓存行但未写回 SDRAM,DMA 读取到旧数据。 - **读一致性**:DMA 写入 SDRAM 新数据,但 CPU 缓存中仍是旧副本。 因此,在 CPU 与 DMA 交替访问同一 SDRAM 区域时,必须显式维护缓存一致性。 # 三种排查手段 ## 手段一:使用 CMSIS 函数进行缓存清理与失效 ### 原理 CMSIS 提供 `SCB_CleanDCache()`、`SCB_InvalidateDCache()` 及按地址操作的函数。清理(Clean)将脏缓存行写回 SDRAM;失效(Invalidate)丢弃缓存行,强制下次从 SDRAM 读取。 ### 配置步骤 1. 确保在 `system_stm32f4xx.c` 中启用了 D-Cache(`SCB_EnableDCache()`)。 2. 在 DMA 写入 SDRAM 前,调用 `SCB_CleanDCache_by_Addr()` 清理相关区域。 3. 在 DMA 写入完成后,调用 `SCB_InvalidateDCache_by_Addr()` 使缓存失效。 ### 代码示例 ```c // 假设 SDRAM 缓冲区地址为 0xC0000000,长度 1024 字节 #define BUF_ADDR 0xC0000000 #define BUF_SIZE 1024 // CPU 写数据到 SDRAM,然后 DMA 读取 void CPU_Write_Then_DMA_Read(void) { uint32_t *buf = (uint32_t *)BUF_ADDR; for (int i = 0; i < BUF_SIZE/4; i++) { buf[i] = i; // 写入数据 } // 清理缓存,确保数据写回 SDRAM SCB_CleanDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE); // 启动 DMA 读取(此处省略 DMA 配置) DMA_Start_Read(BUF_ADDR, BUF_SIZE); } // DMA 写入 SDRAM 后,CPU 读取 void DMA_Write_Then_CPU_Read(void) { // 启动 DMA 写入(此处省略) DMA_Start_Write(BUF_ADDR, BUF_SIZE); // 等待 DMA 完成 while(DMA_IsBusy()); // 使缓存失效,强制从 SDRAM 读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t *)BUF_ADDR, BUF_SIZE); uint32_t *buf = (uint32_t *)BUF_ADDR; for (int i = 0; i < BUF_SIZE/4; i++) { printf("%d\n", buf[i]); // 读取数据 } } ``` ### 注意事项 - 地址必须 32 字节对齐,长度最好为 32 的倍数,否则可能未完全覆盖。 - 频繁清理/失效会降低性能,应仅在必要区域使用。 ## 手段二:配置 MPU 将 SDRAM 区域设为非缓存 ### 原理 通过 MPU(内存保护单元)将 SDRAM 区域配置为 `Normal, Non-Cacheable`,则 CPU 访问该区域时直接读写 SDRAM,不经过 D-Cache,从而彻底避免一致性问题。适合对性能要求不高或访问频率低的区域。 ### 配置步骤 1. 初始化 MPU,设置区域基地址、大小和属性。 2. 属性中 `TEX=0, C=0, B=0` 表示 Non-Cacheable。 3. 使能 MPU。 ### 代码示例 ```c void MPU_Config_SDRAM_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置 SDRAM 区域,基地址 0xC0000000,大小 8MB(根据实际调整) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_8MB; 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_NOT_SHAREABLE; 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); } ``` ### 注意事项 - MPU 区域大小必须是 2 的幂,且最小 32 字节。 - 非缓存区域访问速度较慢,若 SDRAM 带宽要求高,需权衡。 - 确保 MPU 配置在系统早期完成,避免运行时切换。 ## 手段三:使用 DMA 缓冲区的对齐与双缓冲策略 ### 原理 将 DMA 缓冲区设计为 32 字节对齐,并采用双缓冲(Double Buffer)机制。CPU 处理一个缓冲区时,DMA 操作另一个,并在切换时进行缓存维护。这能减少缓存操作频率,并避免同一时刻的冲突。 ### 配置步骤 1. 定义两个缓冲区,使用 `__attribute__((aligned(32)))` 对齐。 2. 在 DMA 完成中断中切换缓冲区,并调用缓存维护函数。 3. 确保 DMA 配置为循环或双缓冲模式(如 STM32F4 的 DMA2D 或通用 DMA)。 ### 代码示例 ```c #define BUF_SIZE 1024 __attribute__((aligned(32))) uint8_t buf1[BUF_SIZE]; __attribute__((aligned(32))) uint8_t buf2[BUF_SIZE]; volatile uint8_t active_buf = 0; // 0 表示 buf1,1 表示 buf2 void DMA_TransferComplete_Callback(void) { // 切换缓冲区 if (active_buf == 0) { active_buf = 1; // 使旧缓冲区失效,准备 CPU 读取 SCB_InvalidateDCache_by_Addr((uint32_t *)buf1, BUF_SIZE); // 配置 DMA 使用 buf2(此处省略) DMA_SetBuffer(buf2, BUF_SIZE); } else { active_buf = 0; SCB_InvalidateDCache_by_Addr((uint32_t *)buf2, BUF_SIZE); DMA_SetBuffer(buf1, BUF_SIZE); } } void CPU_Process_Data(void) { uint8_t *data = (active_buf == 0) ? buf1 : buf2; // 处理数据(注意:处理前确保对应缓冲区已失效) Process(data, BUF_SIZE); } ``` ### 注意事项 - 缓冲区必须 32 字节对齐,否则缓存操作可能出错。 - 双缓冲可避免 CPU 和 DMA 同时访问同一缓冲区,但需在切换时正确维护缓存。 - 若使用 DMA2D(图形加速),需注意其与 D-Cache 的交互,通常需要清理缓存。 # 总结 D-Cache 与 SDRAM 的一致性问题是 STM32F4 开发中的常见陷阱。本文介绍了三种排查手段: - **缓存清理/失效**:最直接,但需注意地址对齐和性能开销。 - **MPU 非缓存区域**:彻底规避,但牺牲速度。 - **双缓冲策略**:工程上常用,兼顾性能与正确性。 实际开发中,建议先使用手段二快速验证问题是否由缓存引起,再根据性能需求选择手段一或三。掌握这些技巧,能让你在嵌入式开发中少走弯路。