引言:D-Cache 与 DMA 的“隐形冲突”

在嵌入式开发中,STM32F4 系列(基于 Cortex-M4 内核)的 D-Cache(数据缓存)能显著提升 CPU 访问外部存储器(如 SDRAM)的速度。然而,当 DMA 与外设或内存交互时,D-Cache 与 DMA 之间的数据一致性(Cache Coherency)问题便成为开发者的“噩梦”。

  • 问题场景:CPU 写入数据到内存,DMA 读取该内存发送到外设;或 DMA 接收数据到内存,CPU 读取该内存。若 D-Cache 未同步,CPU 可能读到旧数据(DMA 写入但 Cache 未更新),或 DMA 发送旧数据(CPU 写入但 DMA 从内存读取时 Cache 未写回)。
  • 后果:数据错乱、通信丢包、图像显示异常等。

本文将基于 STM32F407 平台,深入剖析三种实战解法,并给出完整代码。

解法一:直接关闭 D-Cache(最简单,但牺牲性能)

原理

关闭 D-Cache 后,CPU 直接访问内存,DMA 与 CPU 共享同一物理内存,自然不存在一致性问题。此方法适用于对性能要求不高、或调试阶段快速验证的场景。

配置步骤

  1. 在系统初始化时,禁用 D-Cache(注意:必须在启用前关闭,或先无效化)。
  2. 确保所有外设 DMA 配置正常。

代码示例

#include "stm32f4xx.h"

void SystemInit_CacheDisable(void) {
    // 禁用 D-Cache(若已启用,先无效化)
    SCB_DisableDCache();
    // 可选:同时禁用 I-Cache(若不需要)
    // SCB_DisableICache();
}

int main(void) {
    // 系统初始化
    SystemInit();
    // 关闭 D-Cache
    SystemInit_CacheDisable();
    
    // 配置 DMA 和 USART 等外设...
    // 正常使用 DMA,无需关心一致性
    while(1);
}

注意事项

  • 关闭 D-Cache 会降低 CPU 访问外部存储器的性能(尤其 SDRAM),可能导致实时性下降。
  • 若使用 RTOS,需确保所有任务均不依赖 D-Cache。

解法二:手动 Cache 清理与无效化(灵活,但需精确控制)

原理

在 DMA 操作前,CPU 需将 D-Cache 中相关地址的数据写回内存(Clean),确保 DMA 能读取最新数据;在 DMA 完成后,CPU 需使 D-Cache 中相关地址的数据无效(Invalidate),强制 CPU 从内存重新读取。

配置步骤

  1. 确定 DMA 操作的缓冲区地址和大小(需对齐到 32 字节)。
  2. 在 DMA 发送前,调用 SCB_CleanDCache_by_Addr 写回数据。
  3. 在 DMA 接收完成后,调用 SCB_InvalidateDCache_by_Addr 使缓存无效。
  4. 确保缓冲区地址在 MPU 配置为“可缓存”区域(默认即可)。

代码示例

#include "stm32f4xx.h"
#include <string.h>

#define BUF_SIZE 1024
// 缓冲区需 32 字节对齐(Cortex-M4 要求)
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];

void DMA_Send(uint8_t *data, uint32_t len) {
    // 1. 将数据写入 tx_buf
    memcpy(tx_buf, data, len);
    // 2. 清理 D-Cache,确保数据写回内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
    // 3. 启动 DMA 发送(此处以 USART 为例)
    // DMA_Config();
    // DMA_Start();
}

void DMA_Receive(uint8_t *data, uint32_t len) {
    // 1. 启动 DMA 接收(数据写入 rx_buf)
    // DMA_Start();
    // 等待 DMA 完成...
    // 2. 使 D-Cache 无效,强制 CPU 从内存读取
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, len);
    // 3. 拷贝数据
    memcpy(data, rx_buf, len);
}

int main(void) {
    // 初始化时钟和 D-Cache(保持启用)
    SystemInit();
    SCB_EnableDCache();
    
    // 使用示例
    uint8_t send_data[] = "Hello DMA";
    DMA_Send(send_data, strlen(send_data));
    
    uint8_t recv_data[BUF_SIZE];
    DMA_Receive(recv_data, 100);
    while(1);
}

注意事项

  • 缓冲区地址必须 32 字节对齐,否则 SCB_CleanDCache_by_Addr 可能无法正确操作。
  • 清理和无效化操作有开销,频繁调用会影响性能,建议批量操作。
  • 若 DMA 缓冲区位于外部 SDRAM,需确保 MPU 配置为“可缓存”且“写回”模式。

解法三:使用 MPU 配置非缓存区域(推荐,性能与一致性兼得)

原理

通过 MPU(内存保护单元)将 DMA 缓冲区所在内存区域配置为“不可缓存”(或“直写”模式),这样 CPU 访问该区域时绕过 D-Cache,而其他区域仍可享受缓存加速。此方法既保证一致性,又保留性能。

配置步骤

  1. 定义 DMA 缓冲区,并放置于特定内存区域(如内部 SRAM 或外部 SDRAM)。
  2. 配置 MPU 区域,设置该区域为“Normal memory, Non-cacheable”。
  3. 启用 MPU。

代码示例

#include "stm32f4xx.h"

// 定义 DMA 缓冲区,放在非缓存区域(通过 MPU 配置)
__attribute__((section(".noncacheable"))) uint8_t dma_buf[1024];

void MPU_Config(void) {
    // 禁用 MPU 进行配置
    MPU_Disable();
    
    // 配置区域 0:覆盖整个 4GB 地址空间,默认属性(可缓存)
    MPU_Region_InitTypeDef MPU_InitStruct;
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x00000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4GB;
    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_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    // 配置区域 1:覆盖 DMA 缓冲区所在区域(假设在 0x20000000 附近,大小 4KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_buf & ~0xFFF; // 对齐到 4KB
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    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;
    MPU_InitStruct.Number = MPU_REGION_NUMBER1;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    
    // 启用 MPU
    MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

int main(void) {
    // 初始化时钟和 D-Cache
    SystemInit();
    SCB_EnableDCache();
    
    // 配置 MPU
    MPU_Config();
    
    // 现在 dma_buf 区域不可缓存,DMA 和 CPU 访问一致
    // 正常使用 DMA
    while(1);
}

注意事项

  • MPU 区域大小必须是 2 的幂次,且基地址对齐到区域大小。
  • 若缓冲区跨区域,需配置多个 MPU 区域或调整缓冲区位置。
  • 非缓存区域的访问性能略低于缓存区域,但远高于关闭 D-Cache。
  • 需在链接脚本中定义 .noncacheable 段,或直接使用绝对地址。

总结与选型建议

| 解法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 关闭 D-Cache | 实现简单,无一致性烦恼 | 性能损失大 | 调试阶段、性能要求低 | | 手动 Clean/Invalidate | 灵活,保留缓存性能 | 需精确控制,有开销 | 缓冲区较少、操作频繁 | | MPU 非缓存区域 | 性能与一致性兼得 | 配置稍复杂 | 生产环境、高性能需求 |

  • 推荐:对于正式产品,优先使用 MPU 方案,将 DMA 缓冲区隔离为非缓存区域,同时保留 D-Cache 加速其他内存访问。
  • 调试技巧:若出现数据异常,可先用解法一快速定位是否一致性导致,再切换至解法三。

希望本文能帮你彻底解决 STM32F4 的 D-Cache 与 DMA 一致性问题,让数据传输稳定可靠。