# 引言:DMA 与 D-Cache 的“隐形战争” 在嵌入式开发中,DMA 负责高效搬运数据,而 CPU 依赖 D-Cache 加速访问。然而,当两者操作同一内存时,D-Cache 的“懒惰”写回策略会引发数据不一致:CPU 修改的数据可能仍留在 Cache 中,DMA 却从主存读取旧数据;反之,DMA 写入主存后,CPU 可能读到 Cache 中的过期数据。STM32F4 系列(Cortex-M4)默认不启用 D-Cache,但若使用外部 SDRAM 或开启 D-Cache 加速,问题便浮现。本文以 STM32F429 为例,通过 MPU 配置解决这一经典难题。 # 原理剖析:Cache 与 DMA 的冲突根源 ## 1. D-Cache 的工作机制 - **写回(Write-back)**:CPU 写数据时仅更新 Cache,标记为脏(Dirty),延迟写回主存。 - **写分配(Write-allocate)**:读未命中时,将主存数据加载到 Cache。 ## 2. DMA 的“旁路”特性 DMA 直接访问主存(SRAM 或 SDRAM),不经过 Cache。因此,当 CPU 与 DMA 共享缓冲区时,Cache 中的脏数据或过期数据会导致不一致。 ## 3. 解决方案思路 - **禁用 Cache**:简单粗暴,但牺牲性能。 - **MPU 配置区域为非缓存(Device 或 Strongly-ordered)**:强制 CPU 直接访问主存,适用于 DMA 缓冲区。 - **软件维护 Cache**:使用 `SCB_CleanDCache` 或 `SCB_InvalidateDCache`,但需谨慎时机。 # 实战配置:MPU 设置非缓存区域 ## 1. 硬件环境 - 芯片:STM32F429ZIT6(Cortex-M4,带 D-Cache) - 外部 SDRAM:IS42S16400J(1M×16bit,位于 Bank1) - 开发环境:Keil MDK 5.27 + STM32CubeF4 固件库 ## 2. 配置步骤 ### 步骤 1:启用 D-Cache 和 MPU 在系统初始化时,先使能 MPU,再使能 D-Cache(顺序不可颠倒)。 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 禁用 MPU 进行配置 HAL_MPU_Disable(); // 配置 SDRAM 区域(地址 0xC0000000,大小 4MB)为非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0xC0000000; MPU_InitStruct.Size = MPU_REGION_SIZE_4MB; 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); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } void Cache_Init(void) { // 使能 D-Cache(需在 MPU 之后) SCB_EnableDCache(); } ``` ### 步骤 2:定义 DMA 缓冲区并放置到非缓存区域 将 DMA 缓冲区定义在 SDRAM 中(或通过链接脚本指定),确保其地址落在 MPU 配置的非缓存区域。 ```c // 使用 __attribute__ 指定段,或直接定义在 SDRAM 地址 uint8_t dma_rx_buffer[1024] __attribute__((section(".sdram"))); // 在链接脚本中,将 .sdram 段定位到 0xC0000000 之后 ``` ### 步骤 3:DMA 与 CPU 交互示例 以 UART DMA 接收为例,演示数据一致性保证。 ```c // 启动 DMA 接收 HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer)); // 在 DMA 传输完成回调中处理数据 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 由于缓冲区是非缓存的,CPU 直接读取主存,无需 Cache 操作 ProcessData(dma_rx_buffer); // 重新启动 DMA 接收 HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer)); } } ``` ### 步骤 4:软件维护 Cache(备用方案) 若无法使用 MPU,可在 DMA 操作前后手动维护 Cache。 ```c // 在 CPU 写数据后,启动 DMA 前,清 Cache SCB_CleanDCache(); // 在 DMA 完成,CPU 读数据前,无效化 Cache SCB_InvalidateDCache(); ``` # 完整代码示例:MPU + DMA 环形缓冲区 以下是一个更完整的示例,包含 MPU 配置、DMA 环形缓冲区实现。 ```c #include "stm32f4xx_hal.h" // 定义环形缓冲区结构(位于非缓存区域) typedef struct { uint8_t data[256]; uint16_t head; uint16_t tail; } RingBuffer; // 将缓冲区放置到 SDRAM 非缓存区域 RingBuffer g_rb __attribute__((section(".sdram"))); // MPU 配置函数(如前所述) void MPU_Config(void); // 初始化 DMA 接收 void DMA_Init(void) { // 配置 UART DMA 等,省略具体细节 } int main(void) { HAL_Init(); SystemClock_Config(); MPU_Config(); SCB_EnableDCache(); DMA_Init(); // 启动 DMA 接收,数据直接写入 g_rb.data HAL_UART_Receive_DMA(&huart1, g_rb.data, sizeof(g_rb.data)); while (1) { // 主循环处理数据,无需 Cache 操作 if (g_rb.tail != g_rb.head) { ProcessByte(g_rb.data[g_rb.tail++]); g_rb.tail %= sizeof(g_rb.data); } } } ``` # 注意事项与调试技巧 - **MPU 区域大小对齐**:MPU 区域大小必须是 2 的幂次,且基地址对齐。例如 4MB 区域要求基地址 0xC0000000 对齐到 4MB。 - **链接脚本**:确保 `.sdram` 段正确放置在 SDRAM 起始地址,否则 MPU 不生效。 - **Cache 操作时机**:若使用软件维护,务必在 DMA 启动前 Clean,完成后 Invalidate,顺序错误会导致数据错乱。 - **调试技巧**:使用逻辑分析仪或断点观察内存值,确认 Cache 行为。也可临时禁用 D-Cache 对比测试。 - **性能权衡**:非缓存区域访问速度较慢,仅对 DMA 缓冲区使用,其他高频数据仍可缓存。 # 结语 通过 MPU 将 DMA 缓冲区配置为非缓存,从根源上避免了 Cache 与 DMA 的数据一致性冲突,既保证了正确性,又保留了对其他区域的缓存加速。掌握这一技巧,将使你在处理高速数据采集、网络通信等场景时更加游刃有余。希望本文能成为你嵌入式开发路上的有力工具。