Cortex-M7 上 MPU 配置不当引发的 DMA 与缓存一致性故障:一次深度排查实战
👁 1 阅读 · 2026-08-27 · 嵌入式
在基于 Cortex-M7 的高性能嵌入式系统中,MPU(内存保护单元)与 Cache 的协同配置是保障系统稳定性的关键。本文通过一个真实案例,详细剖析了因 MPU 区域属性配置不当,导致 DMA 与 CPU 缓存数据不一致的故障现象、排查思路及最终解决方案。我们将深入探讨 MPU 的 Cache 策略、DMA 的工作机制,并给出可复用的配置代码与调试技巧,帮助开发者规避同类陷阱。
# 基于 Cortex-M7 的 MPU 配置不当导致 DMA 与 CPU 缓存一致性故障的排查流程
## 引言
在嵌入式开发中,Cortex-M7 凭借其强大的处理能力和可选的 L1 Cache,成为高性能应用的理想选择。然而,Cache 的引入也带来了新的挑战——缓存一致性(Cache Coherency)问题。当 DMA 外设直接访问内存,而 CPU 通过 Cache 读写同一区域时,若缺乏正确的内存属性配置,就会导致数据错乱。本文以 STM32H743 平台为例,记录一次因 MPU 配置不当引发的 DMA 传输数据损坏的完整排查过程。
## 故障现象
某项目中,使用 SPI DMA 接收传感器数据,数据长度 256 字节,存入全局数组 `rx_buffer`。系统运行后,发现接收到的数据前 64 字节正确,后续字节随机出错,且错误模式不固定。初步怀疑是 SPI 配置问题,但反复检查时序和 DMA 设置均无异常。
## 原理分析:MPU、Cache 与 DMA 的三角关系
### 1. Cortex-M7 的 Cache 与内存属性
Cortex-M7 内核具有独立的 I-Cache 和 D-Cache。CPU 访问内存时,会优先查找 Cache。而 DMA 是独立于 CPU 的外设,直接访问物理内存(SRAM),不经过 Cache。当 CPU 和 DMA 同时操作同一内存区域时,就会产生一致性问题。
### 2. MPU 的作用
MPU 允许我们将内存区域划分为不同的区域,并设置其访问权限和属性。其中最关键的是 **Cache 策略**,主要有三种:
- **Write-Through(写通)**:CPU 写数据时,同时更新 Cache 和主存,读操作仍使用 Cache。
- **Write-Back(写回)**:CPU 写数据时,仅更新 Cache,标记为脏,延迟写回主存。
- **Non-cacheable(不可缓存)**:CPU 直接访问主存,不使用 Cache。
对于 DMA 与 CPU 共享的内存区域,推荐使用 **Non-cacheable** 或 **Write-Through** 策略,避免因 Write-Back 导致的数据滞后。
### 3. 故障根因
在本案例中,`rx_buffer` 位于默认的 SRAM 区域,而默认情况下,Cortex-M7 的 D-Cache 对该区域采用 **Write-Back** 策略。当 DMA 写入数据到 `rx_buffer` 时,CPU 的 Cache 中可能仍保留着旧数据。当 CPU 读取时,会优先命中 Cache,从而读到过时数据。此外,DMA 写入的数据可能被 CPU 的 Cache 写回操作覆盖,导致数据损坏。
## 排查流程
### 步骤 1:确认 Cache 状态
首先,检查 D-Cache 是否启用。在 STM32H7 中,默认启动文件会启用 D-Cache。通过调试器查看 SCB->CCR 寄存器,确认 D-Cache 已使能。
```c
if (SCB->CCR & SCB_CCR_DC_Msk) {
// D-Cache 已启用
}
```
### 步骤 2:检查 MPU 配置
查看 MPU 寄存器,发现默认配置中,SRAM 区域被设置为 Write-Back 策略。这证实了我们的怀疑。
### 步骤 3:临时禁用 D-Cache 验证
为了快速验证,我们可以临时禁用 D-Cache,观察故障是否消失。
```c
SCB_DisableDCache();
// 测试 DMA 接收
```
测试发现数据完全正确,这进一步确认了问题与 Cache 相关。
### 步骤 4:正确配置 MPU
我们需要为 DMA 缓冲区所在的内存区域配置 MPU,将其设置为 **Non-cacheable** 或 **Write-Through**。这里我们选择 Non-cacheable,因为它能彻底避免一致性问题,且性能损失在可接受范围内(对于缓冲区而言)。
## 解决方案:MPU 配置代码
以下代码展示了如何为 `rx_buffer` 所在的 SRAM 区域配置 MPU,使其成为 Non-cacheable。
```c
#include "cmsis_armcc.h"
void MPU_Config(void)
{
// 确保 MPU 已禁用
MPU_Disable();
// 配置区域 0:整个 SRAM1(地址 0x30000000,大小 128KB)
// 实际使用时,可根据缓冲区地址和大小调整
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
MPU_InitStruct.SubRegionDisable = 0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; // TEX=0, C=0, B=0 -> Non-cacheable
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE; // 可共享,便于 DMA 访问
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
注意:`TypeExtField` 和 `IsCacheable`、`IsBufferable` 的组合决定了 Cache 策略。对于 Non-cacheable,TEX=0, C=0, B=0。
## 完整示例:DMA 接收缓冲区配置
以下是一个完整的初始化序列,包括 MPU 配置、DMA 配置和接收逻辑。
```c
// 全局缓冲区,需 32 字节对齐(Cache line 大小)
__attribute__((aligned(32))) uint8_t rx_buffer[256];
void SystemInit_MPU(void)
{
// 禁用 MPU
MPU_Disable();
// 配置区域 0:rx_buffer 所在区域(假设在 SRAM1)
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)rx_buffer;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小调整
MPU_InitStruct.SubRegionDisable = 0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
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);
// 使能 MPU,默认特权模式访问
MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
void SPI_DMA_Init(void)
{
// 标准 SPI 和 DMA 初始化,此处省略
// 确保 DMA 目标地址为 rx_buffer
}
int main(void)
{
HAL_Init();
SystemClock_Config();
// 先配置 MPU,再使能 D-Cache
SystemInit_MPU();
SCB_EnableDCache();
SPI_DMA_Init();
// 启动 DMA 接收
HAL_SPI_Receive_DMA(&hspi, rx_buffer, 256);
while (1) {
// 处理数据
}
}
```
## 注意事项
- **MPU 区域大小**:MPU 区域大小必须是 2 的幂次,且最小为 32 字节。如果缓冲区大小不是 2 的幂次,可以扩大区域覆盖,但需确保不与其他关键数据冲突。
- **对齐要求**:缓冲区地址必须与区域大小对齐(例如,区域大小为 256B,地址需 256 字节对齐)。使用 `__attribute__((aligned(32)))` 至少保证 Cache line 对齐,但 MPU 区域对齐要求更高。
- **共享属性**:对于 DMA 访问的内存,建议设置为 Shareable,以确保总线一致性(在单核中影响不大,但多核或总线矩阵中重要)。
- **性能权衡**:Non-cacheable 区域会降低 CPU 访问速度,因此仅对 DMA 缓冲区使用,其他数据仍保持 Cacheable。
- **调试技巧**:利用调试器观察 Cache 和内存内容,或使用 `SCB_CleanDCache()` 和 `SCB_InvalidateDCache()` 手动维护一致性,但这不是长久之计。
## 总结
本次故障的根源是 MPU 默认配置将 SRAM 设置为 Write-Back 策略,导致 DMA 与 CPU 缓存不一致。通过正确配置 MPU,将 DMA 缓冲区设为 Non-cacheable,问题得以解决。在实际项目中,务必为 DMA 相关的内存区域规划合适的 Cache 策略,这是 Cortex-M7 开发中不可忽视的一环。希望本文的排查流程和代码示例能帮助你快速定位类似问题。