STM32H7 的 D-Cache 与 DMA 一致性维护:按 cache line 对齐的收发缓冲区设计
STM32H7 系列凭借 Cortex-M7 内核、480MHz 主频和 16KB D-Cache,成为高性能嵌入式应用的首选。然而,开启 D-Cache 后,DMA 与 CPU 之间的数据一致性问题会频繁导致“数据错乱”“接收丢包”等诡异现象。本文从原理到实践,带你彻底解决这一痛点。
一、为什么 DMA 与 D-Cache 会“打架”?
Cortex-M7 的 D-Cache 是写回(write-back)+ 写分配(write-allocate) 策略。CPU 访问内存时,数据先被加载到 Cache,修改后不会立即写回主存。而 DMA 直接访问物理内存(SRAM),不经过 Cache。
- 发送方向:CPU 写好缓冲区 → 数据可能还在 Cache 中 → DMA 从 SRAM 读到旧数据。
- 接收方向:DMA 把新数据写入 SRAM → CPU 读到的却是 Cache 中的旧数据。
解决思路只有两条:要么让 DMA 也走 Cache(不可行),要么在 DMA 传输前后手动维护 Cache 一致性。
二、Cache Line 对齐:一切维护的前提
Cortex-M7 的 D-Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 等函数按地址范围操作,但硬件实际以 cache line 为单位。
如果缓冲区起始地址或长度不是 32 字节对齐,维护操作可能“误伤”相邻数据,导致其他变量被清空或写回。因此,DMA 缓冲区必须按 32 字节对齐,且长度建议为 32 的整数倍。
// 推荐:使用编译器属性强制对齐
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
若使用动态分配,可用 memalign(32, size) 或 aligned_alloc(32, size)。
三、发送与接收的 Cache 维护流程
发送(CPU → DMA)
- CPU 填充缓冲区。
- Clean(写回)缓冲区,确保数据到达 SRAM。
- 启动 DMA 发送。
- 等待传输完成。
接收(DMA → CPU)
- Invalidate(无效化)缓冲区,丢弃 Cache 中的旧数据。
- 启动 DMA 接收。
- 等待传输完成。
- 再次 Invalidate,确保 CPU 读取到 DMA 写入的新数据。
注意:Invalidate 操作会丢弃未写回的数据,因此接收缓冲区在 Invalidate 前不应有 CPU 未写回的内容。
四、完整代码示例(基于 HAL 库)
以下代码以 UART DMA 收发为例,展示完整的维护流程。
#include "stm32h7xx_hal.h"
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[DMA_BUF_SIZE];
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
// 发送函数
void uart_dma_send(uint8_t *data, uint16_t len) {
// 1. 拷贝数据到对齐缓冲区(若 data 未对齐)
memcpy(tx_buf, data, len);
// 2. Clean D-Cache,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
// 接收启动函数
void uart_dma_start_receive(uint16_t len) {
// 1. Invalidate D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
// 2. 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buf, len);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1) {
// 再次 Invalidate,确保 CPU 读到新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);
// 处理数据...
process_data(rx_buf, DMA_BUF_SIZE);
// 重新启动接收
uart_dma_start_receive(DMA_BUF_SIZE);
}
}
五、关键注意事项与常见陷阱
- 对齐是硬性要求:缓冲区地址和长度都应为 32 字节的整数倍。若长度不是 32 的倍数,维护操作会覆盖相邻内存。
- Clean 与 Invalidate 的顺序:发送前 Clean,接收前 Invalidate,接收后再次 Invalidate。顺序错误会导致数据丢失或读到旧值。
- 避免在中断中频繁维护:Cache 维护操作有一定开销,高频率中断中应谨慎使用。
- MPU 配置:可将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会牺牲部分性能。
-
调试技巧:若怀疑 Cache 问题,可临时关闭 D-Cache 验证;使用
SCB_InvalidateDCache()全局无效化(慎用)辅助定位。 - 多缓冲区场景:若使用多个缓冲区,每个缓冲区都应独立对齐,并分别维护。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性维护并不复杂,核心就是 32 字节对齐 + 正确的 Clean/Invalidate 时序。按照本文的缓冲区设计模式,你可以安全地在 H7 上发挥 DMA 与 Cache 的全部性能。记住:对齐是基础,时序是关键,测试是保障。
延伸阅读:STM32H7 参考手册中关于 Cortex-M7 Cache 的章节,以及 ARM 官方
SCB_CleanDCache_by_Addr等 CMSIS 函数说明。