# 引言 在 STM32F4 系列(如 STM32F407、STM32F429)中,Cortex-M4 内核集成了可选的 D-Cache(数据缓存)和 I-Cache(指令缓存)。启用 D-Cache 后,CPU 对内存的读写会先经过缓存,而 DMA 外设则直接访问物理内存(SRAM)。这种架构差异导致了一个经典问题:**CPU 和 DMA 看到的数据可能不一致**。例如,CPU 写入数据后,数据可能仍停留在 Cache 中,尚未写回 SRAM,此时 DMA 读取 SRAM 就会得到旧数据;反之,DMA 更新了 SRAM,CPU 读取时却可能命中过期的 Cache 行。 本文将针对 STM32F4 系列,提供三种实用解法,并附上基于 HAL 库的代码示例。 # 问题根源:Cache 与 DMA 的“视角”差异 Cortex-M4 的 D-Cache 采用写回(Write-back)策略,即 CPU 写操作只更新 Cache,并在特定时机(如 Cache 行被替换或显式清理)才写回 SRAM。DMA 控制器不经过 Cache,直接读写 SRAM。因此,当 CPU 和 DMA 共享同一块内存区域时,就会发生数据不一致。 **典型场景**: - 使用 DMA 发送内存中的数据(如 UART、SPI、以太网)。 - 使用 DMA 接收数据到内存,再由 CPU 处理。 # 解法一:软件维护 Cache(清理与失效) 这是最直接的方法,通过操作协处理器 CP15 指令或 CMSIS 提供的函数,在关键操作前后手动同步 Cache 和 SRAM。 ## 原理 - **清理(Clean)**:将 Cache 中脏数据写回 SRAM。 - **失效(Invalidate)**:丢弃 Cache 中的数据,下次访问时重新从 SRAM 加载。 ## 配置步骤 1. 启用 D-Cache(通常在系统初始化时)。 2. 在 DMA 发送前,调用 `SCB_CleanDCache_by_Addr` 清理缓冲区。 3. 在 DMA 接收完成后,调用 `SCB_InvalidateDCache_by_Addr` 使缓冲区失效。 ## 代码示例 ```c // 缓冲区需按 32 字节对齐(Cache 行大小) __ALIGN_BEGIN static uint8_t tx_buffer[256] __ALIGN_END; __ALIGN_BEGIN static uint8_t rx_buffer[256] __ALIGN_END; // 启用 D-Cache SCB_EnableDCache(); // DMA 发送前:确保 CPU 写入的数据写回 SRAM SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer)); HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer)); // DMA 接收完成后(在回调中):使 Cache 失效,强制从 SRAM 重新读取 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer)); // 现在可以安全处理 rx_buffer } } ``` ## 注意事项 - 地址和大小必须按 32 字节对齐,否则可能无效或引发异常。 - 频繁清理/失效会降低性能,适合数据量小或低频场景。 # 解法二:MPU 配置为“不可缓存”区域 通过内存保护单元(MPU)将 DMA 使用的内存区域配置为“不可缓存”(或写-through),从而绕过 Cache。 ## 原理 MPU 允许将内存区域划分为不同属性,包括 Cache 策略。将共享缓冲区设为“不可缓存”后,CPU 访问该区域时直接读写 SRAM,与 DMA 保持一致。 ## 配置步骤 1. 在系统初始化时,配置 MPU 区域。 2. 设置区域基地址、大小、属性(禁止缓存)。 3. 使能 MPU。 ## 代码示例 ```c void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct; __HAL_RCC_MPU_CLK_ENABLE(); 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.SubRegionDisable = 0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // 不使用 TEX MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; // 共享属性 MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:不可缓存 MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT); } // 在 main 中调用 MPU_Config() 后再启用 D-Cache ``` ## 注意事项 - MPU 区域大小必须是 2 的幂,且起始地址对齐。 - 不可缓存区域会降低 CPU 访问速度,但保证一致性。 - 适用于缓冲区固定且大小已知的场景。 # 解法三:使用无缓存 DMA 缓冲区(通过链接脚本或属性) 将 DMA 缓冲区放置在特定的内存段中,该段被配置为无缓存属性。在 STM32F4 上,通常利用 GCC 的 `__attribute__((section))` 或 IAR 的 `#pragma location` 将变量放入自定义段,并在链接脚本中设置该段的属性。 ## 原理 通过链接脚本将缓冲区分配到 SRAM 的某个区域,并利用 MPU 或硬件特性(如 TCM 内存)使其绕过 Cache。STM32F4 没有 TCM,但可以使用 MPU 配合链接脚本实现。 ## 配置步骤 1. 在链接脚本中定义一个新的内存区域(如 `RAM_DMA`)。 2. 将缓冲区变量放入该区域。 3. 使用 MPU 将该区域配置为不可缓存(类似解法二)。 ## 代码示例(GCC 环境) ```c // 定义在自定义段中 __attribute__((section(".dma_buffer"))) uint8_t dma_rx_buf[128]; // 链接脚本(.ld)中添加: // .dma_buffer (NOLOAD) : // { // . = ALIGN(32); // *(.dma_buffer) // . = ALIGN(32); // } >RAM_DMA // 在内存区域定义中: // RAM_DMA (xrw) : ORIGIN = 0x20000000, LENGTH = 32K // 示例,需根据实际 SRAM 划分 // 然后使用 MPU 将 0x20000000 区域配置为不可缓存(如解法二) ``` ## 注意事项 - 需要手动调整链接脚本,确保区域不重叠。 - 这种方法将缓冲区隔离,便于管理,但增加了链接配置复杂度。 - 适合大型项目,可集中管理所有 DMA 缓冲区。 # 总结与选型建议 | 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 软件维护 Cache | 简单、无需额外配置 | 性能损失,需注意对齐 | 低频、小数据量 | | MPU 不可缓存区域 | 性能较好,配置一次 | 占用 MPU 区域,灵活性低 | 固定缓冲区、中等数据量 | | 无缓存 DMA 缓冲区 | 隔离清晰,性能最优 | 链接脚本复杂,移植性差 | 大型项目、高频传输 | **核心建议**: - 如果项目简单,优先使用软件维护 Cache,快速解决问题。 - 如果缓冲区固定且性能要求高,使用 MPU 配置。 - 如果项目复杂,推荐结合链接脚本和 MPU,实现无缓存专用缓冲区。 无论选择哪种方法,务必确保缓冲区地址按 32 字节对齐,并在 DMA 操作前后正确处理 Cache。希望本文能帮助你在 STM32F4 上稳定运行 DMA 外设,避免数据不一致的坑。