STM32H7 的 DCache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 时机与踩坑复盘
STM32H7 系列凭借 Cortex-M7 内核和 L1 Cache 实现了高主频下的零等待执行,但 DCache 的引入也让 DMA 传输变得“危机四伏”。如果你曾遇到 DMA 发送的数据错乱、接收数据被覆盖,或程序在开启 DCache 后莫名 HardFault,那大概率是踩中了缓存一致性的坑。本文从原理到实践,帮你彻底理清。
一、为什么 DCache 会与 DMA 冲突?
Cortex-M7 的 L1 DCache 是**写回(Write-Back)**策略:CPU 写数据时只更新 Cache,不立即写回 SRAM;读数据时若 Cache 未命中,则从 SRAM 加载并缓存。
DMA 则直接访问 SRAM(物理内存),不经过 Cache。这就导致:
- CPU 写 → DMA 读:数据可能还在 Cache 中,DMA 读到的是 SRAM 里的旧值。
- DMA 写 → CPU 读:DMA 更新了 SRAM,但 CPU 读到的却是 Cache 中的旧数据。
解决思路只有两个:Clean(清理) 和 Invalidate(无效化)。
- Clean:将 Cache 中已修改的数据写回 SRAM。
- Invalidate:将 Cache 中的对应行标记为无效,强制下次读取时从 SRAM 重新加载。
二、地址对齐:32 字节的硬性要求
Cortex-M7 的 Cache Line 为 32 字节。Clean/Invalidate 操作以 Cache Line 为单位,因此操作地址必须 32 字节对齐,长度也建议为 32 字节的整数倍。
若缓冲区未对齐,Clean 或 Invalidate 会波及相邻数据,导致“误伤”。例如,一个 20 字节的缓冲区位于地址 0x20000010,Invalidate 时会覆盖 0x20000000~0x2000001F 整个 Cache Line,可能丢弃相邻变量的最新值。
解决方案:
- 使用
__attribute__((aligned(32)))强制 32 字节对齐。 - 缓冲区大小向上取整到 32 的倍数。
- 或者使用 MPU 将 DMA 缓冲区配置为 Write-Through/Non-Cacheable,但会牺牲性能。
// 推荐:32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
三、Clean/Invalidate 的正确时机
1. CPU 写 → DMA 读(发送方向)
在启动 DMA 传输之前,对发送缓冲区执行 Clean 操作,确保数据已写回 SRAM。
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, sizeof(dma_tx_buf));
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf, (uint32_t)&periph, len);
2. DMA 写 → CPU 读(接收方向)
在 DMA 传输完成之后,对接收缓冲区执行 Invalidate 操作,丢弃 Cache 中的旧数据。
// 在 DMA 完成中断或轮询等待完成后
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
// 此时 CPU 读取 dma_rx_buf 才能得到 DMA 写入的新数据
3. 双向传输(如 SPI 全双工)
发送前 Clean TX 缓冲区,接收完成后 Invalidate RX 缓冲区。注意:若 TX 和 RX 是同一缓冲区,需先 Clean 再启动,完成后 Invalidate。
四、完整代码示例(以 UART DMA 接收为例)
#include "stm32h7xx.h"
#define RX_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t uart_rx_buf[RX_BUF_SIZE];
void uart_dma_init(void) {
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, RX_BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 关键:Invalidate 接收缓冲区,使 CPU 能读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, RX_BUF_SIZE);
// 此时可以安全处理 uart_rx_buf 中的数据
process_data(uart_rx_buf, RX_BUF_SIZE);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, RX_BUF_SIZE);
}
}
// 发送函数
void uart_send_dma(uint8_t *data, uint16_t len) {
// 确保长度和地址对齐(简化处理,实际应确保缓冲区对齐)
SCB_CleanDCache_by_Addr((uint32_t *)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
五、踩坑复盘与注意事项
- 坑 1:忘记 Invalidate 导致接收数据“不变”。DMA 明明收到了新数据,但 CPU 读到的还是上一次的旧值。原因就是 Cache 未失效。
-
坑 2:缓冲区未对齐导致相邻变量被破坏。Invalidate 操作波及相邻 Cache Line,把其他变量的最新值丢弃。务必使用
aligned(32)。 - 坑 3:在 DMA 传输过程中 Invalidate。若 DMA 正在写缓冲区,此时 Invalidate 可能丢弃已写入的部分数据。必须在 DMA 完成后再操作。
- 坑 4:Clean 和 Invalidate 顺序颠倒。发送前应 Clean,接收后应 Invalidate,切勿混淆。
-
坑 5:使用
SCB_InvalidateDCache()全量失效。这会清空整个 DCache,严重影响性能,且可能影响其他任务。应使用SCB_InvalidateDCache_by_Addr()按地址操作。 -
注意:若使用 STM32CubeMX 生成的代码,默认可能未开启 DCache。需在
main()中调用SCB_EnableDCache(),并确保 MPU 配置正确。 - 替代方案:对于频繁小数据量传输,可将 DMA 缓冲区配置为 Non-Cacheable(通过 MPU),避免手动维护一致性,但会降低 CPU 访问速度。
六、总结
STM32H7 的 DCache 与 DMA 共存的核心就是对齐、Clean、Invalidate三要素。牢记:
- 缓冲区 32 字节对齐;
- 发送前 Clean,接收后 Invalidate;
- 操作地址和长度按 Cache Line 对齐;
- 在 DMA 完成后再 Invalidate。
掌握这些,你就能在 H7 上既享受 Cache 的性能,又保证 DMA 的数据一致性。