一、为什么 D-Cache 会让 DMA 数据错乱?
STM32H7 的 Cortex-M7 内核带 16KB D-Cache 和 16KB I-Cache。D-Cache 作为 CPU 与 SRAM 之间的高速缓冲,会缓存 CPU 读写过的数据。而 DMA 是独立于 CPU 的总线主设备,它直接访问 SRAM,不经过 Cache。
于是出现两种典型问题:
- CPU 写、DMA 读:CPU 把数据写入 Cache,尚未回写到 SRAM,DMA 启动后从 SRAM 读到的是旧数据。
- DMA 写、CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的仍是 Cache 中的旧副本。
解决思路就是按需执行 Clean(清理,把 Cache 写回 SRAM) 和 Invalidate(无效化,丢弃 Cache 内容,强制从 SRAM 重读)。
二、Clean 与 Invalidate 的时机
记住一条原则:谁写谁负责同步。
- CPU 写缓冲区 → DMA 读(如 UART 发送、DAC 输出):DMA 启动前对缓冲区做 Clean。
- DMA 写缓冲区 → CPU 读(如 ADC 采集、以太网接收):DMA 完成后对缓冲区做 Invalidate。
-
双向传输(如以太网描述符):先 Clean 再 Invalidate,或使用
SCB_CleanInvalidateDCache_by_Addr()。
注意:Invalidate 会丢弃 Cache 中未回写的数据,若 CPU 曾写过该区域且未 Clean,数据会丢失。因此 Invalidate 前必须确保没有脏数据。
三、配置步骤
-
使能 D-Cache:在
main()最开始调用SCB_EnableDCache(),且必须在使能前初始化 MPU(若使用)。 - 配置 MPU(可选但推荐):将 DMA 缓冲区所在 SRAM 区域设为 Non-Cacheable,可彻底避免一致性问题,但会牺牲性能。
- 地址与长度对齐:Cache 操作以 32 字节 Cache Line 为单位,地址必须 32 字节对齐,长度建议向上取整到 32 字节倍数。
- 在 DMA 前后插入维护函数:发送前 Clean,接收后 Invalidate。
四、完整代码示例
以 ADC + DMA 采集为例,缓冲区定义与维护函数如下:
#include "stm32h7xx.h"
#define ADC_BUF_LEN 256
/* 必须 32 字节对齐,否则 Cache 维护会误伤相邻数据 */
__attribute__((aligned(32))) uint16_t adc_buf[ADC_BUF_LEN];
/* CPU 写、DMA 读:发送前 Clean */
void dma_tx_prepare(void *buf, uint32_t len)
{
uint32_t addr = (uint32_t)buf;
uint32_t size = (len + 31U) & ~31U; /* 向上取整到 32 字节 */
SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
}
/* DMA 写、CPU 读:接收后 Invalidate */
void dma_rx_complete(void *buf, uint32_t len)
{
uint32_t addr = (uint32_t)buf;
uint32_t size = (len + 31U) & ~31U;
SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
}
int main(void)
{
SCB_EnableICache();
SCB_EnableDCache(); /* 使能 D-Cache */
adc_dma_init(adc_buf, ADC_BUF_LEN);
while (1)
{
/* 等待 DMA 传输完成标志 */
if (dma_done_flag)
{
dma_done_flag = 0;
/* 先 Invalidate,再读取缓冲区 */
dma_rx_complete(adc_buf, sizeof(adc_buf));
process_adc_data(adc_buf, ADC_BUF_LEN);
}
}
}
若使用以太网发送,则发送前对描述符和数据缓冲区执行 Clean:
void eth_send_frame(uint8_t *frame, uint32_t len)
{
memcpy(tx_buf, frame, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, (len + 31U) & ~31U);
ETH_TransmitFrame(len);
}
五、踩坑复盘
-
坑 1:缓冲区未对齐。
uint8_t buf[100]默认 4 字节对齐,Clean 时按 32 字节操作会波及相邻变量,导致莫名数据被改写。务必用__attribute__((aligned(32)))。 - 坑 2:Invalidate 前未 Clean。CPU 刚写过接收缓冲区又立即 Invalidate,未回写的脏数据被丢弃。正确做法是先 Clean 再 Invalidate,或避免 CPU 提前写。
- 坑 3:DMA 传输中操作 Cache。DMA 正在读写时执行 Clean/Invalidate 会破坏传输。必须在 DMA 完成中断或传输前后操作。
-
坑 4:忘记使能 D-Cache。代码里写了维护函数,但
SCB_EnableDCache()没调用,问题不出现,一旦使能就暴露,调试时容易误判。 - 坑 5:多缓冲区共享 Cache Line。两个变量落在同一 32 字节行内,Invalidate 一个会连带丢弃另一个的修改。用对齐隔离或 MPU 分区解决。
六、总结
D-Cache 与 DMA 的一致性没有银弹,核心是理解 Clean 用于“CPU 写后让 DMA 看到”,Invalidate 用于“DMA 写后让 CPU 看到”。配合 32 字节对齐、传输前后操作、必要时用 MPU 设为 Non-Cacheable,就能在享受 H7 高性能的同时,保证数据可靠。建议把维护函数封装成统一接口,在驱动层调用,避免遗漏。