# 引言 在 STM32F4 系列(如 STM32F407、STM32F429)中,D-Cache 的引入大幅提升了 CPU 访问外部 SDRAM 或 Flash 的速度。然而,DMA 控制器直接访问物理内存,不经过 Cache,导致 CPU 与 DMA 看到的数据可能不一致。例如,CPU 写入数据到内存(仅更新 Cache),DMA 读取时可能拿到旧数据;反之,DMA 写入内存后,CPU 读取时可能命中过期的 Cache 行。这种问题在高速数据传输(如 ADC 采样、以太网收发)中尤为致命。 本文将针对 STM32F4 系列,提供三种经过验证的规避方案,并深入分析其原理与适用场景。 # 问题根源 STM32F4 的 D-Cache 采用写回(Write-back)策略,即 CPU 写操作仅更新 Cache,直到 Cache 行被替换时才写回内存。DMA 访问内存时绕过 Cache,因此: - **CPU → DMA**:CPU 写数据后,若 Cache 未写回,DMA 读到的是旧值。 - **DMA → CPU**:DMA 写入内存后,若 CPU 读取时命中 Cache 中的旧数据,则读到错误值。 解决思路无非三种:主动同步 Cache、绕过 Cache、或避免共享内存。 # 方案一:Cache 清理与失效(软件控制) ## 原理 通过操作 Cortex-M4 内核提供的 SCB 寄存器,强制将 Cache 内容写回内存(Clean)或使 Cache 行失效(Invalidate)。在 DMA 操作前后调用相应函数,保证数据一致性。 ## 配置步骤 1. 启用 D-Cache(若未启用): ```c SCB_EnableDCache(); ``` 2. 在 DMA 发送前,调用 `SCB_CleanDCache()` 或按地址范围清理。 3. 在 DMA 接收后,调用 `SCB_InvalidateDCache()` 或按地址范围失效。 ## 代码示例 ```c #include "stm32f4xx.h" // 假设使用 DMA 传输缓冲区 uint32_t buffer[1024] #define BUFFER_SIZE 1024 uint32_t buffer[BUFFER_SIZE] __attribute__((aligned(32))); // 32字节对齐,匹配Cache行 void DMA_Send(uint32_t *data, uint32_t size) { // 清理Cache,确保数据写回内存 SCB_CleanDCache_by_Addr((uint32_t*)data, (int32_t)size * sizeof(uint32_t)); // 配置并启动DMA传输(此处省略具体DMA配置) DMA_Start(data, size); } void DMA_Receive(uint32_t *data, uint32_t size) { // 启动DMA接收 DMA_Start(data, size); // 等待DMA完成(轮询或中断) while(DMA_IsBusy()); // 使Cache失效,强制从内存重新加载 SCB_InvalidateDCache_by_Addr((uint32_t*)data, (int32_t)size * sizeof(uint32_t)); } ``` ## 注意事项 - 地址必须 32 字节对齐,大小最好为 32 的倍数,否则可能影响相邻数据。 - 频繁调用 Clean/Invalidate 会降低性能,适合低频或小数据量场景。 - 使用 `SCB_CleanInvalidateDCache()` 可同时完成清理和失效。 # 方案二:MPU 配置非缓存区域(硬件隔离) ## 原理 利用 Cortex-M4 的 MPU(内存保护单元),将 DMA 使用的内存区域配置为“非缓存(Non-cacheable)”属性。这样 CPU 访问该区域时直接读写内存,绕过 D-Cache,从根本上避免不一致。 ## 配置步骤 1. 定义 MPU 区域,设置基地址、大小、属性。 2. 在系统初始化时,调用 MPU 配置函数。 3. 确保 DMA 缓冲区位于该区域内。 ## 代码示例 ```c #include "stm32f4xx.h" void MPU_Config(void) { // 禁用 MPU MPU->CTRL = 0; // 配置区域0:非缓存,可读写 MPU->RNR = 0; // 区域编号 MPU->RBAR = (uint32_t)buffer; // 基地址 MPU->RASR = (0x03 << 1) | // 全权限 (0x01 << 16) | // 指令访问禁止 (0x00 << 18) | // 非共享 (0x01 << 19) | // 非缓存(Normal, Non-cacheable) (0x00 << 21) | // 写回,但非缓存已覆盖 (0x01 << 24) | // 使能区域 (0x0F << 1); // 区域大小:2^(size+1) 字节,这里需根据实际调整 // 使能 MPU MPU->CTRL = 1; __DSB(); __ISB(); } // 初始化时调用 void SystemInit(void) { MPU_Config(); // ... 其他初始化 } ``` ## 注意事项 - 区域大小必须为 2 的幂,且基地址对齐。 - 非缓存区域会降低 CPU 访问速度,但 DMA 操作频繁时收益更大。 - 需确保 MPU 配置在启用 D-Cache 之前或之后均可,但建议在系统早期完成。 # 方案三:双缓冲机制(软件设计) ## 原理 使用两个缓冲区交替进行 DMA 和 CPU 操作。CPU 写入缓冲区 A 时,DMA 从缓冲区 B 读取;完成后交换角色。由于同一时刻只有一个缓冲区被 CPU 或 DMA 使用,且每次切换时进行 Cache 同步,可避免冲突。 ## 配置步骤 1. 分配两个缓冲区,大小相同。 2. 使用一个索引变量切换当前活动缓冲区。 3. 在切换前,对即将使用的缓冲区执行 Clean/Invalidate。 ## 代码示例 ```c #define BUF_SIZE 1024 uint32_t buf[2][BUF_SIZE] __attribute__((aligned(32))); volatile uint8_t active_buf = 0; void ProcessData(void) { // 假设DMA接收数据到当前缓冲区 uint32_t *current = buf[active_buf]; // 启动DMA接收(省略配置) DMA_Start(current, BUF_SIZE); while(DMA_IsBusy()); // 使当前缓冲区Cache失效,确保读取最新数据 SCB_InvalidateDCache_by_Addr(current, BUF_SIZE * sizeof(uint32_t)); // 处理数据... // 切换缓冲区 active_buf ^= 1; // 清理新缓冲区(即将用于DMA),确保无残留数据 SCB_CleanDCache_by_Addr(buf[active_buf], BUF_SIZE * sizeof(uint32_t)); } ``` ## 注意事项 - 缓冲区切换需原子操作,避免中断干扰。 - 适用于连续数据流场景,如音频、网络收发。 - 内存开销翻倍,但性能最优,无需频繁同步。 # 总结 三种方案各有优劣: - **Cache 清理/失效**:简单灵活,适合小数据量或低频操作,但需注意对齐和性能损耗。 - **MPU 非缓存区域**:硬件隔离,彻底避免问题,但牺牲部分 CPU 访问速度,适合 DMA 频繁且数据量大的场景。 - **双缓冲**:软件设计巧妙,性能最佳,但内存占用翻倍,适合实时性要求高的流式传输。 开发者应根据实际需求(数据量、频率、内存资源)选择合适方案。在 STM32F4 项目中,务必在初始化阶段规划好内存布局和 Cache 策略,避免后期调试的噩梦。希望本文能帮助你解决 D-Cache 与 DMA 的“爱恨情仇”,让系统稳定高效运行。