一、为什么 D-Cache 会和 DMA 打架?
STM32H7 的 Cortex-M7 内核带有一级 D-Cache(通常 16KB),CPU 访问数据时先查 Cache,命中则直接读写 Cache,不会立即同步到 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,完全绕过 Cache。
这就导致两个经典问题:
- DMA 写入 SRAM 后,CPU 读到的却是 Cache 里的旧数据(读脏数据)。
- CPU 写入的数据还在 Cache 中,DMA 却从 SRAM 读到了旧值(写丢失)。
解决思路只有两条:要么把 DMA 缓冲区放到非 Cache 区域(如 DTCM),要么在恰当时机手动维护 Cache 一致性。本文聚焦后者。
二、Cache Line 与地址对齐:一切的基础
Cortex-M7 的 D-Cache 以 Cache Line 为单位操作,STM32H7 的 Cache Line 固定为 32 字节。
关键规则:
- Clean(清理):把 Cache 中的脏数据写回 SRAM。
- Invalidate(无效化):丢弃 Cache 内容,下次读时从 SRAM 重新加载。
- 这两个操作的最小粒度都是 一整条 Cache Line(32 字节),无法只操作其中几个字节。
因此,如果 DMA 缓冲区没有按 32 字节对齐,或者长度不是 32 的整数倍,Invalidate 时就会误伤相邻变量,导致其他数据被意外丢弃。
正确做法:
- 缓冲区起始地址按 32 字节对齐。
- 缓冲区大小补齐到 32 字节的整数倍。
- 推荐使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。
#define ALIGN_32BYTES __attribute__((aligned(32)))
// DMA 接收缓冲区,强制 32 字节对齐
ALIGN_32BYTES static uint8_t dma_rx_buf[256];
// 如果长度不是 32 的倍数,向上取整
#define CACHE_LINE_SIZE 32
#define ALIGN_UP(x) (((x) + CACHE_LINE_SIZE - 1) & ~(CACHE_LINE_SIZE - 1))
三、Clean 与 Invalidate 的使用时机
根据数据流向,选择不同操作:
- CPU 写 → DMA 读(发送):CPU 写完数据后,执行 Clean,把数据刷到 SRAM,再启动 DMA。
- DMA 写 → CPU 读(接收):DMA 完成中断里,先 Invalidate,丢弃 Cache 旧内容,再让 CPU 读。
- 双向缓冲区:先 Invalidate 再 Clean,或分区域处理,避免误伤。
HAL 库提供了封装函数,位于 stm32h7xx_hal.h:
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
注意:dsize 是字节数,函数内部会自动按 Cache Line 对齐处理,但地址本身必须 32 字节对齐,否则行为未定义。
四、完整实战代码:UART DMA 接收
以 UART 空闲中断 + DMA 接收为例,展示完整流程。
#include "stm32h7xx_hal.h"
#define RX_BUF_SIZE 256
ALIGN_32BYTES static uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t rx_flag = 0;
void uart_dma_init(UART_HandleTypeDef *huart, DMA_HandleTypeDef *hdma)
{
// 启动 DMA 接收,长度按 32 字节对齐
HAL_UART_Receive_DMA(huart, rx_buf, ALIGN_UP(RX_BUF_SIZE));
// 使能空闲中断
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
}
// UART 空闲中断回调(在 stm32h7xx_it.c 中调用)
void UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 1. 停止 DMA,获取已接收长度
HAL_UART_DMAStop(huart);
uint16_t len = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 2. 关键:无效化 Cache,保证 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, ALIGN_UP(RX_BUF_SIZE));
// 3. 此时 rx_buf 中的数据才是有效的
process_data(rx_buf, len);
// 4. 重新启动 DMA 接收
HAL_UART_Receive_DMA(huart, rx_buf, ALIGN_UP(RX_BUF_SIZE));
__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);
}
}
// 发送示例:CPU 写 → DMA 读
void uart_send_dma(UART_HandleTypeDef *huart, uint8_t *data, uint16_t len)
{
// 确保数据已刷入 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)data, ALIGN_UP(len));
HAL_UART_Transmit_DMA(huart, data, len);
}
五、避坑要点总结
-
地址必须 32 字节对齐:用
ALIGN_32BYTES修饰所有 DMA 缓冲区,否则 Invalidate 会破坏相邻变量。 -
长度补齐到 32 的整数倍:
ALIGN_UP宏必不可少,尤其在小缓冲区场景。 - Invalidate 前先停止 DMA:DMA 还在写时执行 Invalidate,可能丢数据或读到半包。
- 不要对同一区域反复 Invalidate:若 CPU 刚写过该区域,Invalidate 会丢弃未刷回的数据,应先 Clean。
- 优先使用 DTCM:DTCM 不经过 Cache,天然一致,适合小容量高频 DMA 缓冲区。
- MPU 配置非 Cache 区域:对大片 DMA 缓冲区,可用 MPU 将其设为 Non-Cacheable,一劳永逸。
- 调试时关闭 Cache 对比:若数据错乱,先关 D-Cache 验证是否为一致性问题。
六、结语
D-Cache 与 DMA 的一致性问题是 STM32H7 开发中的高频坑点,核心就三句话:对齐、时机、粒度。只要缓冲区按 32 字节对齐、在正确的时机执行 Clean/Invalidate、并注意 Cache Line 粒度,就能稳定可靠地让 DMA 与 CPU 协同工作。建议将上述封装成通用宏或函数,在项目中统一使用,避免重复踩坑。