引言
在 STM32F4 系列(如 STM32F429/469)中,当使用外部 SDRAM 作为帧缓冲或数据缓冲区时,D-Cache 的引入虽然提升了 CPU 访问速度,但也带来了数据一致性问题。CPU 写入的数据可能暂存在 Cache 中,而 DMA 或 LTDC 外设直接访问 SDRAM,导致读取到旧数据;反之,外设写入的数据可能被 Cache 中的旧数据覆盖。本文针对这一痛点,提供三种经过验证的解决方案。
问题根源
STM32F4 的 D-Cache 是 4 路组关联、总大小 4KB(部分型号 8KB),以 32 字节为一行。当 CPU 写内存时,数据先写入 Cache(写回策略),仅当 Cache 行被替换或显式清理时才写回 SDRAM。DMA 控制器不经过 Cache,直接访问 SDRAM,因此:
- CPU 写 → DMA 读:DMA 可能读到 SDRAM 中的旧数据,因为 CPU 的新数据还在 Cache 中。
- DMA 写 → CPU 读:CPU 可能读到 Cache 中的旧数据,因为 DMA 已更新 SDRAM,但 Cache 未失效。
解决方案一:Cache 清理与失效(软件控制)
原理
通过调用 CMSIS 提供的函数,在关键操作前后手动维护 Cache 一致性。
-
SCB_CleanDCache():将 Cache 中脏数据写回 SDRAM。 -
SCB_InvalidateDCache():使 Cache 行失效,强制下次读取从 SDRAM 加载。 -
SCB_CleanInvalidateDCache():两者结合。
配置步骤
- 启用 D-Cache(默认在启动文件中已启用,若未启用则需在 main 中调用
SCB_EnableDCache())。 - 在 DMA 传输前,清理包含源数据的 Cache 区域。
- 在 DMA 传输完成后,失效包含目标数据的 Cache 区域。
代码示例
// 假设使用 DMA 从 SDRAM 缓冲区 buf_src 传输数据到外设
#define BUF_SIZE 1024
uint32_t buf_src[BUF_SIZE] __attribute__((section(".sdram")));
uint32_t buf_dst[BUF_SIZE] __attribute__((section(".sdram")));
void dma_transfer_with_cache_maintenance(void) {
// 清理源缓冲区,确保 DMA 能看到 CPU 写入的最新数据
SCB_CleanDCache_by_Addr((uint32_t*)buf_src, BUF_SIZE * sizeof(uint32_t));
// 启动 DMA 传输(此处省略 DMA 配置)
DMA_Start_Transfer(buf_src, buf_dst, BUF_SIZE);
// 等待 DMA 完成
while(DMA_Is_Busy());
// 失效目标缓冲区,使 CPU 读取时从 SDRAM 重新加载
SCB_InvalidateDCache_by_Addr((uint32_t*)buf_dst, BUF_SIZE * sizeof(uint32_t));
}
注意事项
- 地址必须 32 字节对齐,长度建议为 32 的倍数,否则需手动处理边界。
- 频繁调用会影响性能,适合低频操作。
解决方案二:MPU 配置非缓存区域(硬件隔离)
原理
利用 MPU(内存保护单元)将 SDRAM 区域配置为“非缓存”属性(如 NORMAL, NON-CACHEABLE),使 CPU 访问该区域时直接读写 SDRAM,绕过 D-Cache。这样 DMA 和 CPU 看到的数据始终一致,无需软件干预。
配置步骤
- 定义 MPU 区域,覆盖 SDRAM 地址范围(例如 0xC0000000,大小 8MB)。
- 设置属性:
MPU_REGION_NORMAL_NON_CACHEABLE(或MPU_ACCESS_BUFFERABLE)。 - 使能 MPU 和区域。
代码示例
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_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);
}
注意事项
- 非缓存区域会降低 CPU 访问速度,但保证一致性,适合帧缓冲等高频访问场景,可接受性能损失。
- 需确保 MPU 区域与 SDRAM 物理地址完全匹配,避免覆盖其他外设。
解决方案三:DMA 双缓冲策略(架构优化)
原理
为 DMA 和 CPU 分配独立的缓冲区,通过交替使用避免同时访问同一内存区域。例如,CPU 写入缓冲区 A,DMA 从缓冲区 B 读取;当 DMA 完成,交换角色。这样 CPU 和 DMA 永远不会同时操作同一块内存,从根源上消除一致性问题。
配置步骤
- 在 SDRAM 中分配两个缓冲区(A 和 B)。
- 使用 DMA 双缓冲模式(如 STM32 的 DMA2D 或通用 DMA 的 double-buffer 功能)。
- 在 DMA 传输完成中断中切换缓冲区指针。
代码示例
#define BUF_SIZE 1024
uint32_t buf_A[BUF_SIZE] __attribute__((section(".sdram")));
uint32_t buf_B[BUF_SIZE] __attribute__((section(".sdram")));
volatile uint32_t *active_buf = buf_A;
volatile uint32_t *dma_buf = buf_B;
void DMA_Transfer_Complete_Callback(void) {
// 交换缓冲区
uint32_t *tmp = active_buf;
active_buf = dma_buf;
dma_buf = tmp;
// 启动下一次 DMA 传输,使用新的 dma_buf
DMA_Start_Transfer(dma_buf, BUF_SIZE);
}
void main_loop(void) {
// CPU 写入 active_buf,无需 Cache 维护
process_data(active_buf);
}
注意事项
- 需要额外的内存空间,但避免了 Cache 操作,性能最优。
- 适用于数据流场景(如音频、图像采集),不适合随机访问。
总结
| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | Cache 清理/失效 | 无需额外内存,实现简单 | 软件开销大,需注意对齐 | 低频小数据量传输 | | MPU 非缓存 | 硬件保证一致性,无需软件干预 | 降低 CPU 访问速度 | 高频访问的帧缓冲 | | DMA 双缓冲 | 性能最优,无 Cache 操作 | 内存占用翻倍,逻辑复杂 | 连续数据流处理 |
开发者应根据实际需求选择:若追求简单,用方案一;若性能敏感且可接受非缓存,用方案二;若数据流大且内存充足,用方案三。理解 D-Cache 与 SDRAM 的交互机制,是解决嵌入式系统数据一致性问题的关键。