# 引言:一次诡异的 SDRAM 数据错乱 在嵌入式开发中,STM32F4 系列(如 STM32F407、F429)凭借其高性能和丰富外设广受欢迎。当项目需要大容量存储时,外扩 SDRAM 是常见选择。然而,当你启用 D-Cache 后,可能会遇到这样的问题:DMA 从 SDRAM 读取的数据偶尔是旧的,或者 CPU 写入的数据 DMA 看不到,甚至程序随机跑飞。这就是典型的 D-Cache 与 SDRAM 数据一致性问题。 # 1. 原理剖析:为什么会出现数据不一致? ## 1.1 D-Cache 的工作机制 D-Cache(数据缓存)是 CPU 与主存(SDRAM)之间的小容量高速缓存。当 CPU 访问内存时,首先检查 Cache 是否命中;若命中,则直接操作 Cache 行(通常为 32 字节),不会立即更新 SDRAM。这种“写回(Write-back)”策略提高了性能,但也带来了隐患。 ## 1.2 不一致的根源 - **CPU 写,DMA 读**:CPU 修改了 Cache 中的数据,但数据尚未写回 SDRAM。此时 DMA 从 SDRAM 读取,得到的是旧数据。 - **DMA 写,CPU 读**:DMA 将新数据写入 SDRAM,但 Cache 中仍保留旧副本。CPU 读取时命中 Cache,得到的是过时数据。 - **代码执行**:如果代码或常量数据存放在 SDRAM,且被 Cache 缓存,DMA 更新后同样会导致执行错误。 # 2. 踩坑实录:我的项目经历 在开发一个基于 STM32F429 的音频采集系统时,我使用 SDRAM 作为音频缓冲区。启用 D-Cache 后,ADC 通过 DMA 将数据写入 SDRAM,CPU 读取处理。起初一切正常,但运行几分钟后,音频出现爆音,调试发现部分数据块是旧数据。排查半天,最终定位到 D-Cache 未失效(Invalidate)导致。后来在另一项目中,CPU 将图像数据写入 SDRAM,然后 DMA 输出到 LCD,出现图像撕裂,原因是 Cache 未清理(Clean)。 # 3. 解决方案:三种配置策略 ## 3.1 方案一:关闭 D-Cache(最简单,但性能损失大) 直接不启用 D-Cache,所有访问都直达 SDRAM。适合对性能要求不高的场景。 ```c // 在 main 函数初始化时,不调用 SCB_EnableDCache() 即可 // 或者显式关闭(默认关闭) SCB_DisableDCache(); ``` **优点**:无一致性烦恼。 **缺点**:CPU 访问 SDRAM 速度大幅下降,可能成为性能瓶颈。 ## 3.2 方案二:软件维护 Cache(常用,需精细控制) 在关键操作前后,手动执行 Cache 清理(Clean)或无效化(Invalidate)。 ### 3.2.1 核心函数 ```c // 清理:将 Cache 中修改的数据写回 SDRAM SCB_CleanDCache(); // 无效化:丢弃 Cache 中的数据,下次从 SDRAM 重新读取 SCB_InvalidateDCache(); // 清理并无效化 SCB_CleanInvalidateDCache(); // 按地址范围操作(注意:地址需 32 字节对齐,长度需为 32 的倍数) SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize); SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize); ``` ### 3.2.2 典型场景代码 **场景 A:CPU 写数据,DMA 输出(如 LCD 显示)** ```c // 假设 buffer 在 SDRAM,长度为 1024 字节,且 32 字节对齐 uint8_t buffer[1024] __attribute__((aligned(32))); // CPU 填充数据 fill_buffer(buffer); // 关键:将 Cache 中的数据写回 SDRAM SCB_CleanDCache_by_Addr((uint32_t*)buffer, 1024); // 启动 DMA 传输(从 SDRAM 读取) DMA_Start_Transmit(buffer, size); ``` **场景 B:DMA 写入数据,CPU 读取(如 ADC 采集)** ```c // 启动 DMA 接收(写入 SDRAM) DMA_Start_Receive(buffer, size); // 等待 DMA 完成 while(DMA_GetFlag() == 0); // 关键:使 Cache 失效,强制从 SDRAM 重新加载 SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, 1024); // 现在可以安全读取 buffer process_data(buffer); ``` ### 3.2.3 注意事项 - **对齐与长度**:地址必须 32 字节对齐,长度必须是 32 的倍数,否则行为未定义。 - **性能开销**:频繁清理/无效化会降低性能,应尽量批量操作。 - **中断安全**:在中断中维护 Cache 时,注意临界区保护。 ## 3.3 方案三:MPU 配置(推荐,硬件自动处理) 通过 MPU(内存保护单元)将 SDRAM 区域配置为“非缓存”或“写直达”属性,让硬件自动保证一致性。 ### 3.3.1 配置步骤 1. **使能 MPU**:在系统初始化时开启 MPU。 2. **配置区域**:设置 SDRAM 基地址、大小、访问权限和缓存属性。 3. **设置缓存策略**:选择“非缓存”或“写直达,读分配”。 ### 3.3.2 示例代码(基于 STM32F4 HAL 库) ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); // 配置 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_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); } // 在 main 中调用 int main(void) { HAL_Init(); SystemClock_Config(); MPU_Config(); // 先配置 MPU // 初始化 SDRAM 等外设 // 注意:D-Cache 可以正常使能,但 SDRAM 区域不会被缓存 SCB_EnableDCache(); // ... } ``` ### 3.3.3 优点与注意 - **硬件自动处理**:无需手动维护 Cache,代码简洁。 - **性能折中**:非缓存区域访问速度稍慢,但比关闭 D-Cache 好(其他区域仍可缓存)。 - **配置顺序**:MPU 必须在使能 D-Cache 之前配置,否则无效。 - **区域重叠**:确保 SDRAM 区域不与其它 MPU 区域冲突。 # 4. 总结与最佳实践 - **优先使用 MPU 方案**:对于 SDRAM 等大块内存,配置为非缓存或写直达,从根源解决一致性问题。 - **若使用软件维护**:务必注意地址对齐和长度倍数,并在每次 DMA 传输前后正确调用清理/无效化。 - **调试技巧**:若出现随机数据错误,先检查 Cache 配置,再检查 DMA 描述符。 - **性能考量**:如果 SDRAM 访问频繁且对性能要求高,可考虑使用“写直达”策略(TEX=0,C=1,B=0),但需硬件支持。 最后,记住一句口诀:**“DMA 前 Clean,DMA 后 Invalidate”**。掌握 D-Cache 与 SDRAM 的一致性配置,你的嵌入式项目将更加稳定可靠。