# 引言 在 STM32F4 系列(如 STM32F429、F407)中,D-Cache(数据缓存)是提升 CPU 访问外部 SDRAM 或低速外设的关键硬件。然而,D-Cache 的引入也带来了一个经典难题:**DMA 与 CPU 之间的数据一致性**。当 CPU 和 DMA 同时访问同一内存区域时,若未正确处理缓存,轻则数据错误,重则系统崩溃。本文面向有嵌入式开发基础的工程师,深入剖析三种典型场景,并提供基于 CMSIS 的解决方案。 # 背景知识:D-Cache 与 DMA 的冲突根源 D-Cache 是 CPU 与主存之间的高速缓存,默认采用 **写回(Write-back)** 策略:CPU 写数据时,数据先写入缓存,标记为脏(Dirty),之后才被回写到主存。DMA 则直接访问主存,不经过缓存。因此,当 CPU 修改数据后,DMA 可能从主存读到旧数据;反之,DMA 写入新数据后,CPU 可能从缓存读到旧数据。 STM32F4 的 Cortex-M4 内核提供了两个关键操作: - **Clean**:将脏缓存行回写到主存。 - **Invalidate**:使缓存行失效,下次访问时从主存重新加载。 CMSIS 提供了 `SCB_CleanDCache()`、`SCB_InvalidateDCache()` 等函数,以及更精细的按地址操作函数 `SCB_CleanDCache_by_Addr()`、`SCB_InvalidateDCache_by_Addr()`。 # 场景一:CPU 写数据,DMA 读数据 ## 问题描述 CPU 在内存中准备好一组数据(如 ADC 采样值),然后启动 DMA 将其传输到外设(如 DAC 或 UART)。由于 D-Cache 的写回策略,数据可能仍停留在缓存中,DMA 从主存读取时得到的是旧数据。 ## 解决方案:Clean 操作 在启动 DMA 传输前,必须对缓冲区执行 Clean 操作,确保脏数据回写到主存。 ## 代码示例 ```c // 缓冲区定义(需对齐到 32 字节,即缓存行大小) __attribute__((aligned(32))) uint32_t tx_buffer[256]; void prepare_data_and_start_dma(void) { // CPU 写入数据 for (int i = 0; i < 256; i++) { tx_buffer[i] = i * 2; } // 关键:Clean D-Cache,将脏数据回写到主存 SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, sizeof(tx_buffer)); // 启动 DMA 传输(假设 DMA 已配置好) DMA_Start_Transfer(tx_buffer, 256); } ``` ## 注意事项 - 缓冲区地址和大小必须对齐到 32 字节(缓存行大小),否则 Clean 操作可能不完整。 - 若缓冲区较大,可考虑使用 `SCB_CleanDCache()` 全清,但性能较低,建议按地址操作。 # 场景二:DMA 写数据,CPU 读数据 ## 问题描述 DMA 从外设(如 ADC、UART)接收数据到内存缓冲区,然后 CPU 读取这些数据进行处理。由于 D-Cache 可能包含旧数据,CPU 读到的可能是缓存中的过期内容。 ## 解决方案:Invalidate 操作 在 CPU 读取数据前,必须对缓冲区执行 Invalidate 操作,使缓存行失效,强制从主存重新加载。 ## 代码示例 ```c __attribute__((aligned(32))) uint32_t rx_buffer[256]; void DMA_Transfer_Complete_Callback(void) { // 关键:Invalidate D-Cache,使缓存行失效,从主存重新加载 SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer)); // 现在可以安全地读取 rx_buffer 中的数据 for (int i = 0; i < 256; i++) { process_sample(rx_buffer[i]); } } ``` ## 注意事项 - Invalidate 操作会丢弃缓存中的脏数据,因此仅当 DMA 完全覆盖缓冲区时才安全。若 DMA 只写入部分区域,需谨慎处理。 - 若 DMA 在后台持续传输,建议使用双缓冲机制(见场景三)。 # 场景三:共享缓冲区,CPU 和 DMA 交替访问 ## 问题描述 在实时系统中,CPU 和 DMA 可能频繁交替访问同一缓冲区(如音频流、图像处理)。若每次切换都执行全量 Clean/Invalidate,性能开销巨大,且容易出错。 ## 解决方案:双缓冲 + 缓存维护策略 采用 **双缓冲**(Ping-Pong Buffer)机制,将缓冲区分为两个独立区域。CPU 处理一个区域时,DMA 填充另一个区域,通过切换角色避免冲突。同时,在每个区域切换时执行必要的缓存操作。 ## 代码示例 ```c #define BUFFER_SIZE 1024 __attribute__((aligned(32))) uint32_t buffer[2][BUFFER_SIZE]; volatile uint8_t current_buf = 0; void DMA_Transfer_Complete_Callback(void) { uint8_t done_buf = current_buf; // 切换缓冲区 current_buf ^= 1; // 对已完成 DMA 写入的缓冲区执行 Invalidate SCB_InvalidateDCache_by_Addr((uint32_t*)buffer[done_buf], sizeof(buffer[done_buf])); // 处理数据(此时 CPU 读取的是最新数据) process_data(buffer[done_buf], BUFFER_SIZE); // 处理完毕后,若 CPU 修改了缓冲区,需 Clean 后再交给 DMA SCB_CleanDCache_by_Addr((uint32_t*)buffer[done_buf], sizeof(buffer[done_buf])); } void DMA_Init(void) { // 配置 DMA 使用 buffer[current_buf] 作为目标地址 DMA_SetTarget(buffer[current_buf]); DMA_Start(); } ``` ## 注意事项 - 双缓冲可避免 CPU 和 DMA 同时访问同一内存区域,但缓存操作仍需按需执行。 - 若缓冲区较大,可考虑使用 MPU(内存保护单元)将区域配置为 **非缓存(Non-cacheable)**,彻底避免一致性问题,但会牺牲 CPU 访问性能。 # 进阶技巧:使用 MPU 配置内存属性 对于实时性要求高、且频繁被 DMA 访问的缓冲区,可配置 MPU 将该区域设为 **非缓存** 或 **写通(Write-through)**。这样 CPU 写入时直接更新主存,DMA 读取时始终得到最新数据,无需手动 Clean/Invalidate。 ```c void MPU_Config_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct; // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); // 配置区域:例如 0x20000000,大小 32KB,非缓存 MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x20000000; MPU_InitStruct.Size = MPU_REGION_SIZE_32KB; 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; HAL_MPU_ConfigRegion(&MPU_InitStruct); // 使能 MPU HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } ``` # 总结与最佳实践 - **场景一**:CPU 写 → DMA 读,必须执行 **Clean**。 - **场景二**:DMA 写 → CPU 读,必须执行 **Invalidate**。 - **场景三**:交替访问,使用 **双缓冲** + 按需 Clean/Invalidate,或配置 MPU 为非缓存。 **最佳实践**: - 缓冲区对齐到 32 字节,并使用 `__attribute__((aligned(32)))`。 - 优先使用 `SCB_CleanDCache_by_Addr` 和 `SCB_InvalidateDCache_by_Addr` 进行精细操作,避免全缓存操作带来的性能损失。 - 在 DMA 中断回调中执行缓存操作,确保时序正确。 - 若系统对实时性要求极高,可考虑使用 MPU 将 DMA 相关区域配置为非缓存,但需权衡 CPU 访问性能。 通过以上方案,你可以彻底解决 STM32F4 系列 D-Cache 与 DMA 的数据一致性问题,让系统稳定运行。