引言

在 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 大小)。

// 定义 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 区域。

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);
}

关键点TypeExtFieldIsCacheableIsBufferable 组合决定缓存策略。对于 Write-Through,需要设置 TEX=1C=1B=1(即 MPU_TEX_LEVEL1 + MPU_ACCESS_CACHEABLE + MPU_ACCESS_BUFFERABLE)。

4. 使能 D-Cache

在 MPU 配置完成后,使能 D-Cache。

void Cache_Init(void)
{
    // 使能 D-Cache(需在 MPU 之后)
    SCB_EnableDCache();
    // 使能 I-Cache(可选)
    SCB_EnableICache();
}

5. 完整初始化顺序

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    
    // 1. 配置 MPU(先)
    MPU_Config();
    // 2. 使能 Cache(后)
    Cache_Init();
    
    // 3. 初始化 DMA 描述符(此时 CPU 写入会直接更新主存)
    ETH_DMA_Init();
    
    // 其他外设初始化...
}

完整代码示例

以下是一个基于 STM32F407 和以太网 DMA 的简化示例,演示如何配置 MPU 并初始化描述符。

#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,区域对齐,大小幂次。