# 一、D-Cache 与 SDRAM 的相爱相杀 STM32F4 系列(如 STM32F429/439)内置了 4KB 的 D-Cache 和 I-Cache,用于加速对慢速存储器(如外部 SDRAM)的访问。Cache 作为 CPU 与 SDRAM 之间的高速缓存,遵循局部性原理,将常用数据复制到 Cache 中,从而减少总线等待时间。 然而,Cache 的引入带来了**数据一致性(Coherency)**问题: - **CPU 写 SDRAM**:数据可能只写入 Cache,尚未回写(Write-Back)到 SDRAM,此时若 DMA 外设直接读 SDRAM,会读到旧数据。 - **DMA 写 SDRAM**:DMA 将数据从外设搬运到 SDRAM,但 Cache 中可能还保留着旧副本,CPU 读 Cache 时得到的是过期数据。 STM32F4 的 D-Cache 采用**写回(Write-Back)**策略,且不具备硬件一致性协议(如 MESI),因此软件必须显式维护一致性。 # 二、一致性问题的本质与 volatile 的局限 很多开发者习惯用 `volatile` 修饰共享变量,期望解决一致性问题。但 `volatile` 只告诉编译器“不要优化对该变量的访问”,它**不生成任何内存屏障指令**,也不影响 Cache 行为。例如: ```c volatile uint32_t *buf = (uint32_t *)0xC0000000; // SDRAM 地址 *buf = 0x12345678; // 可能只写 Cache ``` 此时若 DMA 随后读取该地址,可能得到旧值。`volatile` 无法保证数据从 Cache 回写到 SDRAM。 真正解决一致性需要两步: 1. **Cache 维护操作**:使用 CMSIS 提供的函数 `SCB_CleanDCache()`、`SCB_InvalidateDCache()` 或按地址操作的 `SCB_CleanDCache_by_Addr()` 等。 2. **内存屏障**:确保 Cache 操作完成后再进行 DMA 或外设访问,防止指令重排。 # 三、实战配置:以 STM32F429 + SDRAM + DMA 为例 ## 3.1 硬件环境 - MCU:STM32F429ZI(Cortex-M4,带 D-Cache) - 外部 SDRAM:IS42S16400J(16MB,映射到 0xC0000000) - 外设:DMA2 从 ADC 搬运数据到 SDRAM ## 3.2 初始化步骤 ### 1. 使能 D-Cache 在 `main()` 中,初始化 SDRAM 后使能 Cache: ```c void SystemInit_Cache(void) { SCB_EnableDCache(); SCB_EnableICache(); } ``` ### 2. 配置 SDRAM 为 Cacheable STM32F4 的 MPU(内存保护单元)默认将外部 RAM 区域配置为 Write-Back 且 Cacheable。若需更改,需配置 MPU 寄存器。一般默认即可,但建议显式设置: ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_16MB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_Init(&MPU_InitStruct); MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` ### 3. DMA 传输前的 Cache 清理 当 CPU 写数据到 SDRAM,然后启动 DMA 将数据发送到外设(如 DAC)时,需先 Clean Cache: ```c void DMA_SendFromSDRAM(uint32_t *src, uint32_t len) { // 确保 CPU 写的数据已回写到 SDRAM SCB_CleanDCache_by_Addr((uint32_t *)src, (int32_t)len); // 内存屏障,确保 Clean 完成 __DSB(); // 启动 DMA 传输(假设 DMA 已配置) DMA_Start(src, (uint32_t *)DAC_BASE, len); } ``` ### 4. DMA 接收后的 Cache 失效 当 DMA 从外设接收数据到 SDRAM,然后 CPU 读取时,需先 Invalidate Cache: ```c void DMA_ReceiveToSDRAM(uint32_t *dst, uint32_t len) { // 启动 DMA 传输 DMA_Start((uint32_t *)ADC_BASE, dst, len); // 等待 DMA 完成(如中断标志) while (DMA_GetFlagStatus(DMA_FLAG_TC) == RESET); // 使 Cache 失效,丢弃旧副本 SCB_InvalidateDCache_by_Addr((uint32_t *)dst, (int32_t)len); __DSB(); // 现在 CPU 读取的是 SDRAM 中的新数据 } ``` ## 3.3 完整代码示例(简化) ```c #include "stm32f4xx.h" #define SDRAM_BASE 0xC0000000 #define BUFFER_SIZE 1024 uint32_t buf[BUFFER_SIZE] __attribute__((at(SDRAM_BASE))); // 放在 SDRAM void SystemInit(void) { // 初始化时钟、SDRAM、MPU(略) MPU_Config(); SCB_EnableDCache(); SCB_EnableICache(); } int main(void) { // 填充数据 for (int i = 0; i < BUFFER_SIZE; i++) { buf[i] = i; } // 发送到外设前 Clean Cache SCB_CleanDCache_by_Addr((uint32_t *)buf, sizeof(buf)); __DSB(); // 启动 DMA 发送(略) // 接收数据后 Invalidate Cache // 假设 DMA 已填充 buf SCB_InvalidateDCache_by_Addr((uint32_t *)buf, sizeof(buf)); __DSB(); // 读取 buf 即为最新数据 uint32_t val = buf[0]; while (1); } ``` # 四、内存屏障指令详解 Cortex-M4 支持两条屏障指令: - **DMB(Data Memory Barrier)**:确保在 DMB 之前的所有内存访问(读/写)完成后,才执行后续的内存访问。适用于数据同步。 - **DSB(Data Synchronization Barrier)**:更强的屏障,确保在 DSB 之前的所有内存访问完成后,才执行后续的**任何指令**(包括取指)。适用于需要等待 Cache 操作完成或外设操作完成时。 在 CMSIS 中,可使用 `__DMB()` 和 `__DSB()`。通常,在 Cache 维护操作后使用 `__DSB()` 以确保操作完成。 # 五、注意事项与调试技巧 - **地址对齐**:`SCB_CleanDCache_by_Addr()` 和 `SCB_InvalidateDCache_by_Addr()` 要求地址按 32 字节对齐(Cache line 大小)。若未对齐,需手动处理边界。 - **性能权衡**:频繁 Clean/Invalidate 会降低性能,建议采用 DMA 双缓冲或环形缓冲区,减少 Cache 操作次数。 - **调试方法**:若怀疑一致性问题,可暂时禁用 D-Cache(`SCB_DisableDCache()`)观察现象是否消失。若消失,则确认是 Cache 问题。 - **使用 MPU 配置非 Cacheable 区域**:对于 DMA 频繁访问的缓冲区,可将其配置为 Write-Through 或 Non-Cacheable,避免手动维护。但会牺牲性能。 - **注意编译器优化**:即使使用 volatile,编译器也可能将多个访问合并,因此仍需显式屏障。 # 六、总结 STM32F4 的 D-Cache 是性能利器,但必须正确维护数据一致性。`volatile` 不能替代 Cache 操作和内存屏障。通过 `SCB_CleanDCache` / `SCB_InvalidateDCache` 配合 `__DSB()`,可以确保 CPU 与 DMA 之间的数据同步。在实际项目中,建议将 DMA 缓冲区设计为独立区域,并统一管理 Cache 操作,避免散落各处的维护代码。掌握这些技巧,能让你在嵌入式开发中少走弯路,快速定位疑难 Bug。