STM32H7 L1 Cache 未使能导致 DMA 数据不一致:从踩坑到彻底修复
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 系列高性能 MCU 上,L1 Cache 默认未使能,但一旦使能而 DMA 未配合,极易出现数据不一致的诡异 Bug。本文从一个实际网络采集项目出发,详细剖析了 Cache 与 DMA 的协同机制,给出了三种修复方案(关闭 Cache、配置 MPU 为非缓存区、软件维护 Cache),并附带了完整的 HAL 库配置代码和调试技巧,帮助开发者彻底规避此类嵌入式经典陷阱。
# STM32H7 L1 Cache 未使能导致 DMA 数据不一致:从踩坑到彻底修复
## 一、问题现象:数据像“幽灵”一样丢失
最近在调试一个基于 STM32H743 的数据采集系统,使用 SPI DMA 接收传感器数据。代码逻辑很简单:DMA 将数据搬运到内存缓冲区,主循环解析。但发现一个诡异现象:**DMA 中断标志已置位,缓冲区却偶尔出现旧数据或全零**。更奇怪的是,如果关闭优化或加延时,问题就消失。
排查了 DMA 配置、SPI 时序、中断优先级,均无异常。最后怀疑到 L1 Cache 头上——因为 STM32H7 的 Cortex-M7 内核带有 L1 Cache,而默认复位后 Cache 是关闭的,但我在初始化代码中使能了它(为了提升性能)。
## 二、原理剖析:Cache 与 DMA 的“信息孤岛”
### 2.1 什么是 L1 Cache?
Cortex-M7 内置了 I-Cache(指令缓存)和 D-Cache(数据缓存),用于加速 CPU 对内存的访问。当 CPU 读取数据时,会先查 Cache,命中则直接返回,未命中则从 RAM 加载到 Cache。写操作则可能采用 **write-back** 策略:数据先写入 Cache,标记为脏,延迟写回 RAM。
### 2.2 DMA 的“旁路”特性
DMA 控制器直接访问 RAM,**不经过 Cache**。这意味着:
- **DMA 写入 RAM**:CPU 的 Cache 中可能保留着旧数据,CPU 读到的还是旧值(Cache 未失效)。
- **DMA 读取 RAM**:如果 CPU 刚写过数据但仍在 Cache 中(write-back),DMA 读到的可能是旧值(Cache 未写回)。
这就是数据不一致的根源。
### 2.3 为什么默认关闭?
STM32H7 复位后 L1 Cache 默认关闭,这保证了与旧代码的兼容性。但很多开发者为了性能主动使能,却忽略了与 DMA 的协同,导致上述问题。
## 三、修复方案:三种策略对比
### 方案一:直接关闭 D-Cache(简单粗暴)
适用于对性能要求不高的场景。
```c
// 关闭 D-Cache(在 SystemInit 后调用)
SCB_DisableDCache();
```
### 方案二:配置 MPU 将 DMA 缓冲区设置为非缓存区域(推荐)
使用 MPU 将 DMA 使用的内存区域配置为 **normal memory, non-cacheable**,这样 CPU 访问该区域时直接读写 RAM,与 DMA 保持一致。
```c
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 DMA 缓冲区区域(例如 0x30000000,大小 64KB)
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_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_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
注意:MPU 配置必须在使能 Cache 之前完成,且要确保缓冲区地址对齐到区域大小。
### 方案三:软件维护 Cache(灵活但需谨慎)
在 DMA 操作前后手动维护 Cache。
- **DMA 写入 RAM 后**,要使 CPU 的 Cache 失效,强制从 RAM 重新读取:
```c
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer, size);
```
- **DMA 读取 RAM 前**,要将 CPU 的 Cache 写回,确保 DMA 看到最新数据:
```c
SCB_CleanDCache_by_Addr((uint32_t*)buffer, size);
```
注意:地址必须 32 字节对齐,大小也需对齐。
## 四、完整代码示例(基于 HAL 库)
以下是一个典型的 SPI DMA 接收流程,采用方案二(MPU 非缓存区)进行修复。
```c
// main.c 片段
// 定义 DMA 缓冲区(放在非缓存区域,例如 AXI SRAM 0x30000000)
#if defined(__ICCARM__)
#pragma location=0x30000000
uint8_t dma_rx_buf[1024];
#elif defined(__GNUC__)
uint8_t dma_rx_buf[1024] __attribute__((section(".non_cacheable")));
#endif
void SystemClock_Config(void);
void MPU_Config(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
// 先配置 MPU,再使能 Cache
MPU_Config();
SCB_EnableDCache();
SCB_EnableICache();
// SPI 和 DMA 初始化...
MX_SPI1_Init();
MX_DMA_Init();
// 启动 DMA 接收
HAL_SPI_Receive_DMA(&hspi1, dma_rx_buf, sizeof(dma_rx_buf));
while (1)
{
// 主循环解析数据(此时 CPU 读取 dma_rx_buf 直接访问 RAM,与 DMA 一致)
}
}
// 链接脚本中需定义 .non_cacheable 段,并映射到 0x30000000 区域
```
## 五、调试与验证技巧
1. **使用断点观察**:在 DMA 完成中断中设置断点,查看缓冲区内容。如果与预期不符,优先怀疑 Cache。
2. **对比测试**:临时关闭 Cache,看问题是否消失。
3. **查看 MPU 配置**:使用调试器查看 MPU->RBAR 和 MPU->RASR 寄存器,确认区域属性。
4. **注意内存区域**:STM32H7 的 RAM 分为 DTCM(0x20000000)、AXI SRAM(0x24000000)和 SRAM1-4(0x30000000 等)。DTCM 不支持 DMA,而 AXI SRAM 和 SRAM1-4 支持。建议将 DMA 缓冲区放在 AXI SRAM 或 SRAM1-4,并配置 MPU。
## 六、注意事项与总结
- **MPU 配置必须在使能 Cache 之前**,否则不生效。
- **缓冲区地址对齐**:MPU 区域大小必须与缓冲区大小匹配,且基地址对齐到区域大小(例如 64KB 区域要求地址低 16 位为 0)。
- **软件维护 Cache 时**,地址和长度必须 32 字节对齐,否则会导致硬件错误。
- **不要混合使用方案二和方案三**,否则可能造成二次不一致。
- **性能权衡**:非缓存区域会降低 CPU 访问速度,但 DMA 缓冲区通常访问频率不高,影响可忽略。
总之,STM32H7 的 L1 Cache 是一把双刃剑。在涉及 DMA 的场景中,务必明确 Cache 策略。推荐使用 MPU 将 DMA 缓冲区配置为非缓存区域,既保证一致性,又保留 Cache 带来的性能提升。希望本文能帮你少踩一个经典的嵌入式大坑。