STM32F4 系列 D-Cache 与 DMA 数据一致性:三种典型场景与规避方案

引言

在 STM32F4 系列(特别是带有 D-Cache 的型号,如 STM32F429/439)中,CPU 内核通过 D-Cache 加速对 SRAM 的访问,但 DMA 控制器直接访问物理内存,绕过 D-Cache。这种架构差异导致数据一致性问题:当 CPU 和 DMA 同时操作同一内存区域时,可能读到陈旧数据或丢失最新数据。本文面向有嵌入式基础的开发者,分析三种典型场景,并提供实用的规避方案。

原理基础

  • D-Cache 工作原理:D-Cache 是 CPU 与内存之间的高速缓存,采用写回(Write-back)策略。CPU 写数据时,先写入 Cache,标记为脏(Dirty),延迟写回内存;CPU 读数据时,优先从 Cache 读取,若未命中则从内存加载。
  • DMA 访问路径:DMA 控制器直接通过总线访问物理内存(如 SRAM),不经过 D-Cache。因此,DMA 看到的是内存中的真实数据,而 CPU 可能看到的是 Cache 中的副本。
  • 一致性问题的本质:当 CPU 和 DMA 共享同一内存区域时,Cache 与内存之间的数据不同步,导致一方读取到过时数据。

三种典型场景与规避方案

场景一:DMA 写入内存,CPU 读取(如 ADC 采样数据)

问题描述:DMA 将外设数据(如 ADC 转换结果)传输到内存缓冲区,CPU 随后读取该缓冲区。若 DMA 写入后,CPU 的 Cache 中仍保留旧数据,则 CPU 读到的不是最新数据。

规避方案:在 CPU 读取前,使 D-Cache 失效(Invalidate),强制从内存重新加载。

配置步骤

  1. 启用 D-Cache(在系统初始化时调用 SCB_EnableDCache())。
  2. 定义 DMA 缓冲区,并确保其地址对齐(通常 32 字节对齐)。
  3. 在 DMA 传输完成中断中,调用 SCB_InvalidateDCache_by_Addr 使缓冲区对应的 Cache 行失效。

代码示例

// 缓冲区定义,32字节对齐
__ALIGN_BEGIN static uint32_t adc_buffer[64] __ALIGN_END;

// DMA 传输完成中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 使缓冲区对应的 Cache 行失效
    SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buffer, sizeof(adc_buffer));
    // 现在 CPU 可以安全读取 adc_buffer
    process_adc_data(adc_buffer);
}

注意事项

  • 失效操作必须在读取之前完成,且地址和大小需与缓冲区匹配。
  • 若缓冲区较大,建议使用双缓冲,避免在 DMA 传输期间 CPU 访问。

场景二:CPU 写内存,DMA 读取(如发送数据到 DAC 或 UART)

问题描述:CPU 准备数据并写入内存缓冲区,然后启动 DMA 将数据传输到外设。若 CPU 写入的数据仍在 Cache 中未写回内存,DMA 会读取到旧数据。

规避方案:在启动 DMA 前,将缓冲区对应的 Cache 行清理(Clean),强制写回内存。

配置步骤

  1. 在 CPU 完成数据写入后,调用 SCB_CleanDCache_by_Addr
  2. 确保清理操作在 DMA 启动之前完成。
  3. 若缓冲区被复用,考虑在 DMA 完成后再次清理或失效。

代码示例

// 发送缓冲区
__ALIGN_BEGIN static uint8_t tx_buffer[128] __ALIGN_END;

void send_data_via_dma(uint8_t* data, uint32_t len) {
    // 复制数据到缓冲区
    memcpy(tx_buffer, data, len);
    // 清理 Cache,确保数据写回内存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, len);
    // 启动 DMA 传输
    HAL_UART_Transmit_DMA(&huart, tx_buffer, len);
}

注意事项

  • 清理操作会消耗 CPU 周期,对于高频传输需评估性能。
  • 若使用 DMA 循环模式,需确保缓冲区大小是 Cache 行大小的整数倍。

场景三:外设寄存器与内存交互(如以太网 DMA 描述符)

问题描述:某些外设(如以太网 MAC)使用 DMA 描述符管理数据传输,描述符位于内存中,由 CPU 和 DMA 共同访问。若描述符被 Cache 缓存,可能导致状态同步失败。

规避方案:将描述符所在内存区域配置为“不可缓存”(Non-cacheable),或使用 MPU 设置内存属性。

配置步骤

  1. 使用 MPU 将描述符区域配置为 DeviceStrongly-ordered 属性,禁用 Cache。
  2. 或者,使用 SCB_CleanDCacheSCB_InvalidateDCache 手动维护,但效率较低。
  3. 推荐使用 MPU 配置,因为外设描述符通常较小且访问频繁。

代码示例(MPU 配置):

void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    HAL_MPU_Disable();
    
    // 配置描述符区域(例如地址 0x20001000,大小 1KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20001000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_1KB;
    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);
    
    HAL_MPU_Enable(MPU_CONTROL_PRIVILEGED_DEFAULT);
}

// 在 main 中调用 MPU_Config() 后再启用 D-Cache

注意事项

  • MPU 配置需在启用 D-Cache 之前完成,否则可能无效。
  • 不可缓存区域访问速度较慢,但保证一致性,适用于描述符等关键数据。

总结与最佳实践

  • 明确数据流向:根据 CPU 和 DMA 的访问方向,选择清理或失效操作。
  • 使用 MPU 隔离关键区域:对于高频访问的描述符或共享缓冲区,配置为不可缓存,简化维护。
  • 双缓冲设计:在 DMA 传输期间,CPU 处理另一缓冲区,避免等待,同时减少一致性操作频率。
  • 注意对齐:确保缓冲区地址和大小对齐到 Cache 行(通常 32 字节),否则操作可能无效。
  • 性能权衡:清理和失效操作有开销,需在实时性和性能间平衡。

通过以上方案,开发者可以有效规避 D-Cache 与 DMA 的数据一致性问题,确保系统稳定运行。在实际项目中,建议结合具体外设和内存布局,选择最合适的策略。