引言
在 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 加载。
配置步骤
- 启用 D-Cache(通常在系统初始化时)。
- 在 DMA 发送前,调用
SCB_CleanDCache_by_Addr清理缓冲区。 - 在 DMA 接收完成后,调用
SCB_InvalidateDCache_by_Addr使缓冲区失效。
代码示例
// 缓冲区需按 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 保持一致。
配置步骤
- 在系统初始化时,配置 MPU 区域。
- 设置区域基地址、大小、属性(禁止缓存)。
- 使能 MPU。
代码示例
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 配合链接脚本实现。
配置步骤
- 在链接脚本中定义一个新的内存区域(如
RAM_DMA)。 - 将缓冲区变量放入该区域。
- 使用 MPU 将该区域配置为不可缓存(类似解法二)。
代码示例(GCC 环境)
// 定义在自定义段中
__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 外设,避免数据不一致的坑。