一、为什么 DMA 和 D-Cache 会打架?
STM32H7 的 Cortex-M7 内核最高运行在 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,如果命中 Cache,就直接从 Cache 读写,不会立即同步到 SRAM。
而 DMA 是“绕过 CPU”的搬运工,它直接访问 SRAM 或外设。当 CPU 把数据写入 Cache 但尚未回写到 SRAM 时,DMA 从 SRAM 读到的就是旧数据;反之,DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 里的旧副本,就会“看不到”新数据。这就是 数据一致性问题。
二、Cache 行与地址对齐:一切的基础
- Cache 行大小:STM32H7 的 D-Cache 行大小为 32 字节。Cache 的维护(Clean/Invalidate)以“行”为单位,而不是以字节为单位。
- 地址对齐:DMA 缓冲区建议按 32 字节对齐。如果缓冲区跨越两个 Cache 行,操作一行会影响相邻数据,极易误伤。
-
推荐做法:使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏定义缓冲区,并确保长度是 32 的整数倍(或至少不与其他变量共享 Cache 行)。
// 定义 32 字节对齐的 DMA 缓冲区
ALIGN_32BYTES(uint8_t dma_rx_buf[256]);
ALIGN_32BYTES(uint8_t dma_tx_buf[256]);
三、Clean 与 Invalidate:方向决定操作
- Clean(清理):将 Cache 中已修改的数据写回 SRAM。用于 CPU 写 → DMA 读 的场景(发送方向)。
- Invalidate(无效化):将 Cache 中的对应行标记为无效,下次 CPU 读取时会从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景(接收方向)。
- 注意:Invalidate 会丢弃 Cache 中尚未回写的数据!如果 CPU 刚写过该缓冲区,必须先 Clean 再 Invalidate,否则数据丢失。
四、实战配置步骤(以 UART DMA 接收为例)
4.1 初始化阶段
- 使能 D-Cache(通常在
SystemInit中完成)。 - 将 DMA 缓冲区放在 非缓存区域 或使用 MPU 配置 为 Write-Through/Non-Cacheable,可一劳永逸。但本文重点讲手动维护方法。
4.2 发送数据(CPU 写 → DMA 读)
// 1. 填充数据到 dma_tx_buf
memcpy(dma_tx_buf, src, len);
// 2. Clean 操作,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
4.3 接收数据(DMA 写 → CPU 读)
// 1. 启动 DMA 接收前,先 Invalidate 缓冲区,避免 Cache 中有旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
HAL_UART_Receive_DMA(&huart1, dma_rx_buf, sizeof(dma_rx_buf));
// 2. 等待 DMA 完成(中断或轮询)
// 3. DMA 完成后,再次 Invalidate,让 CPU 读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
// 4. 此时 CPU 读取 dma_rx_buf 才是正确的
process_data(dma_rx_buf, len);
五、完整代码示例(基于 HAL 库)
#include "stm32h7xx_hal.h"
ALIGN_32BYTES(uint8_t uart_tx_buf[128]);
ALIGN_32BYTES(uint8_t uart_rx_buf[128]);
volatile uint8_t dma_rx_done = 0;
void uart_dma_send(uint8_t *data, uint16_t len) {
if (len > sizeof(uart_tx_buf)) return;
memcpy(uart_tx_buf, data, len);
// Clean 确保数据写入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)uart_tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, uart_tx_buf, len);
}
void uart_dma_start_receive(void) {
// 启动前 Invalidate,丢弃 Cache 旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, sizeof(uart_rx_buf));
HAL_UART_Receive_DMA(&huart1, uart_rx_buf, sizeof(uart_rx_buf));
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 再次 Invalidate,让 CPU 看到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)uart_rx_buf, sizeof(uart_rx_buf));
dma_rx_done = 1;
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_UART4_Init();
uart_dma_start_receive();
while (1) {
if (dma_rx_done) {
dma_rx_done = 0;
// 处理 uart_rx_buf 中的数据
uart_dma_send(uart_rx_buf, 128);
uart_dma_start_receive();
}
}
}
六、避坑指南与注意事项
- 缓冲区必须 32 字节对齐,且长度最好是 32 的倍数,避免跨 Cache 行误伤。
- Clean 和 Invalidate 不要搞反:发送用 Clean,接收用 Invalidate。
- 接收前也要 Invalidate:否则 Cache 中的旧数据可能被 CPU 误读。
- DMA 传输期间不要操作缓冲区:否则 Cache 与 SRAM 可能再次不一致。
- 使用 MPU 配置非缓存区域:如果不想手动维护,可将 DMA 缓冲区所在区域配置为 Non-Cacheable,但会牺牲性能。
- 注意中断优先级:DMA 完成中断中调用 Invalidate 是安全的,但需确保没有其他 CPU 访问同一缓冲区。
- 多缓冲区场景:如果使用双缓冲,每个缓冲区都要独立维护,不能只维护一个。
掌握以上要点,你就能在 STM32H7 上放心使用 D-Cache 和 DMA,既享受高性能,又避免数据错乱。