# 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` 中): ```c SCB_EnableDCache(); ``` 2. **定义描述符和缓冲区**(建议使用 `__attribute__((aligned(32)))`): ```c 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 看到最新值: ```c SCB_CleanDCache_by_Addr((uint32_t *)DMARxDscrTab, sizeof(DMARxDscrTab)); SCB_CleanDCache_by_Addr((uint32_t *)DMATxDscrTab, sizeof(DMATxDscrTab)); ``` 4. **接收数据时**,DMA 写入缓冲区后,CPU 读取前需 Invalidate: ```c SCB_InvalidateDCache_by_Addr((uint32_t *)RxBuff[idx], ETH_RX_BUF_SIZE); // 然后处理数据 ``` 5. **发送数据时**,CPU 写入缓冲区后,DMA 读取前需 Clean: ```c SCB_CleanDCache_by_Addr((uint32_t *)TxBuff[idx], len); // 然后触发 DMA 发送 ``` ## 完整代码示例(基于 HAL 库) 以下是以太网收发核心片段,展示一致性操作的正确位置。 ```c // 接收中断回调或轮询中 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。描述符和缓冲区都要覆盖。遵循本文的步骤和代码,可有效避免数据错乱,提升系统稳定性。