STM32F4 实战:D-Cache 与 MPU 配置解决 DMA 与 CPU 数据一致性
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 F429、F407)中,当 CPU 与 DMA 同时访问同一内存区域时,D-Cache 可能导致数据不一致,引发难以排查的 bug。本文深入剖析 Cache 与 DMA 的冲突根源,手把手教你通过 MPU 配置将特定内存区域设置为非缓存(或写透),并提供完整代码示例与调试技巧,确保 DMA 与 CPU 数据同步,提升系统稳定性。
# 引言:DMA 与 D-Cache 的“隐形战争”
在嵌入式开发中,DMA 负责高效搬运数据,而 CPU 依赖 D-Cache 加速访问。然而,当两者操作同一内存时,D-Cache 的“懒惰”写回策略会引发数据不一致:CPU 修改的数据可能仍留在 Cache 中,DMA 却从主存读取旧数据;反之,DMA 写入主存后,CPU 可能读到 Cache 中的过期数据。STM32F4 系列(Cortex-M4)默认不启用 D-Cache,但若使用外部 SDRAM 或开启 D-Cache 加速,问题便浮现。本文以 STM32F429 为例,通过 MPU 配置解决这一经典难题。
# 原理剖析:Cache 与 DMA 的冲突根源
## 1. D-Cache 的工作机制
- **写回(Write-back)**:CPU 写数据时仅更新 Cache,标记为脏(Dirty),延迟写回主存。
- **写分配(Write-allocate)**:读未命中时,将主存数据加载到 Cache。
## 2. DMA 的“旁路”特性
DMA 直接访问主存(SRAM 或 SDRAM),不经过 Cache。因此,当 CPU 与 DMA 共享缓冲区时,Cache 中的脏数据或过期数据会导致不一致。
## 3. 解决方案思路
- **禁用 Cache**:简单粗暴,但牺牲性能。
- **MPU 配置区域为非缓存(Device 或 Strongly-ordered)**:强制 CPU 直接访问主存,适用于 DMA 缓冲区。
- **软件维护 Cache**:使用 `SCB_CleanDCache` 或 `SCB_InvalidateDCache`,但需谨慎时机。
# 实战配置:MPU 设置非缓存区域
## 1. 硬件环境
- 芯片:STM32F429ZIT6(Cortex-M4,带 D-Cache)
- 外部 SDRAM:IS42S16400J(1M×16bit,位于 Bank1)
- 开发环境:Keil MDK 5.27 + STM32CubeF4 固件库
## 2. 配置步骤
### 步骤 1:启用 D-Cache 和 MPU
在系统初始化时,先使能 MPU,再使能 D-Cache(顺序不可颠倒)。
```c
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct;
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 SDRAM 区域(地址 0xC0000000,大小 4MB)为非缓存
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0xC0000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4MB;
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
void Cache_Init(void)
{
// 使能 D-Cache(需在 MPU 之后)
SCB_EnableDCache();
}
```
### 步骤 2:定义 DMA 缓冲区并放置到非缓存区域
将 DMA 缓冲区定义在 SDRAM 中(或通过链接脚本指定),确保其地址落在 MPU 配置的非缓存区域。
```c
// 使用 __attribute__ 指定段,或直接定义在 SDRAM 地址
uint8_t dma_rx_buffer[1024] __attribute__((section(".sdram")));
// 在链接脚本中,将 .sdram 段定位到 0xC0000000 之后
```
### 步骤 3:DMA 与 CPU 交互示例
以 UART DMA 接收为例,演示数据一致性保证。
```c
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer));
// 在 DMA 传输完成回调中处理数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 由于缓冲区是非缓存的,CPU 直接读取主存,无需 Cache 操作
ProcessData(dma_rx_buffer);
// 重新启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, dma_rx_buffer, sizeof(dma_rx_buffer));
}
}
```
### 步骤 4:软件维护 Cache(备用方案)
若无法使用 MPU,可在 DMA 操作前后手动维护 Cache。
```c
// 在 CPU 写数据后,启动 DMA 前,清 Cache
SCB_CleanDCache();
// 在 DMA 完成,CPU 读数据前,无效化 Cache
SCB_InvalidateDCache();
```
# 完整代码示例:MPU + DMA 环形缓冲区
以下是一个更完整的示例,包含 MPU 配置、DMA 环形缓冲区实现。
```c
#include "stm32f4xx_hal.h"
// 定义环形缓冲区结构(位于非缓存区域)
typedef struct {
uint8_t data[256];
uint16_t head;
uint16_t tail;
} RingBuffer;
// 将缓冲区放置到 SDRAM 非缓存区域
RingBuffer g_rb __attribute__((section(".sdram")));
// MPU 配置函数(如前所述)
void MPU_Config(void);
// 初始化 DMA 接收
void DMA_Init(void)
{
// 配置 UART DMA 等,省略具体细节
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MPU_Config();
SCB_EnableDCache();
DMA_Init();
// 启动 DMA 接收,数据直接写入 g_rb.data
HAL_UART_Receive_DMA(&huart1, g_rb.data, sizeof(g_rb.data));
while (1)
{
// 主循环处理数据,无需 Cache 操作
if (g_rb.tail != g_rb.head)
{
ProcessByte(g_rb.data[g_rb.tail++]);
g_rb.tail %= sizeof(g_rb.data);
}
}
}
```
# 注意事项与调试技巧
- **MPU 区域大小对齐**:MPU 区域大小必须是 2 的幂次,且基地址对齐。例如 4MB 区域要求基地址 0xC0000000 对齐到 4MB。
- **链接脚本**:确保 `.sdram` 段正确放置在 SDRAM 起始地址,否则 MPU 不生效。
- **Cache 操作时机**:若使用软件维护,务必在 DMA 启动前 Clean,完成后 Invalidate,顺序错误会导致数据错乱。
- **调试技巧**:使用逻辑分析仪或断点观察内存值,确认 Cache 行为。也可临时禁用 D-Cache 对比测试。
- **性能权衡**:非缓存区域访问速度较慢,仅对 DMA 缓冲区使用,其他高频数据仍可缓存。
# 结语
通过 MPU 将 DMA 缓冲区配置为非缓存,从根源上避免了 Cache 与 DMA 的数据一致性冲突,既保证了正确性,又保留了对其他区域的缓存加速。掌握这一技巧,将使你在处理高速数据采集、网络通信等场景时更加游刃有余。希望本文能成为你嵌入式开发路上的有力工具。