STM32F4 上利用 D-Cache 与 MPU 配置解决 DMA 与 CPU 数据一致性问题的实战方法
👁 3 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 STM32F407)中,当 CPU 与 DMA 同时访问同一内存区域时,D-Cache 的存在可能导致数据不一致,引发难以排查的随机故障。本文深入剖析 Cache 与 DMA 的冲突根源,并给出基于 MPU 配置的两种实战方案:关闭 Cache 或配置 Cache 策略为 Write-Through。通过完整代码示例和注意事项,帮助开发者彻底解决 DMA 与 CPU 的数据一致性问题,提升系统稳定性。
# 引言
在嵌入式开发中,STM32F4 凭借高性能 Cortex-M4 内核和丰富外设成为主流选择。然而,当启用 D-Cache(数据缓存)后,DMA 与 CPU 之间的数据一致性成为隐患。例如,使用 DMA 接收串口数据时,CPU 可能读到过期的缓存数据,导致数据错乱。本文将从原理到实战,讲解如何利用 MPU(内存保护单元)配置 D-Cache 策略,彻底解决该问题。
# 1. 问题根源:D-Cache 与 DMA 的冲突
## 1.1 D-Cache 的工作原理
D-Cache 是 CPU 内部的高速缓存,用于减少对慢速 RAM 的访问次数。当 CPU 读取内存时,会先检查 Cache;若命中则直接返回,否则从 RAM 加载并缓存。写入时,若采用 Write-Back 策略,数据先写入 Cache,标记为脏,待后续回写到 RAM。
## 1.2 DMA 的旁路特性
DMA 控制器直接访问 RAM,不经过 CPU 的 Cache。因此,当 DMA 向 RAM 写入数据时,Cache 中可能保留着旧数据;当 CPU 读取该地址时,会命中 Cache 的旧数据,而非最新数据。反之,CPU 写入数据后,若尚未回写,DMA 读取 RAM 时也会得到旧数据。
## 1.3 典型场景
- DMA 接收串口/SPI 数据到缓冲区,CPU 处理该缓冲区。
- CPU 填充缓冲区,DMA 发送到外设。
- 双缓冲或多缓冲机制中切换时。
# 2. 解决方案:MPU 配置 D-Cache 策略
## 2.1 MPU 简介
MPU 允许将内存区域划分为多个区域,并设置访问权限和 Cache 属性。通过配置区域属性,可以控制 D-Cache 的行为。
## 2.2 两种策略
- **策略一:关闭 DMA 缓冲区对应的 Cache**(Write-Through 或 Non-cacheable)。
- **策略二:使用 Write-Through 模式**,即 CPU 写入时同时更新 RAM,读取时仍可缓存,但 DMA 写入后,CPU 需主动失效 Cache 行。
推荐使用策略一,简单可靠。
# 3. 实战步骤
## 3.1 硬件环境
- STM32F407 开发板
- 串口 DMA 收发
- 外部 SDRAM(可选,但更易触发问题)
## 3.2 配置 MPU
使用 STM32CubeMX 或直接代码配置。以下为直接代码方式:
```c
#include "stm32f4xx.h"
void MPU_Config(void)
{
// 禁用 MPU
MPU->CTRL = 0;
// 配置区域0:DMA缓冲区(例如 0x20000000 起始,大小 32KB)
MPU->RNR = 0; // 区域编号
MPU->RBAR = 0x20000000; // 基地址
// 设置属性:大小=32KB,TEX=0,C=0,B=0(Non-cacheable),允许读写,可执行
MPU->RASR = (0 << 0) | // 禁用指令访问
(1 << 1) | // 允许特权访问
(1 << 2) | // 允许用户访问
(0 << 3) | // 保留
(0 << 4) | // 保留
(0 << 5) | // 保留
(0 << 6) | // 保留
(0 << 7) | // 保留
(0 << 8) | // 保留
(0 << 9) | // 保留
(0 << 10) | // 保留
(0 << 11) | // 保留
(0 << 12) | // 保留
(0 << 13) | // 保留
(0 << 14) | // 保留
(0 << 15) | // 保留
(0 << 16) | // 保留
(0 << 17) | // 保留
(0 << 18) | // 保留
(0 << 19) | // 保留
(0 << 20) | // 保留
(0 << 21) | // 保留
(0 << 22) | // 保留
(0 << 23) | // 保留
(0 << 24) | // 保留
(0 << 25) | // 保留
(0 << 26) | // 保留
(0 << 27) | // 保留
(0 << 28) | // 保留
(0 << 29) | // 保留
(0 << 30) | // 保留
(0 << 31); // 保留
// 实际上,RASR 的位域需要正确设置,建议使用 CMSIS 提供的宏
// 这里简化,实际使用如下:
MPU->RASR = (0x0 << 0) | // TEX=0
(0x1 << 1) | // C=0,B=0 => Non-cacheable
(0x0 << 3) | // S=0
(0x0 << 4) | // 保留
(0x0 << 5) | // 保留
(0x0 << 6) | // 保留
(0x0 << 7) | // 保留
(0x0 << 8) | // 保留
(0x0 << 9) | // 保留
(0x0 << 10) | // 保留
(0x0 << 11) | // 保留
(0x0 << 12) | // 保留
(0x0 << 13) | // 保留
(0x0 << 14) | // 保留
(0x0 << 15) | // 保留
(0x0 << 16) | // 保留
(0x0 << 17) | // 保留
(0x0 << 18) | // 保留
(0x0 << 19) | // 保留
(0x0 << 20) | // 保留
(0x0 << 21) | // 保留
(0x0 << 22) | // 保留
(0x0 << 23) | // 保留
(0x0 << 24) | // 保留
(0x0 << 25) | // 保留
(0x0 << 26) | // 保留
(0x0 << 27) | // 保留
(0x0 << 28) | // 保留
(0x0 << 29) | // 保留
(0x0 << 30) | // 保留
(0x0 << 31); // 保留
// 但实际需要设置 SIZE 和 ENABLE 位,见下方正确写法
}
```
**正确配置示例**(使用 CMSIS 宏):
```c
void MPU_Config(void)
{
// 禁用 MPU
MPU->CTRL = 0;
// 配置区域0:DMA缓冲区(例如 0x20000000 起始,大小 32KB)
MPU->RNR = 0;
MPU->RBAR = 0x20000000;
// 设置属性:TEX=0,C=0,B=0 => Non-cacheable,允许读写,可执行
MPU->RASR = (0x0 << 0) | // TEX=0
(0x0 << 1) | // C=0
(0x0 << 2) | // B=0
(0x0 << 3) | // S=0
(0x0 << 4) | // 保留
(0x0 << 5) | // 保留
(0x0 << 6) | // 保留
(0x0 << 7) | // 保留
(0x0 << 8) | // 保留
(0x0 << 9) | // 保留
(0x0 << 10) | // 保留
(0x0 << 11) | // 保留
(0x0 << 12) | // 保留
(0x0 << 13) | // 保留
(0x0 << 14) | // 保留
(0x0 << 15) | // 保留
(0x0 << 16) | // 保留
(0x0 << 17) | // 保留
(0x0 << 18) | // 保留
(0x0 << 19) | // 保留
(0x0 << 20) | // 保留
(0x0 << 21) | // 保留
(0x0 << 22) | // 保留
(0x0 << 23) | // 保留
(0x0 << 24) | // 保留
(0x0 << 25) | // 保留
(0x0 << 26) | // 保留
(0x0 << 27) | // 保留
(0x0 << 28) | // 保留
(0x0 << 29) | // 保留
(0x0 << 30) | // 保留
(0x0 << 31); // 保留
// 实际应使用宏定义,例如:
// MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) | (0x0 << MPU_RASR_C_Pos) | (0x0 << MPU_RASR_B_Pos) | (0x1 << MPU_RASR_ENABLE_Pos) | (0x1 << MPU_RASR_SIZE_Pos);
// 但为了简洁,这里省略。
}
```
**推荐使用 STM32CubeMX 生成**,在 `MPU Configuration` 中设置区域属性为 `Non-cacheable`。
## 3.3 启用 MPU 和 D-Cache
```c
void Cache_Init(void)
{
// 启用 D-Cache 和 I-Cache(可选)
SCB_EnableDCache();
SCB_EnableICache();
// 配置 MPU
MPU_Config();
// 启用 MPU
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();
}
```
## 3.4 使用 DMA 缓冲区
定义缓冲区时,确保其位于 MPU 配置的区域内:
```c
// 定义在 0x20000000 起始的 32KB 区域内
uint8_t dma_rx_buffer[1024] __attribute__((section(".dma_buffer")));
```
在链接脚本中,将 `.dma_buffer` 段定位到 0x20000000。或者使用 `__attribute__((aligned(32)))` 并手动指定地址。
# 4. 完整代码示例
以下是一个串口 DMA 接收的完整示例,使用 MPU 配置缓冲区为 Non-cacheable:
```c
#include "stm32f4xx.h"
#include
// 缓冲区定义
uint8_t rx_buffer[256] __attribute__((aligned(32)));
// MPU 配置
void MPU_Config(void)
{
// 禁用 MPU
MPU->CTRL = 0;
// 配置区域0:覆盖整个 SRAM(例如 0x20000000,大小 128KB)
MPU->RNR = 0;
MPU->RBAR = 0x20000000;
// 设置属性:Non-cacheable,允许读写,可执行
MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) | // TEX=0
(0x0 << MPU_RASR_C_Pos) | // C=0
(0x0 << MPU_RASR_B_Pos) | // B=0
(0x0 << MPU_RASR_S_Pos) | // S=0
(0x1 << MPU_RASR_ENABLE_Pos) | // 使能区域
(0x0 << MPU_RASR_AP_Pos) | // 特权/用户读写
(0x0 << MPU_RASR_XN_Pos) | // 可执行
(0x0 << MPU_RASR_SIZE_Pos); // 大小,需计算,例如 128KB => 0x10
// 注意:SIZE 位需要正确设置,128KB 对应 0x10(2^17)
// 这里简化,实际使用宏定义。
}
// 初始化串口 DMA
void UART_DMA_Init(void)
{
// 使能时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
// 配置 USART2 引脚等(省略)
// 配置 DMA 流
DMA2_Stream5->CR &= ~DMA_SxCR_EN; // 禁用
DMA2_Stream5->PAR = (uint32_t)&USART2->DR;
DMA2_Stream5->M0AR = (uint32_t)rx_buffer;
DMA2_Stream5->NDTR = sizeof(rx_buffer);
DMA2_Stream5->CR = DMA_SxCR_CHSEL_1 | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC | DMA_SxCR_DIR_0 | DMA_SxCR_TCIE;
// 使能 DMA 中断
NVIC_EnableIRQ(DMA2_Stream5_IRQn);
// 使能 DMA
DMA2_Stream5->CR |= DMA_SxCR_EN;
}
// DMA 中断处理
void DMA2_Stream5_IRQHandler(void)
{
if (DMA2->LISR & DMA_LISR_TCIF5)
{
DMA2->LIFCR |= DMA_LIFCR_CTCIF5;
// 数据已接收,可直接处理 rx_buffer,因为它是 Non-cacheable
// 处理数据...
}
}
int main(void)
{
// 初始化时钟、GPIO 等(省略)
// 配置 MPU 和 Cache
MPU_Config();
SCB_EnableDCache();
SCB_EnableICache();
MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
__DSB();
__ISB();
// 初始化串口 DMA
UART_DMA_Init();
while (1)
{
// 主循环
}
}
```
# 5. 注意事项
- **缓冲区对齐**:MPU 区域要求基地址和大小对齐,通常按 32 字节对齐。
- **区域大小**:MPU 区域大小必须是 2 的幂,且最小 32 字节。
- **覆盖范围**:如果 DMA 缓冲区分散,可配置多个 MPU 区域,或直接配置整个 SRAM 为 Non-cacheable,但会牺牲性能。
- **Cache 维护**:如果使用 Write-Through 策略,仍需在 DMA 写入后执行 `SCB_InvalidateDCache_by_Addr()` 使缓存行失效。
- **中断安全**:在中断中访问缓冲区时,确保 MPU 配置已生效。
- **性能权衡**:Non-cacheable 会降低 CPU 访问该区域的速度,但 DMA 缓冲区通常较小,影响有限。
# 6. 总结
通过 MPU 配置 DMA 缓冲区为 Non-cacheable,可以彻底避免 D-Cache 与 DMA 的数据一致性问题。本文提供了原理分析和完整代码,开发者可根据实际需求调整区域大小和策略。在追求极致性能时,可考虑 Write-Through 加手动失效,但需谨慎处理时序。掌握这一技巧,将显著提升 STM32F4 项目的稳定性。