引言

STM32F4 系列(如 STM32F407、F429)内置了 D-Cache(数据缓存),用于加速 CPU 对片外存储器(如 SDRAM)的访问。然而,当 DMA 控制器直接访问内存时,它不经过 Cache,这会导致 CPU 与 DMA 看到的数据不一致。例如,CPU 写入数据到内存(可能暂存于 Cache),DMA 读取时可能拿到旧数据;反之,DMA 写入新数据,CPU 读取时可能命中 Cache 中的旧副本。这种不一致性在以太网、USB、SDIO 等高速外设中尤为致命。

本文针对 STM32F4 的 D-Cache(注意:F4 系列只有部分型号带 D-Cache,如 F429、F469,而 F407 没有,但原理通用),提供三种实用策略来维护一致性。

原理剖析

D-Cache 的工作机制

D-Cache 是 CPU 与主存之间的高速缓存,以缓存行(通常 32 字节)为单位。当 CPU 读数据时,若命中 Cache 则直接返回;写数据时,采用写回(Write-back)策略,即数据先写入 Cache,标记为脏,待时机成熟再写回主存。

DMA 的访问路径

DMA 控制器直接连接总线矩阵,绕过 CPU 和 Cache,直接读写主存。因此,DMA 看到的是主存的真实数据,而 CPU 可能看到的是 Cache 中的副本。

不一致性场景

  • CPU 写,DMA 读:CPU 写入数据到缓冲区,数据可能留在 Cache 中,尚未写回主存。DMA 读取主存时,得到的是旧数据。
  • DMA 写,CPU 读:DMA 将外设数据写入主存,但 CPU 的 Cache 中可能已有该地址的旧副本,CPU 读取时命中 Cache,得到旧数据。

三种实用策略

策略一:Cache 清理与失效(Clean & Invalidate)

这是最直接的方法,在 DMA 操作前后手动维护 Cache。

原理

  • 清理(Clean):将 Cache 中的脏数据写回主存,确保主存数据最新。
  • 失效(Invalidate):使 Cache 中的行失效,下次 CPU 访问时重新从主存加载。

配置步骤

  1. 启用 D-Cache(若未启用)。
  2. 在 DMA 发送前,调用 SCB_CleanDCache()SCB_CleanDCache_by_Addr() 清理缓冲区。
  3. 在 DMA 接收后,调用 SCB_InvalidateDCache_by_Addr() 使缓冲区失效。

代码示例

// 发送缓冲区,CPU 写入数据后,清理 Cache 再启动 DMA
uint8_t tx_buf[128];
// 填充数据...
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart, tx_buf, sizeof(tx_buf));

// 接收缓冲区,DMA 完成后,使 Cache 失效再读取
uint8_t rx_buf[128];
HAL_UART_Receive_DMA(&huart, rx_buf, sizeof(rx_buf));
// 等待 DMA 完成回调...
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 现在可以安全读取 rx_buf

注意事项

  • 地址和长度需按 32 字节对齐,否则可能影响其他数据。
  • 频繁清理/失效会降低性能,适合数据量小或低频场景。
  • 使用 HAL 库时,部分驱动已内置处理,需确认。

策略二:MPU 配置非缓存区域

通过内存保护单元(MPU)将 DMA 缓冲区所在的区域配置为不缓存(或写-through),从而避免不一致性。

原理

MPU 可以设置内存区域的属性,如 Normal, Non-cacheable。这样 CPU 访问该区域时直接读写主存,绕过 Cache,保证一致性。

配置步骤

  1. 定义缓冲区区域,确保其地址和大小满足 MPU 对齐要求(通常 32 字节)。
  2. 初始化 MPU,配置区域属性为 DeviceNormal, Non-cacheable
  3. 启用 MPU。

代码示例

// 定义缓冲区,放在独立区域
__attribute__((section(".dma_buf"))) uint8_t dma_buf[256];

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置区域 0:dma_buf 所在区域,非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)dma_buf;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256B; // 根据实际大小调整
    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.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 启用 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意事项

  • MPU 区域大小必须是 2 的幂,且地址对齐。
  • 非缓存区域访问速度较慢,但 DMA 一致性得到保证。
  • 需在系统初始化时调用 MPU_Config(),并确保链接脚本将 dma_buf 放入正确段。

策略三:DMA 描述符与缓冲区分区管理

将 DMA 描述符(如 DMA 控制结构)和实际数据缓冲区放置在不同的内存区域,分别管理缓存属性。

原理

描述符通常由 CPU 频繁读写,而数据缓冲区由 DMA 和 CPU 交互。通过将描述符放在可缓存区域(性能高),数据缓冲区放在非缓存区域(一致性),或反之,根据实际需求优化。

配置步骤

  1. 分配两个内存区域:一个用于描述符(如 __attribute__((section(".desc")))),一个用于数据缓冲区(如 __attribute__((section(".buf"))))。
  2. 在链接脚本中定义这些段,并设置 MPU 属性(如描述符可缓存,缓冲区非缓存)。
  3. 初始化 DMA 时,描述符和缓冲区分别使用对应地址。

代码示例

// 链接脚本示例(.ld)
// .desc : { *(.desc) } >RAM
// .buf  : { *(.buf) } >RAM

// 定义段
__attribute__((section(".desc"))) DMA_HandleTypeDef hdma_desc;
__attribute__((section(".buf"))) uint8_t data_buf[1024];

// MPU 配置:desc 区域可缓存,buf 区域非缓存
void MPU_Config_Split(void)
{
    // 配置区域 0:desc 区域,可缓存
    // 配置区域 1:buf 区域,非缓存
    // 具体代码类似策略二,但需设置两个区域
}

// DMA 初始化时,使用 data_buf 作为缓冲区,hdma_desc 作为描述符

注意事项

  • 需要精心设计链接脚本,确保段地址符合 MPU 对齐要求。
  • 描述符和缓冲区的大小需合理规划,避免浪费内存。
  • 此策略适用于复杂系统,如多通道 DMA 或网络协议栈。

总结

在 STM32F4 系列使用 D-Cache 时,维护 DMA 一致性是确保系统稳定的关键。三种策略各有优劣:

  • 策略一 简单直接,但性能损失较大,适合低频小数据。
  • 策略二 配置一次,长期有效,但牺牲了缓存性能,适合缓冲区固定且频繁 DMA 的场景。
  • 策略三 最灵活,可针对不同数据特性优化,但实现复杂,适合大型项目。

开发者应根据实际需求选择合适策略,并在设计初期就考虑内存布局和缓存配置。希望本文能帮助你避开 D-Cache 的坑,写出更健壮的嵌入式代码。