STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 顺序与实测踩坑
STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,启用 D-Cache 后,DMA 传输的数据一致性成为许多开发者的噩梦。本文将系统讲解 D-Cache 与 DMA 的冲突原理、地址对齐要求、Clean/Invalidate 的正确顺序,并给出完整代码与实测避坑指南。
1. 为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据可能只存在于 Cache 中,并未写回 SRAM。而 DMA 直接访问物理内存,若此时 Cache 中有脏数据(Dirty),DMA 读到的是旧数据;反之,DMA 更新了内存,但 Cache 中仍是旧副本,CPU 读到的也是旧数据。
- 写回:CPU 写数据只更新 Cache,不立即写内存。
- 写分配:CPU 写未命中时,先加载整个 Cache Line 到 Cache,再修改。
- Cache Line:STM32H7 的 D-Cache 行大小为 32 字节。
因此,在 DMA 传输前后,必须手动维护 Cache 一致性。
2. 地址对齐:32 字节的硬性要求
由于 Cache Line 为 32 字节,所有 DMA 缓冲区必须按 32 字节对齐,且大小最好是 32 的整数倍。否则,Clean/Invalidate 操作可能误伤相邻数据。
-
对齐声明:使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。 - 大小对齐:缓冲区长度建议向上取整到 32 的倍数。
- 避免栈上变量:局部变量地址不可控,务必使用全局或静态数组。
// 正确示例:32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];
3. Clean 与 Invalidate 的顺序
根据 DMA 传输方向,操作顺序截然不同:
3.1 CPU 发送数据(Memory -> Peripheral)
- CPU 填充缓冲区。
- Clean D-Cache:将 Cache 中的脏数据写回内存。
- 启动 DMA 发送。
注意:Clean 后不要 Invalidate,否则可能丢失尚未写回的数据。
3.2 CPU 接收数据(Peripheral -> Memory)
- Invalidate D-Cache:丢弃 Cache 中的旧数据,强制 CPU 后续从内存读取。
- 启动 DMA 接收。
- DMA 完成后,CPU 读取数据(此时 Cache 未命中,从内存加载新数据)。
注意:Invalidate 必须在 DMA 启动前执行,且要确保没有脏数据未写回。
3.3 双向传输
先 Clean 再 Invalidate 是危险的,因为 Invalidate 会丢弃 Clean 后可能残留的脏数据。正确做法:先 Clean,再 Invalidate,但需确保 Clean 后没有 CPU 写操作。通常建议分开处理。
4. 完整代码示例
以下代码基于 STM32H7 HAL 库,演示 UART DMA 发送与接收的 Cache 维护。
#include "stm32h7xx_hal.h"
#define DMA_BUFFER_SIZE 256
__attribute__((aligned(32))) uint8_t tx_buffer[DMA_BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[DMA_BUFFER_SIZE];
// 清理 D-Cache(写回)
void clean_dcache(void *addr, uint32_t size) {
uint32_t start = (uint32_t)addr & ~0x1F; // 向下对齐到 32 字节
uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
SCB_CleanDCache_by_Addr((uint32_t *)start, end - start);
}
// 无效化 D-Cache
void invalidate_dcache(void *addr, uint32_t size) {
uint32_t start = (uint32_t)addr & ~0x1F;
uint32_t end = ((uint32_t)addr + size + 0x1F) & ~0x1F;
SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start);
}
// UART DMA 发送
void uart_send_dma(uint8_t *data, uint16_t len) {
memcpy(tx_buffer, data, len);
clean_dcache(tx_buffer, len); // 写回 Cache
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}
// UART DMA 接收(启动前无效化)
void uart_receive_dma_start(void) {
invalidate_dcache(rx_buffer, DMA_BUFFER_SIZE); // 丢弃旧数据
HAL_UART_Receive_DMA(&huart1, rx_buffer, DMA_BUFFER_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 此时 rx_buffer 中已是 DMA 写入的新数据,但 Cache 中可能还有旧副本
// 需要再次无效化,确保 CPU 读取到最新数据
invalidate_dcache(rx_buffer, DMA_BUFFER_SIZE);
// 处理数据...
}
5. 实测踩坑与注意事项
-
坑1:忘记对齐导致数据错乱。缓冲区未对齐时,Clean/Invalidate 会波及相邻变量,引发随机错误。务必使用
aligned(32)。 - 坑2:Invalidate 前未 Clean。如果 Cache 中有脏数据,Invalidate 会直接丢弃,导致数据丢失。接收场景下,若 CPU 曾写过该缓冲区,必须先 Clean。
- 坑3:DMA 传输中 CPU 访问缓冲区。DMA 进行时,CPU 若读写同一缓冲区,会破坏一致性。应使用双缓冲或等待传输完成。
- 坑4:MPU 配置不当。可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,但会牺牲性能。推荐使用手动维护方式。
-
坑5:中断中调用 Cache 维护函数。
SCB_CleanDCache_by_Addr等函数执行时间较长,在高速中断中可能影响实时性,建议在任务级处理。
6. 总结
STM32H7 的 D-Cache 与 DMA 一致性并非无解,关键在于:32 字节对齐、正确的 Clean/Invalidate 顺序、以及传输前后的及时维护。掌握这些要点后,你就能在享受 D-Cache 高性能的同时,确保 DMA 数据传输的绝对可靠。建议在项目初期就建立统一的 Cache 维护接口,避免后期调试的痛苦。