STM32H7 的 D-Cache 与 DMA 一致性:Cache Clean/Invalidate 的实操与踩坑
1. 为什么 DMA 和 D-Cache 会打架?
STM32H7 的 Cortex-M7 主频高达 480MHz,为了弥补内核与存储器之间的速度差,芯片内部集成了 D-Cache(数据缓存)。CPU 读写数据时,实际访问的是 Cache,而不是直接访问 SRAM。
DMA 则是一个“独立搬运工”,它直接访问 SRAM,不经过 Cache。于是问题来了:
- CPU 写数据到 SRAM 后,数据可能还在 Cache 里,没写回 SRAM。此时启动 DMA 发送,DMA 读到的就是旧数据。
- DMA 从外设接收数据写入 SRAM 后,CPU 读该地址时,可能读到 Cache 里的旧数据,而不是 DMA 刚写入的新数据。
这就是 Cache 一致性(Coherency) 问题。解决思路有两种:
- 硬件层面:使用 MPU 将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable。
- 软件层面:在 DMA 传输前后,手动执行 Cache Clean(写回) 和 Cache Invalidate(无效化)。
本文重点讲软件层面的实操,因为它在不改动 MPU 全局配置时最灵活。
2. Cache Clean 与 Invalidate 到底做了什么?
- Clean(清理/写回):把 Cache 中“脏”的数据写回 SRAM。用于 CPU 写、DMA 读 的场景。
- Invalidate(无效化):把 Cache 中的对应行标记为无效。下次 CPU 读时会强制从 SRAM 重新加载。用于 DMA 写、CPU 读 的场景。
注意:Invalidate 不会把数据写回 SRAM,它只是丢弃 Cache 内容。如果 Cache 中有未写回的数据,Invalidate 会导致数据丢失!
3. 典型场景与操作顺序
场景一:CPU 填充缓冲区 → DMA 发送
// 1. CPU 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
tx_buf[i] = i;
}
// 2. Clean:确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
// 3. 启动 DMA 发送
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&UART->TDR, BUF_SIZE);
场景二:DMA 接收数据 → CPU 读取
// 1. 启动 DMA 接收
HAL_DMA_Start(&hdma, (uint32_t)&UART->RDR, (uint32_t)rx_buf, BUF_SIZE);
// 2. 等待 DMA 完成(中断或轮询)
while (!dma_done);
// 3. Invalidate:丢弃 Cache 旧数据,强制从 SRAM 读
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 4. CPU 读取数据
process_data(rx_buf);
场景三:双向 DMA(如 SPI 全双工)
先 Clean 发送缓冲区,再 Invalidate 接收缓冲区,顺序不能错。
4. 完整代码示例(基于 HAL 库)
#include "stm32h7xx_hal.h"
#define BUF_SIZE 64
// 注意:缓冲区必须 32 字节对齐!
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
DMA_HandleTypeDef hdma_tx;
DMA_HandleTypeDef hdma_rx;
void dma_send_with_cache(void)
{
// 填充数据
for (int i = 0; i < BUF_SIZE; i++) {
tx_buf[i] = i;
}
// Clean D-Cache:将 tx_buf 写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
// 启动 DMA 发送
HAL_DMA_Start(&hdma_tx, (uint32_t)tx_buf, (uint32_t)&UART4->TDR, BUF_SIZE);
}
void dma_receive_with_cache(void)
{
// 启动 DMA 接收
HAL_DMA_Start(&hdma_rx, (uint32_t)&UART4->RDR, (uint32_t)rx_buf, BUF_SIZE);
// 等待完成(实际项目建议用中断)
while (HAL_DMA_PollForTransfer(&hdma_rx, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);
// Invalidate D-Cache:丢弃 rx_buf 的 Cache 行
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 此时 CPU 读取 rx_buf 才是 DMA 写入的新数据
for (int i = 0; i < BUF_SIZE; i++) {
printf("%02X ", rx_buf[i]);
}
}
5. 踩坑与注意事项
-
地址必须 32 字节对齐:Cortex-M7 的 Cache 行大小是 32 字节。
SCB_CleanDCache_by_Addr内部会按行操作,如果地址不对齐,可能误伤相邻数据。建议用__attribute__((aligned(32)))修饰缓冲区。 -
长度也要按 32 字节对齐:如果长度不是 32 的倍数,函数会向上取整到 Cache 行边界,可能清理/无效化到缓冲区之外的数据。最好让
BUF_SIZE是 32 的倍数。 -
Invalidate 前必须确保 Cache 中没有脏数据:如果 CPU 刚写过该缓冲区,直接 Invalidate 会丢失未写回的数据。正确做法是先 Clean 再 Invalidate,或者用
SCB_CleanInvalidateDCache_by_Addr。 - DMA 传输期间不要操作缓冲区:DMA 工作时 CPU 若访问同一缓冲区,可能触发 Cache 行填充,导致数据错乱。
- MPU 配置是更彻底的方案:如果不想每次手动维护 Cache,可以用 MPU 将 DMA 缓冲区设为 Non-Cacheable 或 Write-Through。但注意 Non-Cacheable 会降低 CPU 访问性能。
-
中断中调用 Cache 维护函数要小心:
SCB_CleanDCache_by_Addr等函数执行时间较长,在高速中断中可能影响实时性。
6. 总结
STM32H7 的 D-Cache 是性能利器,但和 DMA 配合时必须处理好一致性。记住一个口诀:CPU 写、DMA 读 → Clean;DMA 写、CPU 读 → Invalidate。同时注意 32 字节对齐和操作顺序,就能避开大多数坑。如果项目对性能要求极高,建议结合 MPU 配置,从硬件层面减少软件维护开销。