# STM32F4 系列 D-Cache 与 SDRAM 数据一致性问题的三种实战解法 ## 一、问题根源:D-Cache 与 SDRAM 的“双轨制” STM32F4 系列(如 STM32F407、F429)内置了 D-Cache(数据缓存),用于加速对内部 SRAM 和外部存储器的访问。当 CPU 通过 D-Cache 读写 SDRAM 时,数据会先被缓存到 Cache 行(通常 32 字节),而不会立即同步到 SDRAM。这导致两个典型问题: - **写不一致(Write-back)**:CPU 修改了 Cache 中的数据,但 SDRAM 中仍是旧值,若此时外设(如 DMA)直接读取 SDRAM,会拿到过期数据。 - **读不一致(Read-invalidate)**:DMA 将新数据写入 SDRAM,但 Cache 中仍保留旧副本,CPU 读取时命中 Cache,得到错误数据。 这种“双轨制”在涉及 DMA、以太网、摄像头等外设时尤为致命。下面介绍三种实战解法,按复杂度递增排列。 ## 二、解法一:直接关闭 D-Cache(简单粗暴,适合低性能场景) ### 原理 关闭 D-Cache 后,CPU 每次读写 SDRAM 都直接访问物理内存,彻底消除一致性问题。代价是性能下降,尤其对频繁访问 SDRAM 的应用(如 GUI 缓冲)影响明显。 ### 配置步骤 1. 在系统初始化时,调用 `SCB_DisableDCache()` 关闭 D-Cache。 2. 确保所有外设(DMA、LTDC 等)正常工作,无需额外 Cache 维护。 ### 代码示例 ```c #include "stm32f4xx.h" void SystemInit_CacheConfig(void) { // 关闭 D-Cache(注意:需在启动代码后调用) SCB_DisableDCache(); // 可选:同时关闭 I-Cache(若不需要) // SCB_DisableICache(); } int main(void) { SystemInit(); SystemInit_CacheConfig(); // 后续代码直接访问 SDRAM,无需担心一致性 uint32_t *sdram_base = (uint32_t *)0xC0000000; *sdram_base = 0x12345678; // 直接写入 SDRAM while(1); } ``` ### 注意事项 - 仅适用于对性能要求不高的场景,或 SDRAM 访问频率极低的应用。 - 关闭 D-Cache 后,CPU 访问 SDRAM 的延迟可能增加(因无缓存加速),需评估实时性。 - 若使用 LTDC 显示控制器,关闭 D-Cache 可能导致帧缓冲刷新效率降低,需权衡。 ## 三、解法二:手动维护 Cache 一致性(灵活可控,适合中等复杂度) ### 原理 保留 D-Cache,但在关键操作前后手动执行 Cache 清理(Clean)或失效(Invalidate)操作。 - **Clean**:将 Cache 中脏数据写回 SDRAM(用于 CPU 写后,DMA 读前)。 - **Invalidate**:丢弃 Cache 中数据,强制从 SDRAM 重新读取(用于 DMA 写后,CPU 读前)。 ### 配置步骤 1. 初始化 D-Cache(默认开启,若未开启则调用 `SCB_EnableDCache()`)。 2. 在每次 DMA 传输前,调用 `SCB_CleanDCache_by_Addr()` 清理相关地址范围。 3. 在 DMA 传输完成后,调用 `SCB_InvalidateDCache_by_Addr()` 失效相关地址范围。 ### 代码示例 ```c #include "stm32f4xx.h" #define SDRAM_BASE 0xC0000000 #define BUFFER_SIZE 1024 uint32_t buffer[BUFFER_SIZE] __attribute__((at(SDRAM_BASE))); // 放在 SDRAM void DMA_ReadFromSDRAM(void) { // 1. CPU 写数据到 buffer for (int i = 0; i < BUFFER_SIZE; i++) { buffer[i] = i; } // 2. 清理 Cache,确保数据写回 SDRAM SCB_CleanDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE * sizeof(uint32_t)); // 3. 启动 DMA 读取 SDRAM(此处省略 DMA 配置) // DMA_Start(...); } void DMA_WriteToSDRAM(void) { // 1. 等待 DMA 传输完成(此处省略) // 2. 失效 Cache,使 CPU 从 SDRAM 重新读取 SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, BUFFER_SIZE * sizeof(uint32_t)); // 3. CPU 读取 buffer,此时数据是最新的 uint32_t val = buffer[0]; } ``` ### 注意事项 - 地址必须按 32 字节对齐(Cache 行大小),否则操作无效或出错。 - 清理/失效操作有开销,频繁调用会影响性能,建议批量处理。 - 若使用 RTOS,需注意任务切换时 Cache 状态可能变化,建议在临界区执行。 ## 四、解法三:MPU 配置 + DMA 双缓冲(高性能,适合复杂系统) ### 原理 利用 MPU(内存保护单元)将 SDRAM 区域配置为“非缓存”(或写透)属性,同时配合 DMA 双缓冲机制,实现无手动干预的一致性管理。 - 将 SDRAM 区域设置为 `Device` 或 `Strongly-ordered` 属性,禁用 Cache,但这样会失去缓存加速。 - 更优方案:将 SDRAM 分为两个区域,一个配置为“写透”(Write-through),另一个为“写回”(Write-back),并让 DMA 与 CPU 交替使用不同区域(双缓冲),避免冲突。 ### 配置步骤 1. 启用 MPU,并配置 SDRAM 区域的属性。 2. 设置双缓冲:CPU 写区域 A,DMA 读区域 B,然后交换。 3. 在 DMA 中断中切换缓冲,无需手动 Cache 操作。 ### 代码示例 ```c #include "stm32f4xx.h" #define SDRAM_BASE 0xC0000000 #define BUFFER_SIZE 1024 // 定义两个缓冲,分别位于 SDRAM 的不同区域(假设地址连续) uint32_t buf_A[BUFFER_SIZE] __attribute__((at(SDRAM_BASE))); uint32_t buf_B[BUFFER_SIZE] __attribute__((at(SDRAM_BASE + 0x1000))); void MPU_Config(void) { // 使能 MPU MPU_Region_InitTypeDef MPU_InitStruct; MPU_StructInit(&MPU_InitStruct); // 配置区域 0:SDRAM 前 4KB 为写透(CPU 使用) MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = SDRAM_BASE; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // 写透 MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 禁用 Cache MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_Init(&MPU_InitStruct); // 配置区域 1:SDRAM 后 4KB 为写回(DMA 使用) MPU_InitStruct.BaseAddress = SDRAM_BASE + 0x1000; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 写回 MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_Init(&MPU_InitStruct); // 使能 MPU MPU_Cmd(ENABLE); } void DMA_Transfer(void) { static uint8_t toggle = 0; if (toggle) { // CPU 写 buf_A,DMA 读 buf_B(假设 DMA 已配置) for (int i = 0; i < BUFFER_SIZE; i++) buf_A[i] = i; // 启动 DMA 从 buf_B 读取(无需 Cache 操作,因为 buf_B 是写回,但 DMA 读取时 CPU 未写) } else { // CPU 写 buf_B,DMA 读 buf_A for (int i = 0; i < BUFFER_SIZE; i++) buf_B[i] = i; } toggle ^= 1; } int main(void) { SystemInit(); MPU_Config(); // 初始化 DMA 等外设 while(1) { DMA_Transfer(); // 等待 DMA 完成中断,再切换 } } ``` ### 注意事项 - MPU 配置需在启用 D-Cache 之前完成,否则可能无效。 - 双缓冲要求 DMA 和 CPU 严格同步,需使用中断或标志位。 - 区域大小需为 2 的幂,且对齐到区域大小。 - 此方案性能最优,但实现复杂,适合对吞吐量要求高的场景(如音频流、图像处理)。 ## 五、总结与选型建议 | 解法 | 性能影响 | 实现复杂度 | 适用场景 | |------|----------|------------|----------| | 关闭 D-Cache | 高(性能下降) | 低 | 简单应用,SDRAM 访问少 | | 手动维护 Cache | 中(需额外操作) | 中 | 一般 DMA 交互,如 ADC 采集 | | MPU+双缓冲 | 低(性能最优) | 高 | 高速数据流,如 LCD 显示、以太网 | 在实际项目中,建议优先评估性能需求。若 SDRAM 访问不频繁,直接关闭 D-Cache 是最稳妥的;若涉及 DMA 且性能敏感,则采用手动维护或 MPU 方案。无论哪种,务必在开发初期就考虑一致性设计,避免后期调试噩梦。