STM32F4 上利用 D-Cache 与 MPU 配置解决 DMA 与 CPU 数据一致性问题的实战排查
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407)中,当 DMA 与 CPU 同时访问同一内存区域时,D-Cache 的启用可能导致数据不一致,引发难以排查的随机故障。本文从缓存一致性原理出发,结合一个实际 DMA 接收串口数据的案例,详细讲解如何通过 MPU 配置将 DMA 缓冲区设置为非缓存(或写透)区域,并给出完整的 CubeMX 配置步骤与代码示例,帮助开发者彻底解决此类问题。
# 一、问题现象与背景
某产品基于 STM32F407,使用 DMA 接收串口数据,并启用 D-Cache 提高 CPU 性能。调试时发现:接收数据偶尔出现乱码或丢失,且仅在优化等级较高时出现,逻辑分析仪显示数据正确,但 CPU 读取的缓冲区内容错误。
## 根因分析
- STM32F4 的 Cortex-M4 内核具有 4KB D-Cache,默认在复位后关闭,但若通过 `SCB_EnableDCache()` 启用,CPU 访问 SRAM 时会先经过 Cache。
- DMA 控制器(如 DMA2)不经过 Cache,直接访问物理 SRAM。
- 当 CPU 读取 DMA 刚写入的数据时,若 Cache 中已有旧数据(脏行),CPU 会读到过期数据;反之,若 CPU 先写入数据再由 DMA 读取(如 DMA 发送),Cache 中的新数据可能未回写,DMA 读到旧数据。
# 二、解决方案:MPU 配置非缓存区域
MPU(内存保护单元)可以设置内存区域的属性,包括 Cache 策略。将 DMA 缓冲区所在的 SRAM 区域配置为“非缓存(Non-cacheable)”或“写透(Write-through)”,即可保证 CPU 与 DMA 访问的一致性。
## 配置步骤(基于 CubeMX + HAL 库)
1. 在 CubeMX 中启用 MPU:`System Core > MPU`,勾选 Enable。
2. 添加一个 Region,设置:
- Region Base Address:例如 `0x20000000`(SRAM1 起始地址)
- Region Size:根据需要,如 16KB(覆盖缓冲区)
- TEX=1, C=0, B=0(非缓存)或 TEX=0, C=1, B=1(写透,但推荐非缓存)
- Enable Region 勾选。
3. 生成代码后,在 `main()` 中调用 `MPU_Config()`(CubeMX 自动生成),并确保在 `SCB_EnableDCache()` 之前调用。
## 代码示例
```c
// 自定义 DMA 缓冲区(必须位于 MPU 配置的区域内)
#define BUF_SIZE 256
__attribute__((section(".ARM.__at_0x20000000"))) uint8_t dma_rx_buf[BUF_SIZE];
// 在 main 函数中初始化顺序:
void main(void) {
HAL_Init();
SystemClock_Config();
// 1. 配置 MPU(必须在使能 D-Cache 之前)
MPU_Config();
// 2. 使能 D-Cache
SCB_EnableDCache();
// 3. 初始化外设和 DMA
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart2, dma_rx_buf, BUF_SIZE);
while (1) {
// 主循环中处理数据
}
}
// CubeMX 生成的 MPU 配置函数(可手动调整)
void MPU_Config(void) {
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置 SRAM1 区域为非缓存(地址 0x20000000,大小 16KB)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_16KB;
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
# 三、注意事项
- **MPU 配置必须在使能 D-Cache 之前**,否则无效。
- 缓冲区地址必须落在 MPU 配置的区域内,且大小匹配。若使用多个缓冲区,可配置多个 Region 或扩大区域。
- 非缓存区域会降低访问速度,但仅对 DMA 缓冲区影响较小;若追求性能,可使用“写透”策略(TEX=0, C=1, B=1),但需注意写透仍可能产生读不命中,一致性不如非缓存。
- 若使用 `__attribute__((section))` 指定地址,需确保链接脚本中该段未被其他变量占用。
- 对于 DMA 发送(CPU 写,DMA 读),即使配置了非缓存,也建议在启动 DMA 前调用 `SCB_CleanDCache()` 确保数据回写(非缓存区域无需,但写透区域需要)。
- 调试时可用 `SCB_InvalidateDCache_by_Addr()` 强制失效缓存行,但根治方案仍是 MPU 配置。
# 四、总结
通过 MPU 将 DMA 缓冲区配置为非缓存区域,从根本上避免了 D-Cache 与 DMA 的数据一致性问题。本案例中,配置后串口接收数据稳定,不再出现乱码。对于使用 STM32F4 且启用 D-Cache 的开发者,建议在项目初期就规划好内存区域属性,避免后期排查的困扰。