基于 Cortex-M7 的 MPU 配置不当导致 DMA 传输数据被缓存污染的实战分析
👁 2 阅读 · 2026-08-27 · 嵌入式
在基于 Cortex-M7 的高性能嵌入式系统中,MPU(内存保护单元)与 Cache 的协同配置是确保 DMA 数据传输正确性的关键。本文通过一个实际案例,深入剖析 MPU 配置不当导致 DMA 数据被缓存污染的根因,并给出完整的解决方案,帮助开发者规避类似陷阱。
# 基于 Cortex-M7 的 MPU 配置不当导致 DMA 传输数据被缓存污染的实战分析
## 一、问题现象
在某基于 STM32H743(Cortex-M7 内核)的项目中,使用 DMA 从外部 ADC 采集数据到内存缓冲区,同时 CPU 需要实时处理这些数据。调试中发现,DMA 传输完成后,CPU 读取缓冲区数据时,部分数据为旧值(即上一次传输的数据),而直接读取外设寄存器则正常。初步怀疑是 Cache 一致性问题,但检查代码发现已启用了 MPU 并配置了相关区域,为何还会出现?
## 二、Cortex-M7 的 Cache 与 MPU 基础
Cortex-M7 内核内置了 I-Cache 和 D-Cache,其中 D-Cache 用于缓存数据访问。当 CPU 访问内存时,会优先查找 Cache,若命中则直接返回,否则从主存加载。DMA 是独立于 CPU 的外设,它直接访问主存,不经过 Cache。因此,当 DMA 写入内存时,如果该内存区域被 CPU 的 D-Cache 缓存,且 Cache 行尚未失效(Invalidate),CPU 读取时可能得到 Cache 中的旧数据,这就是缓存污染(Cache Pollution)。
MPU 的作用之一是控制内存区域的属性,包括 Cache 策略(如 Write-Back、Write-Through、Non-cacheable)和访问权限。正确配置 MPU 可以避免 Cache 一致性问题,但配置不当反而会引入问题。
## 三、MPU 配置不当的根因分析
### 3.1 典型错误配置
在项目中,开发者将 DMA 缓冲区所在的 SRAM 区域配置为 Write-Back 且 Write-Allocate,代码如下:
```c
// MPU 区域配置示例(错误)
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // SRAM 区域
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_REGION_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_CACHEABLE; // 允许 Cache
MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 对应 Write-Back, Write-Allocate
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
```
### 3.2 问题所在
- 该区域被配置为 Cacheable(可缓存),且策略为 Write-Back。这意味着 CPU 写入数据时,数据可能暂存在 Cache 中,并未立即写入主存。
- 当 DMA 从外设读取数据并写入该缓冲区时,DMA 直接写主存,但 CPU 的 Cache 中可能还保留着旧数据(如果之前 CPU 访问过该区域)。
- 若在 DMA 传输前未执行 Cache Clean(清理)或 Invalidate(失效)操作,CPU 读取时就会命中 Cache 中的旧数据,导致数据污染。
### 3.3 为什么 MPU 配置了却无效?
很多开发者误以为只要配置了 MPU 区域,Cache 就会自动处理一致性。实际上,MPU 只是定义属性,而 Cache 的操作(Clean/Invalidate)需要软件显式调用。此外,如果 MPU 配置的 Cache 策略与 DMA 访问模式不匹配,问题依旧。
## 四、解决方案
### 4.1 方案一:将 DMA 缓冲区配置为 Non-cacheable(推荐)
最简单可靠的方法是将 DMA 缓冲区所在内存区域设置为 Non-cacheable(不可缓存),这样 CPU 和 DMA 都直接访问主存,不存在一致性问题。
```c
// 正确配置:Non-cacheable
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // 对应 Non-cacheable
MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_REGION_BUFFERABLE; // 允许缓冲,但无 Cache
```
注意:Non-cacheable 区域访问速度较慢,但 DMA 缓冲区通常用于数据交换,性能影响可接受。
### 4.2 方案二:使用 Cache 维护指令
如果必须使用 Cacheable 区域(例如缓冲区较大且需要 CPU 频繁访问),则必须在 DMA 传输前后显式维护 Cache。
- **DMA 写入前**:执行 Cache Clean(将 Cache 中脏数据写回主存),防止旧数据覆盖 DMA 写入的新数据。
- **DMA 写入后**:执行 Cache Invalidate(使 Cache 行失效),强制 CPU 从主存重新读取。
```c
// 使用 CMSIS 函数
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size); // DMA 前
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size); // DMA 后
```
注意:地址需 32 字节对齐(Cache 行大小),大小需为行大小的整数倍。
### 4.3 方案三:使用双缓冲 + 非缓存标志
在通信场景中,可以使用双缓冲,并设置一个标志变量(放在 Non-cacheable 区域),当 DMA 完成中断中置位标志,CPU 轮询标志后再处理数据。
## 五、完整代码示例(基于 STM32H743 + HAL 库)
以下示例演示了如何正确配置 MPU 以及使用 Cache 维护函数。
```c
// 初始化 MPU,将 0x30000000 开始的 64KB 配置为 Non-cacheable
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置区域 0:DMA 缓冲区(Non-cacheable)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_REGION_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_REGION_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_REGION_NOT_SHAREABLE;
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);
// 使能 Cache(如果之前未使能)
SCB_EnableDCache();
SCB_EnableICache();
}
// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 数据已在缓冲区,无需额外操作(因为缓冲区 Non-cacheable)
// 直接处理数据
ProcessData((uint32_t*)DMA_BUFFER, BUFFER_SIZE);
}
// 主函数中初始化
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
// ... 初始化 ADC、DMA 等
HAL_ADC_Start_DMA(&hadc, (uint32_t*)DMA_BUFFER, BUFFER_SIZE);
while (1) {}
}
```
## 六、注意事项
- **MPU 区域重叠**:确保配置的区域不与默认的 SRAM 区域冲突,否则默认属性可能覆盖。
- **Cache 维护函数的地址对齐**:使用 `SCB_CleanDCache_by_Addr` 时,地址必须 32 字节对齐,大小需为 32 的倍数,否则会触发断言或无效操作。
- **性能权衡**:Non-cacheable 区域访问较慢,若缓冲区频繁被 CPU 访问,可考虑使用 Cacheable 并配合维护操作,但需注意维护开销。
- **调试技巧**:在调试时,可以临时禁用 D-Cache 来验证是否 Cache 问题,但最终需正确配置。
- **多核或总线矩阵**:在复杂系统中,还需考虑总线主设备(如 DMA2D、MDMA)的缓存一致性,原理相同。
## 七、总结
Cortex-M7 的 Cache 和 MPU 是一把双刃剑,合理配置能大幅提升性能,但配置不当会导致严重的数据一致性问题。本文通过实战案例,展示了 MPU 配置为 Cacheable 但未维护 Cache 时,DMA 数据被污染的现象,并给出了三种解决方案。推荐在 DMA 相关缓冲区使用 Non-cacheable 属性,简单可靠;若追求性能,则必须严格遵循 Clean/Invalidate 操作。希望本文能帮助开发者少走弯路,构建稳定的嵌入式系统。