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