一、为什么 Cache 会与 DMA 打架?
STM32H7 的 Cortex-M7 内核带有 L1 数据缓存(D-Cache)和指令缓存(I-Cache)。当 CPU 访问外部 SDRAM 或内部 SRAM 时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存,不经过 Cache。
这就导致两种典型问题:
- CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 中未写回内存,DMA 读到的却是旧数据。
- DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读到的却是 Cache 中的旧数据。
解决思路:在 DMA 传输前后,对相关内存区域执行 Cache 维护操作(Clean / Invalidate)。
二、Cache 维护操作的正确顺序
2.1 核心 API
CMSIS 提供了以下函数(定义在 core_cm7.h):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
- Clean:将 Cache 中已修改的数据写回内存。
- Invalidate:将 Cache 中的对应行标记为无效,下次读取时从内存重新加载。
- CleanInvalidate:先写回再无效。
2.2 发送方向(CPU → DMA)
CPU 准备好数据后,DMA 要发送出去。
正确顺序:
- CPU 写入数据到内存缓冲区。
- 执行
SCB_CleanDCache_by_Addr(),确保数据写回物理内存。 - 启动 DMA 传输。
错误做法:先启动 DMA,再 Clean —— 此时 DMA 可能已经读取了旧数据。
2.3 接收方向(DMA → CPU)
DMA 接收数据到内存,CPU 随后读取。
正确顺序:
- 启动 DMA 接收。
- 等待 DMA 传输完成。
- 执行
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中的旧数据。 - CPU 读取缓冲区。
错误做法:先 Invalidate,再启动 DMA —— 若 CPU 在 DMA 传输期间访问了该缓冲区,Cache 会再次填充旧数据。
2.4 双向传输
若同一缓冲区既发送又接收,推荐使用 SCB_CleanInvalidateDCache_by_Addr(),但需注意操作时机。
三、失效场景复现:DMA 接收数据错乱
3.1 场景描述
使用 DMA 将 UART 接收的数据搬运到 SDRAM 缓冲区,CPU 轮询读取。未做 Cache 维护时,CPU 读到的数据可能始终是第一次的旧值。
3.2 复现代码(错误示范)
#define BUF_SIZE 32
__attribute__((section(".sdram"))) uint8_t rx_buf[BUF_SIZE];
void uart_dma_init(void) {
// 配置 UART 和 DMA,目标地址为 rx_buf
// ...
}
void process_data(void) {
// 等待 DMA 完成
while (!dma_transfer_complete);
// 直接读取,未 Invalidate Cache
for (int i = 0; i < BUF_SIZE; i++) {
printf("%02X ", rx_buf[i]);
}
}
现象:第一次接收后,后续每次打印都是相同的数据,即使 UART 发送了新内容。
3.3 修正代码
void process_data(void) {
while (!dma_transfer_complete);
// 关键:无效化 Cache,强制从内存重新加载
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
for (int i = 0; i < BUF_SIZE; i++) {
printf("%02X ", rx_buf[i]);
}
}
四、完整示例:UART DMA 接收 + Cache 维护
#include "stm32h7xx.h"
#define RX_BUF_SIZE 64
__attribute__((section(".sdram"))) __attribute__((aligned(32))) uint8_t rx_buf[RX_BUF_SIZE];
volatile uint8_t dma_done = 0;
void uart_dma_config(void) {
// 使能时钟、配置 GPIO、UART、DMA
// 设置 DMA 目标地址为 rx_buf,传输长度 RX_BUF_SIZE
// 使能 DMA 传输完成中断
}
void DMA1_Stream0_IRQHandler(void) {
if (DMA1->LISR & DMA_LISR_TCIF0) {
DMA1->LIFCR = DMA_LIFCR_CTCIF0;
dma_done = 1;
}
}
void read_uart_data(void) {
if (dma_done) {
// 1. 无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_BUF_SIZE);
// 2. 处理数据
for (int i = 0; i < RX_BUF_SIZE; i++) {
// 使用 rx_buf[i]
}
dma_done = 0;
// 3. 重新启动 DMA 接收
// ...
}
}
五、注意事项与避坑指南
- 地址对齐:Cache 操作要求地址 32 字节对齐,长度也建议按 32 字节对齐,否则可能影响相邻数据。
-
缓冲区大小:若缓冲区不是 Cache 行大小的整数倍,Invalidate 可能丢弃相邻变量,建议使用
__attribute__((aligned(32)))并填充到 32 字节倍数。 - 中断与并发:在 DMA 传输期间,避免 CPU 访问同一缓冲区,否则 Cache 可能被意外填充。
- MPU 配置:可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,一劳永逸,但会牺牲性能。
- 性能权衡:频繁的 Cache 维护会降低性能,合理划分 Cache 区域与 DMA 区域。
- 调试技巧:若怀疑 Cache 问题,可暂时关闭 D-Cache 验证,但正式产品不建议关闭。
六、总结
STM32H7 的 L1 Cache 与 DMA 一致性是嵌入式开发中的经典难题。牢记两条黄金法则:
- CPU 写、DMA 读 → 先 Clean,再启动 DMA
- DMA 写、CPU 读 → 先等 DMA 完成,再 Invalidate
配合正确的对齐与 MPU 配置,即可在享受 Cache 高性能的同时,保证数据传输的可靠性。