STM32F4 上利用 D-Cache 与 MPU 配置解决 DMA 与 CPU 数据一致性的实战方案
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407)中,当 DMA 与 CPU 同时访问外部 SRAM 或 SDRAM 时,D-Cache 可能导致数据不一致,引发随机性故障。本文深入剖析 Cache 一致性问题根源,并给出基于 MPU 配置的两种实战方案:关闭 Cache 或配置 Cache 策略为 Write-Through,同时提供完整代码示例与注意事项,帮助开发者彻底解决 DMA 传输中的隐性 Bug。
# 一、问题背景:D-Cache 与 DMA 的冲突
STM32F4 内核(Cortex-M4)内置了 4KB 的 D-Cache 和 I-Cache,用于加速 CPU 对内存的访问。然而,当 DMA 控制器直接访问内存(如外部 SRAM、SDRAM)时,CPU 可能先将数据缓存在 D-Cache 中,而 DMA 直接读写物理内存,导致两者看到的数据不一致。
典型场景:
- 使用 DMA 从 ADC 采集数据到外部 SRAM,CPU 读取该缓冲区时可能读到旧数据(DMA 已更新内存,但 Cache 未失效)。
- CPU 先写入缓冲区,然后启动 DMA 发送,但 DMA 可能读到 Cache 中尚未写回内存的旧数据。
这种问题表现为随机性、偶发性错误,极难排查。
# 二、原理剖析:Cache 一致性模型
Cortex-M4 的 D-Cache 采用 **Write-Back** 策略(默认):CPU 写操作只更新 Cache,不立即写回内存,直到 Cache 行被替换或显式 Clean。而 DMA 不经过 Cache,直接访问物理内存。
因此,一致性维护需要两种操作:
- **Clean**:将 Cache 中脏数据写回内存。
- **Invalidate**:使 Cache 行失效,下次访问从内存重新加载。
STM32F4 提供了 SCB 相关函数(如 `SCB_CleanDCache()`、`SCB_InvalidateDCache()`),但手动管理繁琐且易出错。更优雅的方案是使用 **MPU** 配置内存区域的 Cache 策略。
# 三、MPU 配置方案
MPU(Memory Protection Unit)不仅可以设置访问权限,还能定义内存区域的 Cache 属性。对于 DMA 共享缓冲区,有两种推荐配置:
## 方案 A:关闭该区域的 Cache(Strongly-Ordered 或 Device)
将缓冲区所在区域配置为 **Normal memory, Non-cacheable**,即完全绕过 D-Cache。优点是简单可靠,缺点是性能下降(CPU 每次访问都直接读内存)。
## 方案 B:配置为 Write-Through 模式
Write-Through 策略下,CPU 写操作同时更新 Cache 和内存,读操作仍可缓存。这样 DMA 读内存时总能得到最新数据,而 CPU 读 DMA 写入的数据时,只需在 DMA 完成后执行一次 Invalidate(或依赖硬件自动失效,但 STM32F4 不支持硬件一致性,仍需软件处理)。
实际工程中,**方案 B 更常用**,兼顾性能与一致性。
# 四、实战配置步骤
以 STM32F407 为例,假设使用外部 SRAM(地址 0x68000000,大小 1MB)作为 DMA 缓冲区。
## 1. 使能 D-Cache 和 MPU
在 `main()` 函数初始化阶段:
```c
#include "stm32f4xx.h"
void MPU_Config(void)
{
// 确保 MPU 未使能
MPU->CTRL = 0;
// 配置区域 0:外部 SRAM 区域,Write-Through
MPU->RBAR = 0x68000000 | MPU_REGION_SIZE_1MB | (0 << 0); // Region 0
MPU->RASR = (0x0 << 0) | // 无访问权限限制
(0x1 << 1) | // 允许执行
(0x0 << 3) | // 非共享
(0x1 << 4) | // Write-Through (TEX=0, C=1, B=0)
(0x0 << 5) | // 非缓存
(0x0 << 6) | // 非缓冲
(0x0 << 8) | // 无子区域禁用
(0x0 << 16) | // 无指令访问
(0x1 << 24); // 使能 Region
// 使能 MPU,使用默认内存映射作为后备
MPU->CTRL = (0x1 << 0) | (0x1 << 2); // Enable MPU, Enable default region
// 使能 D-Cache(如果尚未使能)
SCB_EnableDCache();
}
```
注意:`MPU_RASR` 的 TEX、C、B 位组合决定了 Cache 策略。对于 Write-Through,需要设置 TEX=0, C=1, B=0(即 Normal memory, Write-Through)。
## 2. 配置 DMA 缓冲区
将 DMA 缓冲区定义在外部 SRAM 区域,并确保编译器将其放置到正确地址:
```c
#define BUF_SIZE 1024
uint8_t dma_buf[BUF_SIZE] __attribute__((section(".ext_sram")));
```
在链接脚本(.ld)中添加:
```c
.ext_sram 0x68000000 :
{
*(.ext_sram)
} > EXTSRAM
```
## 3. DMA 传输中的一致性处理
即使配置了 Write-Through,在 DMA 写入完成后,CPU 读取前仍需执行 Invalidate,以确保 Cache 中可能存在的旧数据被清除:
```c
// DMA 接收完成回调
void DMA_RxComplete(DMA_HandleTypeDef *hdma)
{
// 使缓冲区对应的 Cache 行失效
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 现在 CPU 可以安全读取 dma_buf
}
// CPU 写入后启动 DMA 发送
void DMA_SendData(void)
{
// 确保 CPU 写操作已通过 Write-Through 更新到内存(实际上已自动完成)
// 无需 Clean,但为了保险可执行 Clean
SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart, dma_buf, BUF_SIZE);
}
```
# 五、完整代码示例
以下是一个完整的初始化与使用示例(基于 HAL 库):
```c
// main.c
#include "stm32f4xx_hal.h"
void MPU_Config(void);
void DMA_Init(void);
uint8_t dma_buf[1024] __attribute__((section(".ext_sram")));
int main(void)
{
HAL_Init();
SystemClock_Config();
// 配置 MPU 和 D-Cache
MPU_Config();
// 初始化 DMA(略)
DMA_Init();
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart, dma_buf, sizeof(dma_buf));
while (1)
{
// 主循环
}
}
void MPU_Config(void)
{
// 禁用 MPU
MPU->CTRL = 0;
// 配置 Region 0:外部 SRAM 0x68000000,1MB,Write-Through
MPU->RBAR = 0x68000000 | (0 << 0); // 基地址 + Region 0
MPU->RASR = (0x0 << 0) | // 权限:全部可访问
(0x1 << 1) | // 可执行
(0x0 << 3) | // 非共享
(0x1 << 4) | // TEX=0, C=1, B=0 => Write-Through
(0x0 << 5) |
(0x0 << 6) |
(0x0 << 8) | // 子区域全部使能
(0x0 << 16) | // 无指令缓存
(0x1 << 24); // Region 使能
// 使能 MPU,并启用默认区域
MPU->CTRL = (0x1 << 0) | (0x1 << 2);
// 使能 D-Cache(如果之前未使能)
SCB_EnableDCache();
}
void DMA_Init(void)
{
// 初始化 DMA 流、通道等(略)
// 注意:DMA 的缓冲区地址必须指向外部 SRAM
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 使缓冲区失效,确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf));
// 处理数据...
}
}
```
# 六、注意事项
- **地址对齐**:`SCB_InvalidateDCache_by_Addr` 和 `SCB_CleanDCache_by_Addr` 要求地址按 32 字节对齐(Cache 行大小)。如果缓冲区未对齐,需手动调整或使用整 Cache 操作(如 `SCB_InvalidateDCache()`),但效率较低。
- **MPU 区域重叠**:确保 MPU 区域不与其他区域重叠,否则行为未定义。建议使用默认内存映射作为后备。
- **性能权衡**:Write-Through 模式会降低写性能(每次写都到内存),但对于 DMA 缓冲区这种低频访问场景,影响可忽略。
- **DMA 方向**:
- 外设→内存:DMA 写入后,CPU 读取前必须 Invalidate。
- 内存→外设:CPU 写入后,DMA 读取前建议 Clean(Write-Through 下可省略,但保险起见执行)。
- **多缓冲区**:如果使用双缓冲,需分别配置 MPU 区域或确保同一区域覆盖所有缓冲区。
- **调试技巧**:若问题依旧,可暂时关闭 D-Cache 验证是否由 Cache 引起,再逐步优化。
# 七、总结
通过 MPU 将 DMA 共享缓冲区配置为 Write-Through 模式,并配合必要的 Cache 维护操作,可以彻底解决 STM32F4 上 DMA 与 CPU 的数据一致性问题。该方案在保证性能的同时,提供了可靠的同步机制。实际项目中,务必根据内存区域特性灵活配置,并遵循上述注意事项,以避免踩坑。