引言

在 STM32F4 系列(如 STM32F407)高性能 MCU 中,D-Cache 的引入极大提升了 CPU 访问外部存储器(如 SDRAM)的速度,但同时也带来了与 DMA 协同工作时的缓存一致性问题。当 DMA 描述符(如 DMA2D、以太网 DMA 描述符)由 CPU 在内存中创建或修改后,DMA 外设可能读取到 D-Cache 中的陈旧数据,导致数据传输错误。本文将深入分析问题根源,并给出三种实用策略,帮助开发者确保数据一致性。

问题根源

STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写操作先更新 Cache,而不会立即写回主存。当 DMA 外设(如 DMA2D、以太网 MAC)通过 AHB 总线直接访问内存时,它绕过 Cache,直接读取主存。如果描述符数据仍停留在 Cache 中,DMA 将读到旧值。同理,DMA 写入内存的数据也可能被 CPU 从 Cache 中读到旧值。

策略一:Cache 清理与无效化(最直接)

原理

在 CPU 修改描述符后、启动 DMA 前,调用 SCB_CleanDCache() 将 Cache 中对应地址的数据写回主存。在 DMA 完成传输后、CPU 读取描述符前,调用 SCB_InvalidateDCache() 使 Cache 中对应行失效,强制从主存重新加载。

配置步骤

  1. 确保在 system_stm32f4xx.c 中启用了 D-Cache(SCB_EnableDCache())。
  2. 在修改描述符后,调用清理函数。
  3. 在 DMA 传输完成后,调用无效化函数。

代码示例

// 假设描述符结构体
__attribute__((aligned(32))) DMA_Descriptor_t desc;

// CPU 修改描述符
desc.ctrl = 0x1234;
desc.addr = (uint32_t)buffer;

// 清理 D-Cache,确保写回主存
SCB_CleanDCache_by_Addr((uint32_t*)&desc, sizeof(desc));

// 启动 DMA(以 DMA2D 为例)
DMA2D->FGMAR = (uint32_t)&desc;
DMA2D->CR |= DMA2D_CR_START;

// 等待传输完成
while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ;

// 无效化 D-Cache,确保读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)&desc, sizeof(desc));

// 现在可以安全读取 desc 中的状态

注意事项

  • 清理/无效化操作有性能开销,频繁调用会影响效率。
  • 地址必须 32 字节对齐(Cache line 大小),否则可能无效。
  • 建议使用 by_Addr 变体,避免全 Cache 操作。

策略二:使用非缓存内存区域(推荐)

原理

将 DMA 描述符放置在 MPU 配置为非缓存(Non-cacheable)的内存区域中。这样 CPU 访问该区域时直接读写主存,无需 Cache 维护,从根源上避免一致性问题。

配置步骤

  1. 在链接脚本中定义一段独立内存区域,如 DMA_DESC
  2. 使用 MPU 将该区域配置为 DeviceStrongly-ordered 属性(非缓存)。
  3. 将描述符变量放置在该区域。

代码示例

// 链接脚本(.ld)中添加:
// .dma_desc (NOLOAD) : { *(.dma_desc) } > RAM_DMA

// 定义描述符变量,并指定段属性
__attribute__((section(".dma_desc"), aligned(32))) DMA_Descriptor_t desc;

// MPU 配置(初始化时调用)
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000; // 假设 RAM_DMA 起始地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL_0; // 非缓存
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_Init(&MPU_InitStruct);
    
    MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

// 使用描述符时无需任何 Cache 操作

注意事项

  • 需要合理规划内存布局,确保区域大小足够。
  • 非缓存区域访问速度较慢,但描述符访问频率低,影响可忽略。
  • 确保 MPU 配置在启用 D-Cache 之前完成。

策略三:描述符对齐与双缓冲设计(高级)

原理

利用 Cache line 对齐特性,将描述符设计为 32 字节对齐,并采用双缓冲机制。CPU 在修改描述符 A 时,DMA 正在使用描述符 B,通过交替使用避免同时访问同一 Cache line。同时,在切换时进行必要的 Cache 操作,但频率降低。

配置步骤

  1. 定义两个描述符,均 32 字节对齐。
  2. 使用一个标志位指示当前活跃描述符。
  3. 在切换前清理旧描述符的 Cache,切换后无效化新描述符的 Cache。

代码示例

#define DESC_NUM 2
__attribute__((aligned(32))) DMA_Descriptor_t descs[DESC_NUM];
volatile uint8_t active_desc = 0;

void DMA_Start_Transfer(void) {
    uint8_t next = active_desc ^ 1;
    
    // 修改下一个描述符
    descs[next].ctrl = ...;
    descs[next].addr = ...;
    
    // 清理下一个描述符的 Cache(因为 DMA 将读取)
    SCB_CleanDCache_by_Addr((uint32_t*)&descs[next], sizeof(DMA_Descriptor_t));
    
    // 启动 DMA 使用 next 描述符
    DMA2D->FGMAR = (uint32_t)&descs[next];
    DMA2D->CR |= DMA2D_CR_START;
    
    // 等待完成,然后无效化当前描述符(因为 CPU 可能读取状态)
    while (!(DMA2D->ISR & DMA2D_ISR_TCIF)) ;
    SCB_InvalidateDCache_by_Addr((uint32_t*)&descs[active_desc], sizeof(DMA_Descriptor_t));
    
    // 切换活跃描述符
    active_desc = next;
}

注意事项

  • 描述符必须独立位于不同 Cache line,避免伪共享。
  • 切换逻辑需保证 DMA 不会同时访问两个描述符。
  • 适用于高速传输场景,可减少 Cache 操作次数。

总结

三种策略各有优劣:策略一简单但开销大;策略二推荐用于新设计,彻底避免问题;策略三适合性能敏感场景。实际开发中,应根据系统对实时性和复杂度的要求选择。无论哪种方案,理解 D-Cache 工作原理是前提。建议在项目初期就规划好内存布局,避免后期调试的困扰。