# 一、问题背景:D-Cache 与 DMA 的冲突 STM32F4 内核(Cortex-M4)内置了 4KB 的 D-Cache 和 I-Cache,用于加速 CPU 对内存的访问。然而,当 DMA 控制器直接访问内存(如外部 SRAM、SDRAM)时,CPU 可能先将数据缓存在 D-Cache 中,而 DMA 直接读写物理内存,导致两者看到的数据不一致。 典型场景: - 使用 DMA 从 ADC 采集数据到外部 SRAM,CPU 读取该缓冲区时可能读到旧数据(DMA 已更新内存,但 Cache 未失效)。 - CPU 先写入缓冲区,然后启动 DMA 发送,但 DMA 可能读到 Cache 中尚未写回内存的旧数据。 这种问题表现为随机性、偶发性错误,极难排查。 # 二、原理剖析:Cache 一致性模型 Cortex-M4 的 D-Cache 采用 **Write-Back** 策略(默认):CPU 写操作只更新 Cache,不立即写回内存,直到 Cache 行被替换或显式 Clean。而 DMA 不经过 Cache,直接访问物理内存。 因此,一致性维护需要两种操作: - **Clean**:将 Cache 中脏数据写回内存。 - **Invalidate**:使 Cache 行失效,下次访问从内存重新加载。 STM32F4 提供了 SCB 相关函数(如 `SCB_CleanDCache()`、`SCB_InvalidateDCache()`),但手动管理繁琐且易出错。更优雅的方案是使用 **MPU** 配置内存区域的 Cache 策略。 # 三、MPU 配置方案 MPU(Memory Protection Unit)不仅可以设置访问权限,还能定义内存区域的 Cache 属性。对于 DMA 共享缓冲区,有两种推荐配置: ## 方案 A:关闭该区域的 Cache(Strongly-Ordered 或 Device) 将缓冲区所在区域配置为 **Normal memory, Non-cacheable**,即完全绕过 D-Cache。优点是简单可靠,缺点是性能下降(CPU 每次访问都直接读内存)。 ## 方案 B:配置为 Write-Through 模式 Write-Through 策略下,CPU 写操作同时更新 Cache 和内存,读操作仍可缓存。这样 DMA 读内存时总能得到最新数据,而 CPU 读 DMA 写入的数据时,只需在 DMA 完成后执行一次 Invalidate(或依赖硬件自动失效,但 STM32F4 不支持硬件一致性,仍需软件处理)。 实际工程中,**方案 B 更常用**,兼顾性能与一致性。 # 四、实战配置步骤 以 STM32F407 为例,假设使用外部 SRAM(地址 0x68000000,大小 1MB)作为 DMA 缓冲区。 ## 1. 使能 D-Cache 和 MPU 在 `main()` 函数初始化阶段: ```c #include "stm32f4xx.h" void MPU_Config(void) { // 确保 MPU 未使能 MPU->CTRL = 0; // 配置区域 0:外部 SRAM 区域,Write-Through MPU->RBAR = 0x68000000 | MPU_REGION_SIZE_1MB | (0 << 0); // Region 0 MPU->RASR = (0x0 << 0) | // 无访问权限限制 (0x1 << 1) | // 允许执行 (0x0 << 3) | // 非共享 (0x1 << 4) | // Write-Through (TEX=0, C=1, B=0) (0x0 << 5) | // 非缓存 (0x0 << 6) | // 非缓冲 (0x0 << 8) | // 无子区域禁用 (0x0 << 16) | // 无指令访问 (0x1 << 24); // 使能 Region // 使能 MPU,使用默认内存映射作为后备 MPU->CTRL = (0x1 << 0) | (0x1 << 2); // Enable MPU, Enable default region // 使能 D-Cache(如果尚未使能) SCB_EnableDCache(); } ``` 注意:`MPU_RASR` 的 TEX、C、B 位组合决定了 Cache 策略。对于 Write-Through,需要设置 TEX=0, C=1, B=0(即 Normal memory, Write-Through)。 ## 2. 配置 DMA 缓冲区 将 DMA 缓冲区定义在外部 SRAM 区域,并确保编译器将其放置到正确地址: ```c #define BUF_SIZE 1024 uint8_t dma_buf[BUF_SIZE] __attribute__((section(".ext_sram"))); ``` 在链接脚本(.ld)中添加: ```c .ext_sram 0x68000000 : { *(.ext_sram) } > EXTSRAM ``` ## 3. DMA 传输中的一致性处理 即使配置了 Write-Through,在 DMA 写入完成后,CPU 读取前仍需执行 Invalidate,以确保 Cache 中可能存在的旧数据被清除: ```c // DMA 接收完成回调 void DMA_RxComplete(DMA_HandleTypeDef *hdma) { // 使缓冲区对应的 Cache 行失效 SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE); // 现在 CPU 可以安全读取 dma_buf } // CPU 写入后启动 DMA 发送 void DMA_SendData(void) { // 确保 CPU 写操作已通过 Write-Through 更新到内存(实际上已自动完成) // 无需 Clean,但为了保险可执行 Clean SCB_CleanDCache_by_Addr((uint32_t*)dma_buf, BUF_SIZE); // 启动 DMA 发送 HAL_UART_Transmit_DMA(&huart, dma_buf, BUF_SIZE); } ``` # 五、完整代码示例 以下是一个完整的初始化与使用示例(基于 HAL 库): ```c // main.c #include "stm32f4xx_hal.h" void MPU_Config(void); void DMA_Init(void); uint8_t dma_buf[1024] __attribute__((section(".ext_sram"))); int main(void) { HAL_Init(); SystemClock_Config(); // 配置 MPU 和 D-Cache MPU_Config(); // 初始化 DMA(略) DMA_Init(); // 启动 DMA 接收 HAL_UART_Receive_DMA(&huart, dma_buf, sizeof(dma_buf)); while (1) { // 主循环 } } void MPU_Config(void) { // 禁用 MPU MPU->CTRL = 0; // 配置 Region 0:外部 SRAM 0x68000000,1MB,Write-Through MPU->RBAR = 0x68000000 | (0 << 0); // 基地址 + Region 0 MPU->RASR = (0x0 << 0) | // 权限:全部可访问 (0x1 << 1) | // 可执行 (0x0 << 3) | // 非共享 (0x1 << 4) | // TEX=0, C=1, B=0 => Write-Through (0x0 << 5) | (0x0 << 6) | (0x0 << 8) | // 子区域全部使能 (0x0 << 16) | // 无指令缓存 (0x1 << 24); // Region 使能 // 使能 MPU,并启用默认区域 MPU->CTRL = (0x1 << 0) | (0x1 << 2); // 使能 D-Cache(如果之前未使能) SCB_EnableDCache(); } void DMA_Init(void) { // 初始化 DMA 流、通道等(略) // 注意:DMA 的缓冲区地址必须指向外部 SRAM } // DMA 接收完成回调 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 使缓冲区失效,确保读取最新数据 SCB_InvalidateDCache_by_Addr((uint32_t*)dma_buf, sizeof(dma_buf)); // 处理数据... } } ``` # 六、注意事项 - **地址对齐**:`SCB_InvalidateDCache_by_Addr` 和 `SCB_CleanDCache_by_Addr` 要求地址按 32 字节对齐(Cache 行大小)。如果缓冲区未对齐,需手动调整或使用整 Cache 操作(如 `SCB_InvalidateDCache()`),但效率较低。 - **MPU 区域重叠**:确保 MPU 区域不与其他区域重叠,否则行为未定义。建议使用默认内存映射作为后备。 - **性能权衡**:Write-Through 模式会降低写性能(每次写都到内存),但对于 DMA 缓冲区这种低频访问场景,影响可忽略。 - **DMA 方向**: - 外设→内存:DMA 写入后,CPU 读取前必须 Invalidate。 - 内存→外设:CPU 写入后,DMA 读取前建议 Clean(Write-Through 下可省略,但保险起见执行)。 - **多缓冲区**:如果使用双缓冲,需分别配置 MPU 区域或确保同一区域覆盖所有缓冲区。 - **调试技巧**:若问题依旧,可暂时关闭 D-Cache 验证是否由 Cache 引起,再逐步优化。 # 七、总结 通过 MPU 将 DMA 共享缓冲区配置为 Write-Through 模式,并配合必要的 Cache 维护操作,可以彻底解决 STM32F4 上 DMA 与 CPU 的数据一致性问题。该方案在保证性能的同时,提供了可靠的同步机制。实际项目中,务必根据内存区域特性灵活配置,并遵循上述注意事项,以避免踩坑。