引言

在 STM32F4 系列(如 STM32F407、STM32F429)中,D-Cache 的引入大幅提升了 CPU 访问外部 SDRAM 或 Flash 的速度,但同时也带来了新的挑战:当 DMA 控制器直接访问内存时,如果 CPU 已经将数据缓存(Dirty Cache),DMA 读到的可能是过时数据;反之,DMA 写入内存后,CPU 可能从缓存中读到旧值。这种缓存一致性问题在 DMA 描述符(如以太网 DMA 描述符、SDIO 描述符)上尤为突出,因为描述符由 CPU 和 DMA 频繁交替读写。

本文将介绍三种实用的维护策略,帮助你在 STM32F4 项目中正确处理 D-Cache 与 DMA 描述符的一致性。

问题根源

STM32F4 的 D-Cache 是写回(Write-back)模式,即 CPU 写操作先更新缓存,仅当缓存行被替换或显式 Clean 时才写回内存。DMA 访问内存时绕过缓存,直接读写物理地址。因此,当 CPU 修改描述符后,若未及时写回,DMA 会读取到旧值;当 DMA 更新描述符后,若 CPU 缓存中仍保留旧行,CPU 会忽略 DMA 的修改。

策略一:全缓存刷新(简单粗暴)

原理

在每次 DMA 操作前后,调用 SCB_CleanDCache() 和 SCB_InvalidateDCache() 将整个 D-Cache 写回或失效。此方法实现简单,但性能开销大,仅适用于描述符操作频率低或对性能不敏感的场景。

配置步骤

  1. 在系统初始化时启用 D-Cache:
    SCB_EnableDCache();
    
  2. 在 DMA 描述符修改前,调用 CleanDCache;在 DMA 操作完成后,调用 InvalidateDCache。

代码示例

// 以太网发送描述符更新
void ETH_DMA_TxDesc_Write(ETH_DMADescTypeDef *desc, uint32_t data) {
    // 修改描述符字段
    desc->DESC0 = data;
    // 写回整个缓存,确保描述符写入内存
    SCB_CleanDCache();
    // 启动 DMA 传输...
}

// 以太网接收描述符处理
void ETH_DMA_RxDesc_Read(ETH_DMADescTypeDef *desc) {
    // 使缓存失效,确保读取到 DMA 写入的最新数据
    SCB_InvalidateDCache();
    uint32_t status = desc->DESC0;
    // 处理数据...
}

注意事项

  • 全缓存刷新会清空所有缓存行,导致后续 CPU 访问性能下降。
  • 在多任务或中断环境中,频繁刷新可能引入不确定性。

策略二:描述符区域配置为非缓存(MPU)

原理

利用 MPU(Memory Protection Unit)将 DMA 描述符所在的 RAM 区域设置为非缓存(Non-cacheable),这样 CPU 和 DMA 都直接访问物理内存,彻底避免一致性问题。此方法性能最佳,但需要合理划分内存区域。

配置步骤

  1. 在链接脚本中为描述符分配独立内存段(例如 .dma_desc)。
  2. 初始化 MPU,配置该区域为 Normal memory, Non-cacheable。
  3. 确保描述符地址对齐到 32 字节(MPU 区域大小要求)。

代码示例

// 定义描述符数组,放在独立段
__attribute__((section(".dma_desc"))) ETH_DMADescTypeDef tx_desc[ETH_TX_DESC_CNT];

// MPU 配置函数
void MPU_Config(void) {
    MPU_Region_InitTypeDef MPU_InitStruct;
    HAL_MPU_Disable();

    // 配置描述符区域(假设地址 0x20000000,大小 1KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20000000;
    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_HRNDM);
}

// 主函数中调用
int main(void) {
    HAL_Init();
    MPU_Config();
    SCB_EnableDCache();
    // 之后对描述符的读写无需任何缓存维护操作
}

注意事项

  • MPU 区域大小必须是 2 的幂次方,且起始地址对齐。
  • 非缓存区域访问速度稍慢,但描述符访问频率低,影响可忽略。
  • 需要确保描述符区域不与普通数据混用,否则可能导致性能下降。

策略三:手动 Clean/Invalidate 描述符行

原理

针对单个描述符或描述符所在的缓存行(通常 32 字节)执行 Clean 或 Invalidate 操作,只维护必要的数据,避免全缓存刷新。此方法在性能和复杂度之间取得平衡,适用于描述符较多但操作不频繁的场景。

配置步骤

  1. 启用 D-Cache。
  2. 在修改描述符前,调用 SCB_CleanDCache_by_Addr() 将描述符地址写回。
  3. 在读取 DMA 更新的描述符前,调用 SCB_InvalidateDCache_by_Addr() 使对应缓存行失效。

代码示例

// 描述符地址对齐到 32 字节
#define CACHE_LINE_SIZE 32

// 写描述符前 Clean
void Desc_PrepareWrite(ETH_DMADescTypeDef *desc) {
    SCB_CleanDCache_by_Addr((uint32_t*)desc, sizeof(ETH_DMADescTypeDef));
}

// 读描述符前 Invalidate
void Desc_PrepareRead(ETH_DMADescTypeDef *desc) {
    SCB_InvalidateDCache_by_Addr((uint32_t*)desc, sizeof(ETH_DMADescTypeDef));
}

// 使用示例
void UpdateTxDesc(ETH_DMADescTypeDef *desc, uint32_t buf_addr) {
    desc->DESC2 = buf_addr;
    Desc_PrepareWrite(desc);  // 确保写入内存
    // 启动 DMA...
}

void ProcessRxDesc(ETH_DMADescTypeDef *desc) {
    Desc_PrepareRead(desc);   // 获取 DMA 写入的最新值
    uint32_t len = desc->DESC1;
    // 处理数据...
}

注意事项

  • 地址必须 32 字节对齐,否则操作可能影响相邻数据。
  • 若描述符大小超过一个缓存行,需确保整个描述符都被覆盖。
  • 在多核或中断嵌套场景,需考虑操作顺序,避免竞态。

总结与选型建议

  • 策略一(全缓存刷新):代码简单,适合原型验证或低频操作,但性能损失大。
  • 策略二(MPU 非缓存):性能最佳,适合对实时性要求高的应用,但需要精心规划内存布局。
  • 策略三(手动行维护):灵活高效,适合大多数生产项目,但需注意对齐和操作顺序。

在实际项目中,建议优先考虑策略二,将描述符区域隔离为非缓存,同时保留其他数据区域的缓存加速。若无法使用 MPU,则采用策略三,并确保所有描述符操作都遵循 Clean/Invalidate 规范。

参考资料

  • STM32F4xx Reference Manual (RM0090)
  • ARM Cortex-M4 Programming Guide
  • STM32CubeF4 HAL 驱动文档