STM32F4 上利用 D-Cache 与 MPU 配置解决 DMA 描述符缓存一致性问题
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列高性能 MCU 上,当使用 DMA 传输数据时,若启用了 D-Cache,CPU 与 DMA 对同一内存区域的访问会因缓存不一致导致数据错误,尤其对于 DMA 描述符(如以太网 DMA 描述符)这类关键结构。本文深入剖析缓存一致性问题的根源,并给出基于 MPU 配置的两种解决方案:禁用描述符区域的 Cache 或配置为 Write-Through 模式,附带完整代码示例和注意事项,帮助开发者彻底规避这一嵌入式开发中的经典坑。
# 引言
在 STM32F4 系列(如 STM32F407、STM32F429)上,CPU 主频高达 168MHz,且内置了 D-Cache(数据缓存)和 I-Cache(指令缓存)。D-Cache 能显著提升 CPU 访问外部 RAM 或 Flash 的速度,但同时也引入了缓存一致性问题(Cache Coherency)。当 DMA 控制器直接访问内存(如 SRAM)时,CPU 可能仍在缓存中持有旧数据,导致 DMA 读取或写入的数据与 CPU 预期不一致。对于 DMA 描述符(例如以太网 DMA 描述符、SDIO 描述符等),这种不一致会直接导致传输错误、死锁甚至系统崩溃。
本文将聚焦于 STM32F4 上如何通过 MPU(Memory Protection Unit)配置来解决 DMA 描述符的缓存一致性问题,提供两种实用方案,并给出可直接移植的代码。
# 缓存一致性问题的根源
## D-Cache 的工作原理
D-Cache 是 CPU 与主存之间的高速缓存,以缓存行(Cache Line)为单位(STM32F4 的 D-Cache 行大小为 32 字节)。当 CPU 读取内存时,会优先从 Cache 中获取;写入时,可能采用 Write-Back 策略(数据先写入 Cache,延迟写回主存)或 Write-Through 策略(同时写入 Cache 和主存)。
## 问题场景
假设 DMA 描述符位于 SRAM 中,CPU 初始化描述符后,DMA 控制器读取该描述符以获取缓冲区地址和长度。若 CPU 写入描述符时数据仍滞留在 D-Cache 中(Write-Back 模式),DMA 直接从 SRAM 读取时可能得到旧数据。反之,当 DMA 更新描述符(如设置完成标志)时,CPU 若从 Cache 读取,可能看不到 DMA 写入的最新值。
# 解决方案概述
解决思路是确保 DMA 描述符所在内存区域不被 D-Cache 缓存,或者采用 Write-Through 策略(保证写操作立即更新主存)。STM32F4 的 MPU 允许我们将特定内存区域配置为:
- **禁止缓存(Strongly-ordered 或 Device)**:CPU 访问该区域时绕过 Cache,直接访问主存。
- **Write-Through**:写入时同时更新 Cache 和主存,读取时仍可缓存,但写操作不会延迟。
对于 DMA 描述符,推荐使用 Write-Through 模式,因为描述符访问频率较低,性能影响可忽略,且能保证一致性。
# 配置步骤
## 1. 使能 D-Cache 和 MPU
在系统初始化时,需要先使能 MPU,再使能 D-Cache(顺序很重要)。
## 2. 定义描述符内存区域
通常将 DMA 描述符放置在一个独立的内存段(如 `.dma_desc`),或者使用固定的 SRAM 地址。这里我们使用一个数组,并确保其对齐到 32 字节(Cache Line 大小)。
```c
// 定义 DMA 描述符数组,对齐到 32 字节
__attribute__((aligned(32))) ETH_DMADescTypeDef dma_descriptor_tab[ETH_TX_DESC_CNT + ETH_RX_DESC_CNT];
```
## 3. 配置 MPU 区域
使用 HAL 库的 `HAL_MPU_ConfigRegion()` 函数配置 MPU 区域。
```c
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
// 禁用 MPU 进行配置
HAL_MPU_Disable();
// 配置 DMA 描述符区域(假设起始地址为 dma_descriptor_tab 的地址,大小根据实际计算)
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)&dma_descriptor_tab;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; // 根据描述符总大小调整,需为 2 的幂
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; // 允许缓存
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // 配置为 Write-Through
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
// 使能 MPU
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
```
**关键点**:`TypeExtField` 和 `IsCacheable`、`IsBufferable` 组合决定缓存策略。对于 Write-Through,需要设置 `TEX=1`,`C=1`,`B=1`(即 `MPU_TEX_LEVEL1` + `MPU_ACCESS_CACHEABLE` + `MPU_ACCESS_BUFFERABLE`)。
## 4. 使能 D-Cache
在 MPU 配置完成后,使能 D-Cache。
```c
void Cache_Init(void)
{
// 使能 D-Cache(需在 MPU 之后)
SCB_EnableDCache();
// 使能 I-Cache(可选)
SCB_EnableICache();
}
```
## 5. 完整初始化顺序
```c
int main(void)
{
HAL_Init();
SystemClock_Config();
// 1. 配置 MPU(先)
MPU_Config();
// 2. 使能 Cache(后)
Cache_Init();
// 3. 初始化 DMA 描述符(此时 CPU 写入会直接更新主存)
ETH_DMA_Init();
// 其他外设初始化...
}
```
# 完整代码示例
以下是一个基于 STM32F407 和以太网 DMA 的简化示例,演示如何配置 MPU 并初始化描述符。
```c
#include "stm32f4xx_hal.h"
// 定义描述符数量
#define ETH_TX_DESC_CNT 4
#define ETH_RX_DESC_CNT 4
// 描述符数组,对齐到 32 字节
__attribute__((aligned(32))) ETH_DMADescTypeDef dma_descriptor_tab[ETH_TX_DESC_CNT + ETH_RX_DESC_CNT];
// MPU 配置函数
void MPU_Config(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
// 配置描述符区域为 Write-Through
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = (uint32_t)&dma_descriptor_tab;
// 计算大小:每个描述符约 32 字节,共 8 个,约 256 字节,选择 512 字节区域(需 2 的幂)
MPU_InitStruct.Size = MPU_REGION_SIZE_512B;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; // Write-Through
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
// Cache 初始化
void Cache_Init(void)
{
SCB_EnableDCache();
SCB_EnableICache();
}
// 初始化 DMA 描述符(示例)
void DMA_Desc_Init(void)
{
for (int i = 0; i < ETH_TX_DESC_CNT + ETH_RX_DESC_CNT; i++) {
dma_descriptor_tab[i].Status = 0;
dma_descriptor_tab[i].ControlBufferSize = 0;
dma_descriptor_tab[i].Buffer1Addr = 0;
dma_descriptor_tab[i].Buffer2Addr = 0;
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
// 先配置 MPU,再使能 Cache
MPU_Config();
Cache_Init();
// 初始化描述符
DMA_Desc_Init();
while (1) {
// 主循环
}
}
```
# 注意事项
- **MPU 区域大小必须为 2 的幂**,且起始地址需对齐到区域大小。如果描述符数组较小,可适当增大区域(如 1KB)以覆盖对齐要求。
- **配置顺序**:必须先使能 MPU,再使能 D-Cache。若顺序颠倒,MPU 配置可能无效。
- **其他 DMA 缓冲区**:对于 DMA 传输的数据缓冲区,如果数据量较大且频繁访问,建议采用手动维护 Cache 的方式(如 `SCB_CleanDCache()` 和 `SCB_InvalidateDCache()`),而不是禁用 Cache,以保持性能。但描述符区域建议使用 Write-Through 或禁用 Cache。
- **多核或共享内存**:如果系统中有多个总线主设备(如 DMA、以太网 MAC),还需考虑 MPU 的 Shareable 属性。对于内部 SRAM,通常设置为 Not Shareable 即可。
- **调试建议**:在调试时,可以临时禁用 D-Cache 来验证问题是否由缓存引起。若禁用后正常,则确认是缓存一致性问题。
# 总结
在 STM32F4 上使用 D-Cache 时,DMA 描述符的缓存一致性是必须处理的关键问题。通过 MPU 将描述符所在内存区域配置为 Write-Through 模式,可以简单有效地保证 CPU 与 DMA 之间的数据一致性,且对性能影响极小。本文提供的配置方法和代码可直接应用于实际项目,帮助开发者避开这一嵌入式开发中的经典陷阱。记住:先 MPU,后 Cache,区域对齐,大小幂次。