STM32F4 系列 D-Cache 与 MPU 配置不当导致 DMA 数据不一致的排查与解决
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 F429、F407)中,当启用 D-Cache 后,DMA 传输的数据可能因缓存一致性问题而出现错误,尤其在以太网、USB 或 ADC 等外设中。本文深入分析 D-Cache 与 MPU 的交互原理,通过实际案例展示配置不当的典型症状,并提供完整的 MPU 配置步骤、代码示例及排查方法,帮助开发者快速定位并修复此类问题。
# 引言
在 STM32F4 系列高性能 MCU 中,D-Cache(数据缓存)能显著提升 CPU 访问内存的效率,但同时也引入了缓存一致性问题。当 DMA 外设直接访问内存时,若未正确配置 MPU(内存保护单元)或未执行缓存维护操作,CPU 与 DMA 看到的数据可能不一致,导致数据损坏或功能异常。本文以实际项目为例,讲解问题原理、排查步骤及解决方案。
# 问题现象与原理
## 典型症状
- 以太网接收数据包偶尔出现 CRC 错误或内容错乱。
- USB 批量传输数据丢失或重复。
- ADC 采集的数据在 DMA 搬运后,CPU 读取的值与预期不符。
## 根本原因
STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写入数据时先更新缓存,仅当缓存行被替换或显式清理时才写回内存。DMA 外设直接访问物理内存,不经过缓存。若 DMA 读取的内存区域仍驻留在缓存中且未写回,则 DMA 读到的是旧数据;若 DMA 写入内存,而 CPU 缓存中已有该区域的副本,则 CPU 可能读到过期的缓存数据。
# 排查方法
## 1. 确认是否启用 D-Cache
检查启动代码或初始化中是否调用了 `SCB_EnableDCache()`。若未启用,则问题可能源于其他原因。
## 2. 定位 DMA 缓冲区地址
查看 DMA 描述符或配置中的缓冲区地址,确认其是否位于可缓存区域(如 SRAM1/SRAM2)。
## 3. 检查 MPU 配置
若未配置 MPU,默认情况下整个 SRAM 区域是可缓存的。此时,任何 DMA 与 CPU 共享的数据都可能引发一致性问题。
## 4. 临时禁用 D-Cache 验证
在初始化时注释掉 `SCB_EnableDCache()`,观察问题是否消失。若消失,则基本确定是缓存一致性问题。
# 解决方案:MPU 配置 + 缓存维护
## 方案一:配置 MPU 将 DMA 缓冲区设为非缓存
将 DMA 使用的内存区域配置为“非缓存、非缓冲”属性,这样 CPU 访问该区域时直接操作内存,避免缓存干扰。
### 配置步骤
1. 确定 DMA 缓冲区地址和大小(需对齐到 32 字节)。
2. 使用 `MPU_Region_InitTypeDef` 配置 MPU 区域。
3. 使能 MPU 并设置优先级。
### 代码示例
```c
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct;
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域(假设地址 0x20001000,大小 4KB)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x20001000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
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_NOT_SHAREABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:非缓存
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
调用 `MPU_Config()` 在系统初始化时执行。
## 方案二:使用缓存维护函数
若不想配置 MPU,也可在 DMA 操作前后手动执行缓存清理或无效化操作。
- 当 CPU 写入数据并准备 DMA 读取时,调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 将缓存写回内存。
- 当 DMA 写入数据并准备 CPU 读取时,调用 `SCB_InvalidateDCache()` 或 `SCB_InvalidateDCache_by_Addr()` 使缓存行失效。
### 代码示例
```c
// 假设缓冲区地址为 buf,长度为 len
// CPU 写入数据后,清理缓存
SCB_CleanDCache_by_Addr((uint32_t*)buf, (int32_t)len);
// 启动 DMA 读取...
// DMA 传输完成后,无效化缓存
SCB_InvalidateDCache_by_Addr((uint32_t*)buf, (int32_t)len);
```
注意:地址需 32 字节对齐,长度需为 32 的倍数,否则需手动调整。
# 完整示例:以太网 DMA 接收
以下展示在 STM32F429 上使用 DMA 接收以太网数据帧时的正确配置。
```c
// 定义 DMA 缓冲区(建议使用 __attribute__((aligned(32))))
__attribute__((aligned(32))) uint8_t rx_buffer[1522];
void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
// 无效化缓存,确保 CPU 读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
// 处理数据...
}
// 在初始化中配置 MPU(可选,但推荐)
void MPU_Config(void)
{
// 如上所示,将 rx_buffer 所在区域设为非缓存
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config(); // 先配置 MPU
SCB_EnableDCache(); // 再使能 D-Cache
// 初始化以太网等...
}
```
# 注意事项
- **对齐与大小**:MPU 区域大小必须是 2 的幂次,且基地址需对齐到区域大小。若缓冲区不满足,可定义更大的区域或使用缓存维护函数。
- **性能权衡**:将 DMA 缓冲区设为非缓存会降低 CPU 访问该区域的性能,但通常影响不大。
- **多区域配置**:若多个 DMA 缓冲区分散在不同地址,可配置多个 MPU 区域,但注意 MPU 区域数量有限(STM32F4 通常 8 个)。
- **缓存维护开销**:频繁调用缓存维护函数可能影响实时性,建议优先使用 MPU 配置。
- **调试技巧**:使用调试器查看内存内容,对比 DMA 写入前后的数据,可快速确认一致性错误。
# 总结
D-Cache 与 DMA 的交互是 STM32F4 开发中的常见陷阱。通过合理配置 MPU 将 DMA 缓冲区设为非缓存,或在使用时执行缓存清理/无效化操作,可有效避免数据不一致问题。建议在项目初期就规划好内存区域属性,以减少后期调试成本。