STM32F4 D-Cache 与 DMA 一致性维护:三种实用策略深度解析
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列高性能 MCU 上,D-Cache 能显著提升 CPU 访问速度,但引入 DMA 后,缓存与内存数据不一致的问题常导致数据损坏或功能异常。本文面向有经验的嵌入式开发者,深入剖析 D-Cache 与 DMA 交互的原理,并给出三种实用的一致性维护策略:Cache 清理/失效、MPU 配置非缓存区域、以及 DMA 描述符与缓冲区分区管理。每种策略均附有配置步骤、完整代码示例及注意事项,助你构建稳定可靠的 DMA 驱动。
# 引言
STM32F4 系列(如 STM32F407、F429)内置了 D-Cache(数据缓存),用于加速 CPU 对片外存储器(如 SDRAM)的访问。然而,当 DMA 控制器直接访问内存时,它不经过 Cache,这会导致 CPU 与 DMA 看到的数据不一致。例如,CPU 写入数据到内存(可能暂存于 Cache),DMA 读取时可能拿到旧数据;反之,DMA 写入新数据,CPU 读取时可能命中 Cache 中的旧副本。这种不一致性在以太网、USB、SDIO 等高速外设中尤为致命。
本文针对 STM32F4 的 D-Cache(注意:F4 系列只有部分型号带 D-Cache,如 F429、F469,而 F407 没有,但原理通用),提供三种实用策略来维护一致性。
# 原理剖析
## D-Cache 的工作机制
D-Cache 是 CPU 与主存之间的高速缓存,以缓存行(通常 32 字节)为单位。当 CPU 读数据时,若命中 Cache 则直接返回;写数据时,采用写回(Write-back)策略,即数据先写入 Cache,标记为脏,待时机成熟再写回主存。
## DMA 的访问路径
DMA 控制器直接连接总线矩阵,绕过 CPU 和 Cache,直接读写主存。因此,DMA 看到的是主存的真实数据,而 CPU 可能看到的是 Cache 中的副本。
## 不一致性场景
- **CPU 写,DMA 读**:CPU 写入数据到缓冲区,数据可能留在 Cache 中,尚未写回主存。DMA 读取主存时,得到的是旧数据。
- **DMA 写,CPU 读**:DMA 将外设数据写入主存,但 CPU 的 Cache 中可能已有该地址的旧副本,CPU 读取时命中 Cache,得到旧数据。
# 三种实用策略
## 策略一:Cache 清理与失效(Clean & Invalidate)
这是最直接的方法,在 DMA 操作前后手动维护 Cache。
### 原理
- **清理(Clean)**:将 Cache 中的脏数据写回主存,确保主存数据最新。
- **失效(Invalidate)**:使 Cache 中的行失效,下次 CPU 访问时重新从主存加载。
### 配置步骤
1. 启用 D-Cache(若未启用)。
2. 在 DMA 发送前,调用 `SCB_CleanDCache()` 或 `SCB_CleanDCache_by_Addr()` 清理缓冲区。
3. 在 DMA 接收后,调用 `SCB_InvalidateDCache_by_Addr()` 使缓冲区失效。
### 代码示例
```c
// 发送缓冲区,CPU 写入数据后,清理 Cache 再启动 DMA
uint8_t tx_buf[128];
// 填充数据...
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart, tx_buf, sizeof(tx_buf));
// 接收缓冲区,DMA 完成后,使 Cache 失效再读取
uint8_t rx_buf[128];
HAL_UART_Receive_DMA(&huart, rx_buf, sizeof(rx_buf));
// 等待 DMA 完成回调...
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 现在可以安全读取 rx_buf
```
### 注意事项
- 地址和长度需按 32 字节对齐,否则可能影响其他数据。
- 频繁清理/失效会降低性能,适合数据量小或低频场景。
- 使用 `HAL` 库时,部分驱动已内置处理,需确认。
## 策略二:MPU 配置非缓存区域
通过内存保护单元(MPU)将 DMA 缓冲区所在的区域配置为不缓存(或写-through),从而避免不一致性。
### 原理
MPU 可以设置内存区域的属性,如 `Normal, Non-cacheable`。这样 CPU 访问该区域时直接读写主存,绕过 Cache,保证一致性。
### 配置步骤
1. 定义缓冲区区域,确保其地址和大小满足 MPU 对齐要求(通常 32 字节)。
2. 初始化 MPU,配置区域属性为 `Device` 或 `Normal, Non-cacheable`。
3. 启用 MPU。
### 代码示例
```c
// 定义缓冲区,放在独立区域
__attribute__((section(".dma_buf"))) uint8_t dma_buf[256];
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置区域 0:dma_buf 所在区域,非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)dma_buf;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小调整
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.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 区域大小必须是 2 的幂,且地址对齐。
- 非缓存区域访问速度较慢,但 DMA 一致性得到保证。
- 需在系统初始化时调用 `MPU_Config()`,并确保链接脚本将 `dma_buf` 放入正确段。
## 策略三:DMA 描述符与缓冲区分区管理
将 DMA 描述符(如 DMA 控制结构)和实际数据缓冲区放置在不同的内存区域,分别管理缓存属性。
### 原理
描述符通常由 CPU 频繁读写,而数据缓冲区由 DMA 和 CPU 交互。通过将描述符放在可缓存区域(性能高),数据缓冲区放在非缓存区域(一致性),或反之,根据实际需求优化。
### 配置步骤
1. 分配两个内存区域:一个用于描述符(如 `__attribute__((section(".desc")))`),一个用于数据缓冲区(如 `__attribute__((section(".buf")))`)。
2. 在链接脚本中定义这些段,并设置 MPU 属性(如描述符可缓存,缓冲区非缓存)。
3. 初始化 DMA 时,描述符和缓冲区分别使用对应地址。
### 代码示例
```c
// 链接脚本示例(.ld)
// .desc : { *(.desc) } >RAM
// .buf : { *(.buf) } >RAM
// 定义段
__attribute__((section(".desc"))) DMA_HandleTypeDef hdma_desc;
__attribute__((section(".buf"))) uint8_t data_buf[1024];
// MPU 配置:desc 区域可缓存,buf 区域非缓存
void MPU_Config_Split(void)
{
// 配置区域 0:desc 区域,可缓存
// 配置区域 1:buf 区域,非缓存
// 具体代码类似策略二,但需设置两个区域
}
// DMA 初始化时,使用 data_buf 作为缓冲区,hdma_desc 作为描述符
```
### 注意事项
- 需要精心设计链接脚本,确保段地址符合 MPU 对齐要求。
- 描述符和缓冲区的大小需合理规划,避免浪费内存。
- 此策略适用于复杂系统,如多通道 DMA 或网络协议栈。
# 总结
在 STM32F4 系列使用 D-Cache 时,维护 DMA 一致性是确保系统稳定的关键。三种策略各有优劣:
- **策略一** 简单直接,但性能损失较大,适合低频小数据。
- **策略二** 配置一次,长期有效,但牺牲了缓存性能,适合缓冲区固定且频繁 DMA 的场景。
- **策略三** 最灵活,可针对不同数据特性优化,但实现复杂,适合大型项目。
开发者应根据实际需求选择合适策略,并在设计初期就考虑内存布局和缓存配置。希望本文能帮助你避开 D-Cache 的坑,写出更健壮的嵌入式代码。