STM32F4 D-Cache与DMA描述符缓存一致性维护的三种正确姿势

1. 问题根源:D-Cache与DMA的“盲区”

STM32F4系列(如STM32F407、STM32F429)内置了D-Cache(数据缓存),用于加速CPU对内存的访问。然而,DMA控制器直接访问物理内存(SRAM或SDRAM),不经过D-Cache。这导致两个经典问题:

  • CPU写,DMA读:CPU更新描述符后,数据可能还滞留在D-Cache中,DMA从内存读取到的是旧数据。
  • DMA写,CPU读:DMA更新描述符后,内存中的数据是最新的,但CPU读取时可能命中D-Cache中的旧缓存行,导致读到过期数据。

对于DMA描述符(如以太网DMA描述符、SDMMC描述符),这种不一致会直接导致传输错误或系统挂起。

2. 方案一:全Cache无效化(简单粗暴,适合低频操作)

原理

在每次DMA传输前,将整个D-Cache无效化(Invalidate),强制CPU从内存重新读取。在传输完成后,同样执行无效化,确保DMA写入的数据对CPU可见。

配置步骤

  1. 启用D-Cache(在启动代码或主函数中)。
  2. 在DMA操作前后调用SCB_InvalidateDCache()

代码示例

// 启用D-Cache
SCB_EnableDCache();

// 以太网DMA描述符示例
ETH_DMADescTypeDef *pDesc = &tx_desc[0];

// 准备描述符(CPU写)
pDesc->tdes0 = ...; // 设置控制字
pDesc->tdes1 = ...; // 设置缓冲区地址

// 关键:使整个D-Cache无效化,确保DMA看到最新描述符
SCB_InvalidateDCache();

// 启动DMA传输
HAL_ETH_Transmit(&heth, pDesc, 1);

// 等待传输完成,再次无效化,使CPU看到DMA更新的状态位
while (HAL_ETH_GetTxDataFreeEntries(&heth) == 0);
SCB_InvalidateDCache();

注意事项

  • 性能损失大:每次操作都冲刷整个Cache,不适合高频DMA场景。
  • 简单可靠:适用于描述符数量少、操作频率低的场景(如低速串口)。

3. 方案二:MPU配置描述符区域为非缓存(推荐,性能与安全兼顾)

原理

通过MPU(内存保护单元)将DMA描述符所在的SRAM区域设置为非缓存(Non-cacheable)。这样CPU访问该区域时直接读写物理内存,绕过D-Cache,从根本上避免一致性问题。

配置步骤

  1. 确定描述符区域地址和大小(需对齐到32字节)。
  2. 配置MPU区域,设置属性为MPU_REGION_NO_CACHE
  3. 使能MPU。

代码示例

// 假设描述符位于SRAM1,起始地址0x20000000,大小4KB
#define DESC_REGION_BASE  0x20000000
#define DESC_REGION_SIZE  (4 * 1024)

void MPU_Config_NonCacheable(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

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

    // 配置区域0:描述符区域为非缓存
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = DESC_REGION_BASE;
    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.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);
}

// 在主函数中调用
int main(void)
{
    HAL_Init();
    MPU_Config_NonCacheable();
    // ... 其他初始化
}

注意事项

  • 描述符区域必须与MPU区域对齐(通常为32字节)。
  • 非缓存区域的访问速度略低于缓存区域,但描述符访问频率低,影响可忽略。
  • 此方案无需在每次DMA操作时手动维护缓存,代码简洁,性能稳定。

4. 方案三:手动Clean/Invalidate操作(精细控制,适合高频DMA)

原理

仅对描述符所在的缓存行执行Clean(写回)或Invalidate(无效化)操作,而不是整个Cache。使用CMSIS提供的函数:SCB_CleanDCache_by_Addr()SCB_InvalidateDCache_by_Addr()

配置步骤

  1. 确保描述符地址按32字节对齐(缓存行大小)。
  2. CPU写描述符后,调用Clean函数将数据写回内存。
  3. DMA更新描述符后,调用Invalidate函数使CPU缓存失效。

代码示例

// 描述符结构体(假设32字节对齐)
__ALIGNED(32) ETH_DMADescTypeDef tx_desc[2];

// CPU准备描述符
void Prepare_Desc(ETH_DMADescTypeDef *desc)
{
    desc->tdes0 = ...;
    desc->tdes1 = ...;
    // 写完后Clean该描述符对应的缓存行
    SCB_CleanDCache_by_Addr((uint32_t *)desc, sizeof(*desc));
}

// DMA完成后,读取描述符状态
void Check_Desc(ETH_DMADescTypeDef *desc)
{
    // 先Invalidate,确保读到最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)desc, sizeof(*desc));
    if (desc->tdes0 & ETH_DMATXDESC_OWN) {
        // 处理完成
    }
}

// 使用示例
Prepare_Desc(&tx_desc[0]);
HAL_ETH_Transmit(&heth, &tx_desc[0], 1);
// 等待中断或轮询
Check_Desc(&tx_desc[0]);

注意事项

  • 地址必须32字节对齐,否则函数可能无效或触发异常。
  • 需要精确计算描述符大小,确保覆盖整个缓存行。
  • 适合高频DMA场景,性能最佳,但代码复杂度稍高。

5. 总结与选型建议

| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 全Cache无效化 | 简单,无需MPU配置 | 性能差,影响实时性 | 低频DMA,调试阶段 | | MPU非缓存区域 | 性能好,代码简洁 | 需要MPU资源,区域管理 | 大多数生产项目,推荐 | | 手动Clean/Invalidate | 性能最优,精细控制 | 代码复杂,易出错 | 高频DMA,性能敏感 |

核心建议:在STM32F4项目中,优先考虑使用MPU将描述符区域配置为非缓存,这是最平衡的方案。若DMA频率极高且描述符数量多,则采用手动Clean/Invalidate。全Cache无效化仅作为临时解决方案。

6. 常见坑点

  • 描述符对齐:无论哪种方案,描述符结构体建议使用__ALIGNED(32),避免跨缓存行。
  • MPU区域重叠:配置MPU时,确保描述符区域不与代码或数据区域重叠,否则可能引发总线错误。
  • 中断上下文:在中断中操作描述符时,同样需要维护缓存一致性,不可忽略。
  • 调试工具:使用调试器查看内存时,注意关闭缓存或手动刷新,否则看到的数据可能不真实。

掌握以上三种姿势,你就能在STM32F4上从容应对D-Cache与DMA描述符的缓存一致性问题,让系统稳定运行在高速DMA场景下。