一、为什么 H7 上 DMA 会“失灵”?
STM32H7 采用 Cortex-M7 内核,带 16KB D-Cache 和 16KB I-Cache。开启 D-Cache 后,CPU 访问 SRAM 会先经过 Cache。而 DMA 是直接访问物理 SRAM 的,它看不见 Cache。
这会导致两类问题:
- DMA 读(外设→内存):DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 里的旧数据。
- DMA 写(内存→外设):CPU 写的数据还在 Cache 中未回写到 SRAM,DMA 却从 SRAM 读到旧数据。
解决思路只有两个:使用非 Cache 区域(MPU 配置) 或 手动维护 Cache 一致性(Clean/Invalidate)。本文重点讲后者。
二、地址对齐:32 字节是硬性要求
Cortex-M7 的 Cache Line 为 32 字节。Clean 和 Invalidate 操作都以 Cache Line 为单位,因此 DMA 缓冲区必须 32 字节对齐,且大小最好是 32 的整数倍。
/* 推荐:使用编译器属性强制对齐 */
__attribute__((aligned(32))) uint8_t dma_rx_buf[256];
__attribute__((aligned(32))) uint8_t dma_tx_buf[256];
如果缓冲区未对齐,Invalidate 时可能误伤相邻变量,导致“莫名其妙”的变量被清零——这是最隐蔽的坑之一。
三、Clean 与 Invalidate 的正确顺序
先明确两个操作的含义:
- Clean:把 Cache 中“脏”数据写回 SRAM。
- Invalidate:丢弃 Cache 内容,下次读时从 SRAM 重新加载。
3.1 外设 → 内存(DMA 接收)
DMA 写入 SRAM 前,CPU 可能已经读过该缓冲区,Cache 中有旧副本。正确顺序:
- Invalidate 缓冲区(丢弃旧 Cache)
- 启动 DMA 接收
- DMA 完成中断中再次 Invalidate(确保读到最新数据)
/* 启动接收前 */
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));
/* DMA 完成回调中 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, sizeof(dma_rx_buf));
/* 此时读取 dma_rx_buf 才是最新数据 */
}
3.2 内存 → 外设(DMA 发送)
CPU 写入缓冲区后数据可能还在 Cache 中,必须 Clean 后才能启动 DMA:
memcpy(dma_tx_buf, src, len);
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, sizeof(dma_tx_buf));
HAL_UART_Transmit_DMA(&huart1, dma_tx_buf, len);
关键原则:Clean 用于“CPU 写、DMA 读”;Invalidate 用于“DMA 写、CPU 读”。顺序错误会导致数据丢失或读到旧值。
四、完整示例:UART DMA 收发
#include "stm32h7xx_hal.h"
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
void uart_dma_init(void)
{
/* 启动接收:先 Invalidate */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
void uart_dma_send(uint8_t *data, uint16_t len)
{
if (len > BUF_SIZE) return;
memcpy(tx_buf, data, len);
/* 发送前 Clean */
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* 接收完成:Invalidate 后再处理 */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
process_data(rx_buf, BUF_SIZE);
/* 重新启动接收 */
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
五、典型踩坑与注意事项
-
坑 1:缓冲区未 32 字节对齐。Invalidate 会清掉相邻变量,表现为“无关变量被改”。务必用
__attribute__((aligned(32)))。 - 坑 2:Clean 和 Invalidate 顺序颠倒。发送前 Invalidate 会把 CPU 刚写的数据丢弃,DMA 发出全 0 或旧数据。
- 坑 3:DMA 传输中访问缓冲区。DMA 进行中 CPU 读写同一缓冲区会破坏一致性,应使用双缓冲或等待完成。
- 坑 4:忘记在中断中再次 Invalidate。接收完成中断里若不 Invalidate,读到的仍是旧 Cache。
-
坑 5:缓冲区大小不是 32 的倍数。
SCB_*DCache_by_Addr内部按 Cache Line 操作,非整数倍可能越界。建议向上取整到 32 字节。 - 坑 6:多缓冲区共享 Cache Line。两个变量落在同一 32 字节行内,Invalidate 一个会影响另一个。用对齐隔离。
替代方案:若数据量大且频繁,建议用 MPU 将 DMA 缓冲区配置为 Write-Through, No Write-Allocate 或 Non-Cacheable,从根源避免一致性问题,代价是 CPU 访问稍慢。
六、总结
STM32H7 的 D-Cache 是性能利器,但与 DMA 共存时必须谨慎。记住三句话:缓冲区 32 字节对齐;CPU 写后 Clean,DMA 写后 Invalidate;顺序不能反。掌握这些,DMA 数据错乱问题将迎刃而解。