# 引言:一个诡异的 DMA 数据错误 在嵌入式开发中,当你使用 STM32F4 的 DMA 传输数据(例如从 ADC 或 UART 接收数据到内存),同时 CPU 也访问同一块内存时,可能会遇到数据时而正确、时而错误的现象。这通常不是逻辑问题,而是 D-Cache(数据缓存)与 DMA 之间的数据一致性问题。STM32F4 内核(Cortex-M4)带有 4KB 的 D-Cache,默认情况下,CPU 读写内存会经过 Cache,而 DMA 直接访问物理内存,两者可能看到不同的数据。 # 原理剖析:D-Cache 与 DMA 的冲突根源 ## 1. D-Cache 的工作机制 - D-Cache 是 CPU 与主存之间的高速缓存,以“行”(通常 32 字节)为单位存储数据。 - 当 CPU 读内存时,若命中 Cache,则直接返回 Cache 中的数据;若未命中,则从主存加载到 Cache。 - 当 CPU 写内存时,若采用“写回”(Write-back)策略,数据先写入 Cache,标记为“脏”,稍后才写回主存。 ## 2. DMA 的访问路径 - DMA 控制器直接通过总线访问物理内存(SRAM 或外部存储器),不经过 Cache。 - 因此,DMA 写入内存时,CPU 可能从 Cache 中读到旧数据;CPU 写入内存后,DMA 可能从主存中读到旧数据。 ## 3. 冲突场景示例 - 场景 A:DMA 从外设接收数据到缓冲区,CPU 读取该缓冲区。若 CPU 之前已缓存了该区域的数据,则可能读到旧值。 - 场景 B:CPU 填充缓冲区,然后启动 DMA 发送。若 CPU 写入的数据还停留在 Cache 中,DMA 会发送主存中的旧数据。 # 解决方案:MPU 配置内存属性 MPU(内存保护单元)允许你为不同内存区域设置访问权限和 Cache 策略。通过将 DMA 使用的内存区域配置为“非缓存”(或“写透”),可以强制 CPU 直接访问主存,从而避免一致性问题。 ## 配置步骤 1. **确定内存区域**:通常使用 SRAM 的某一段(如 0x20000000 起始的 32KB),或者外部 SDRAM。 2. **使能 MPU**:在系统初始化时,使能 MPU 并配置区域。 3. **设置区域属性**: - 对于 DMA 缓冲区,建议配置为“非缓存”(TEX=1, C=0, B=0)或“写透”(TEX=0, C=1, B=0)。 - 非缓存模式最简单,但性能稍低;写透模式读性能较好,但写操作直接写主存。 4. **注意对齐**:MPU 区域大小必须是 2 的幂,且起始地址对齐到区域大小。 # 完整代码示例 以下代码基于 STM32F407,使用 HAL 库,将 SRAM 的 0x20000000 开始的 32KB 配置为非缓存区域。 ```c #include "stm32f4xx_hal.h" // 定义 DMA 缓冲区(位于非缓存区域) // 注意:需要将缓冲区放置到指定地址,使用 __attribute__((at())) 或链接脚本 uint8_t dma_buffer[1024] __attribute__((at(0x20000000))); // MPU 配置函数 void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU HAL_MPU_Disable(); // 配置区域 0:0x20000000,大小 32KB,非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; MPU_InitStruct.Size = MPU_REGION_SIZE_32KB; 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_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } // 主函数示例 int main(void) { HAL_Init(); SystemClock_Config(); // 配置 MPU(必须在启用 Cache 之前) MPU_Config(); // 启用 D-Cache(如果之前未启用) SCB_EnableDCache(); // 初始化 DMA 等外设... while (1) { // 使用 dma_buffer 进行 DMA 传输和 CPU 访问 } } ``` ## 代码说明 - `dma_buffer` 被强制放置在 0x20000000 地址,确保落在 MPU 配置的区域内。 - MPU 配置中,`IsCacheable` 和 `IsBufferable` 均设为 `NOT_`,即非缓存、非缓冲,CPU 访问直接走主存。 - 注意:MPU 配置必须在使能 D-Cache 之前完成,否则可能不生效。 # 调试技巧与注意事项 - **检查 MPU 配置是否生效**:可以通过读取 MPU 寄存器或使用调试器查看内存访问行为。 - **使用 `SCB_CleanDCache()` 和 `SCB_InvalidateDCache()`**:如果不想配置 MPU,可以在 DMA 传输前后手动清理/无效化 Cache,但效率较低且容易遗漏。 - **缓冲区对齐**:DMA 缓冲区建议 32 字节对齐,避免跨 Cache 行导致部分一致性问题。 - **性能权衡**:非缓存区域访问速度较慢,仅将 DMA 缓冲区放在该区域,其他数据仍使用 Cache 以保持性能。 - **多区域配置**:如果使用外部 SDRAM,同样需要配置 MPU 区域,并注意总线带宽。 # 总结 通过 MPU 将 DMA 使用的内存区域配置为非缓存,是解决 STM32F4 上 DMA 与 CPU 数据一致性问题的标准方法。本文的原理分析和代码示例可以帮助你快速定位并修复此类问题。记住,在嵌入式开发中,Cache 一致性是高性能与正确性之间的平衡,理解底层机制才能写出健壮的代码。