STM32F4 D-Cache 与 DMA 一致性实战:描述符与缓冲区的坑与解
👁 1 阅读 · 2026-08-27 · 嵌入式
在 STM32F4 系列(如 F429、F407)上启用 D-Cache 后,DMA 传输数据与 CPU 缓存之间的一致性成为嵌入式开发中的经典难题。本文深入剖析 DMA 描述符和缓冲区在 D-Cache 下的失效场景,提供基于 CMSIS 和 HAL 的完整解决方案,涵盖原理、配置步骤、代码示例及常见陷阱,帮助开发者彻底告别随机性数据错乱。
# 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。描述符和缓冲区都要覆盖。遵循本文的步骤和代码,可有效避免数据错乱,提升系统稳定性。