一、为什么 D-Cache 会成为 DMA 的“猪队友”?
STM32H7 搭载 Cortex-M7 内核,主频可达 480MHz,为了匹配高速内核与相对较慢的 Flash/SRAM,芯片内部集成了 L1 D-Cache(数据缓存)。CPU 访问数据时,若命中 Cache 则直接读写缓存,不会立即同步到物理内存。
而 DMA 是“老实人”,它直接读写物理内存(SRAM),完全不知道 Cache 的存在。这就导致两种经典错误:
- CPU 写,DMA 读:CPU 把数据写入 Cache 但未回写内存,DMA 从内存读到旧数据。
- DMA 写,CPU 读:DMA 把新数据写入内存,但 CPU 读的是 Cache 里的旧数据。
解决思路只有两个:clean(将 Cache 数据写回内存)和 invalidate(将 Cache 行标记为无效,强制下次从内存读取)。但用错时机或忽略地址对齐,就会引入更隐蔽的 bug。
二、地址对齐:32 字节是硬性门槛
Cortex-M7 的 D-Cache 行大小为 32 字节。clean 和 invalidate 操作的最小单位就是一行(32 字节),且起始地址必须 32 字节对齐。
如果缓冲区起始地址不是 32 字节对齐,调用 SCB_CleanDCache_by_Addr() 时,函数内部会向下对齐到行首,可能误伤相邻变量;invalidate 更危险,会把相邻数据也标记为无效,导致其他变量被意外丢弃。
正确做法:使用 __attribute__((aligned(32))) 强制对齐。
// 错误:地址可能不对齐
uint8_t adc_buf[1024];
// 正确:32 字节对齐
__attribute__((aligned(32))) uint8_t adc_buf[1024];
另外,缓冲区大小最好是 32 的整数倍,避免边界行处理不完整。
三、clean 与 invalidate 的正确使用时机
记住一个简单口诀:CPU 写、DMA 读 → clean;DMA 写、CPU 读 → invalidate。
- 发送方向(CPU→DMA):CPU 准备好数据后,先 clean,再启动 DMA。
- 接收方向(DMA→CPU):DMA 传输完成后,先 invalidate,再读数据。
注意:invalidate 会丢弃 Cache 中未回写的数据。如果 CPU 在 DMA 传输期间碰过同一缓冲区,必须先 clean 再 invalidate,否则数据丢失。
四、完整实战代码:ADC + DMA 采集
以 ADC1 连续扫描 + DMA2 传输到内存为例,展示完整配置与缓存维护。
#include "stm32h7xx.h"
#define ADC_BUF_LEN 256
// 32 字节对齐的 DMA 目标缓冲区
__attribute__((aligned(32))) static uint16_t adc_dma_buf[ADC_BUF_LEN];
static volatile uint8_t dma_done = 0;
// DMA 传输完成回调
void DMA2_Stream0_IRQHandler(void)
{
if (DMA2->LISR & DMA_LISR_TCIF0) {
DMA2->LIFCR = DMA_LIFCR_CTCIF0; // 清除传输完成标志
dma_done = 1;
}
}
void adc_dma_init(void)
{
// 1. 使能时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 2. 配置 DMA2 Stream0:外设到内存,循环模式
DMA2_Stream0->CR &= ~DMA_SxCR_EN;
while (DMA2_Stream0->CR & DMA_SxCR_EN);
DMA2_Stream0->PAR = (uint32_t)&ADC1->DR;
DMA2_Stream0->M0AR = (uint32_t)adc_dma_buf;
DMA2_Stream0->NDTR = ADC_BUF_LEN;
DMA2_Stream0->CR = (0 << DMA_SxCR_CHSEL_Pos) |
DMA_SxCR_PL_1 | // 高优先级
DMA_SxCR_MSIZE_0 | // 16 位内存
DMA_SxCR_PSIZE_0 | // 16 位外设
DMA_SxCR_MINC | // 内存递增
DMA_SxCR_CIRC | // 循环模式
DMA_SxCR_TCIE; // 传输完成中断
DMA2_Stream0->FCR &= ~DMA_SxFCR_DMDIS; // 使能直接模式
DMA2_Stream0->CR |= DMA_SxCR_EN;
// 3. 配置 ADC1 连续转换 + DMA
ADC1->CFGR = ADC_CFGR_CONT | ADC_CFGR_DMAEN | ADC_CFGR_DMACFG;
ADC1->SQR1 = (3 << ADC_SQR1_L_Pos); // 4 个通道
ADC1->SQR1 |= (1 << ADC_SQR1_SQ1_Pos);
ADC1->SQR1 |= (2 << ADC_SQR1_SQ2_Pos);
ADC1->SQR1 |= (3 << ADC_SQR1_SQ3_Pos);
ADC1->SQR1 |= (4 << ADC_SQR1_SQ4_Pos);
ADC1->CR |= ADC_CR_ADEN;
while (!(ADC1->ISR & ADC_ISR_ADRDY));
ADC1->CR |= ADC_CR_ADSTART;
// 4. 使能中断
NVIC_EnableIRQ(DMA2_Stream0_IRQn);
}
// 读取 ADC 数据(在 DMA 完成中断后调用)
void adc_read_buffer(uint16_t *out, uint32_t len)
{
if (!dma_done) return;
// 关键:DMA 写内存,CPU 读之前必须 invalidate
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf,
ADC_BUF_LEN * sizeof(uint16_t));
for (uint32_t i = 0; i < len; i++) {
out[i] = adc_dma_buf[i];
}
dma_done = 0;
}
五、五个必知的实战陷阱
-
陷阱 1:忘记地址对齐。
SCB_InvalidateDCache_by_Addr传入非对齐地址时,会向下取整到行首,可能把相邻变量一起 invalidate,导致其他数据被“吃掉”。务必用aligned(32)。 - 陷阱 2:invalidate 前未 clean。如果 CPU 在 DMA 传输期间写过同一缓冲区,invalidate 会直接丢弃这些未回写的数据。正确顺序:先 clean,再 invalidate。
- 陷阱 3:缓冲区跨 Cache 行。若缓冲区大小不是 32 的整数倍,最后一行可能包含其他变量。invalidate 会误伤相邻数据。建议缓冲区大小补齐到 32 的倍数。
- 陷阱 4:在中断中频繁 clean/invalidate。这两个操作会阻塞流水线,影响实时性。尽量批量处理,或使用 MPU 将 DMA 缓冲区配置为 Write-Through/Non-Cacheable。
- 陷阱 5:忽略 MPU 配置。对于频繁 DMA 的缓冲区,最优雅的方案是用 MPU 将其设为 Non-Cacheable,彻底避免一致性问题。但要注意 Non-Cacheable 会降低 CPU 访问速度,需权衡。
六、总结
D-Cache 与 DMA 的冲突是 STM32H7 开发中的高频痛点。核心原则只有三条:地址 32 字节对齐、方向决定 clean 还是 invalidate、先 clean 后 invalidate。掌握这些,你就能在享受 H7 高性能的同时,避开数据错乱的深坑。建议在项目初期就用 MPU 规划好 DMA 缓冲区属性,从架构上消除隐患。