STM32F4 D-Cache 与 DMA 一致性实战:描述符与缓冲区的坑与解

为什么 D-Cache 会破坏 DMA 传输?

STM32F4 系列(Cortex-M4)的 D-Cache 是 CPU 与内存之间的高速缓存。当 DMA 直接访问内存(如 SRAM)时,它绕过 CPU 的 Cache。若 CPU 先写数据到缓冲区(Cache 中),DMA 读取时可能拿到旧数据(Cache 未回写);反之,DMA 写入内存后,CPU 读取时可能命中 Cache 中的旧数据(未失效)。

对于 DMA 描述符(如 STM32 的 DMA2D、以太网 DMA 描述符),同样存在此问题。描述符由 CPU 维护,DMA 硬件读取;若描述符在 Cache 中未回写,DMA 会读到错误内容,导致传输异常。

核心原理:Cache 一致性操作

解决思路是手动维护一致性,常用两个 CMSIS 函数:

  • SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将指定地址的 Cache 行回写到内存(CPU 写后,DMA 读前)。
  • SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):使指定地址的 Cache 行失效(DMA 写后,CPU 读前)。

注意:地址需 32 字节对齐(Cache line 大小),长度需向上取整到 32 的倍数。

典型场景:以太网 DMA 描述符

以 STM32F429 的以太网 MAC 为例,DMA 描述符数组和收发缓冲区通常放在 SRAM。启用 D-Cache 后,必须对描述符和缓冲区都做一致性处理。

配置步骤

  1. 开启 D-Cache(在 main 中):
    SCB_EnableDCache();
    
  2. 定义描述符和缓冲区(建议使用 __attribute__((aligned(32)))):
    ETH_DMADescTypeDef  DMARxDscrTab[ETH_RXBUFNB] __attribute__((aligned(32)));
    ETH_DMADescTypeDef  DMATxDscrTab[ETH_TXBUFNB] __attribute__((aligned(32)));
    uint8_t RxBuff[ETH_RXBUFNB][ETH_RX_BUF_SIZE] __attribute__((aligned(32)));
    uint8_t TxBuff[ETH_TXBUFNB][ETH_TX_BUF_SIZE] __attribute__((aligned(32)));
    
  3. 初始化描述符后,必须 Clean 描述符区域,确保 DMA 看到最新值:
    SCB_CleanDCache_by_Addr((uint32_t *)DMARxDscrTab, sizeof(DMARxDscrTab));
    SCB_CleanDCache_by_Addr((uint32_t *)DMATxDscrTab, sizeof(DMATxDscrTab));
    
  4. 接收数据时,DMA 写入缓冲区后,CPU 读取前需 Invalidate:
    SCB_InvalidateDCache_by_Addr((uint32_t *)RxBuff[idx], ETH_RX_BUF_SIZE);
    // 然后处理数据
    
  5. 发送数据时,CPU 写入缓冲区后,DMA 读取前需 Clean:
    SCB_CleanDCache_by_Addr((uint32_t *)TxBuff[idx], len);
    // 然后触发 DMA 发送
    

完整代码示例(基于 HAL 库)

以下是以太网收发核心片段,展示一致性操作的正确位置。

// 接收中断回调或轮询中
void ETH_RxTask(void) {
    for (int i = 0; i < ETH_RXBUFNB; i++) {
        if (RxDesc[i].Status & ETH_DMARXDESC_OWN) break; // DMA 仍占用
        // 使缓冲区无效,确保读到 DMA 写入的最新数据
        SCB_InvalidateDCache_by_Addr((uint32_t *)RxBuff[i], ETH_RX_BUF_SIZE);
        // 处理数据(例如拷贝或解析)
        process_packet(RxBuff[i], RxDesc[i].ControlBufferSize & ETH_DMARXDESC_FL);
        // 重新初始化描述符后,Clean 描述符,让 DMA 看到更新
        RxDesc[i].Status = ETH_DMARXDESC_OWN;
        SCB_CleanDCache_by_Addr((uint32_t *)&RxDesc[i], sizeof(ETH_DMADescTypeDef));
    }
}

void ETH_TxTask(uint8_t *data, uint16_t len) {
    // 假设使用 TxBuff[0]
    memcpy(TxBuff[0], data, len);
    // Clean 缓冲区,确保 DMA 能读到
    SCB_CleanDCache_by_Addr((uint32_t *)TxBuff[0], len);
    // 配置描述符,并 Clean 描述符
    TxDesc[0].Buffer1Addr = (uint32_t)TxBuff[0];
    TxDesc[0].ControlBufferSize = len;
    TxDesc[0].Status |= ETH_DMATXDESC_OWN;
    SCB_CleanDCache_by_Addr((uint32_t *)&TxDesc[0], sizeof(ETH_DMADescTypeDef));
    // 触发 DMA 发送(如写 ETH->DMASR 等)
}

注意事项与常见陷阱

  • 对齐与长度SCB_CleanDCache_by_Addr 要求地址 32 字节对齐,长度向上取整到 32 的倍数。若缓冲区未对齐,可用 __attribute__((aligned(32))) 强制,或使用 SCB_CleanDCache()(全 Clean,但性能差)。
  • 描述符数组:整个描述符表在初始化后必须 Clean 一次,但每次修改单个描述符后,只需 Clean 该描述符(注意对齐)。
  • 不要用 volatile 替代volatile 不能解决 Cache 一致性问题,它只防止编译器优化,硬件 Cache 仍可能命中旧数据。
  • 性能权衡:频繁 Clean/Invalidate 会降低性能,可考虑将 DMA 缓冲区放在不缓存的区域(如 DTCM RAM,但 STM32F4 的 DTCM 不支持 DMA),或使用 MPU 配置为非缓存(但 F4 的 MPU 配置较复杂,不推荐新手)。
  • 调试技巧:若出现随机性数据错误,先关闭 D-Cache 测试,若正常则基本确定是 Cache 一致性问题。

总结

STM32F4 启用 D-Cache 后,DMA 一致性是必须处理的细节。核心原则:CPU 写后、DMA 读前 Clean;DMA 写后、CPU 读前 Invalidate。描述符和缓冲区都要覆盖。遵循本文的步骤和代码,可有效避免数据错乱,提升系统稳定性。