一、为什么 D-Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核带 16KB D-Cache,CPU 访问数据时优先走 Cache,而 DMA 直接访问物理内存(SRAM)。当两者访问同一块缓冲区时,就会出现:
- CPU 写数据后未回写:DMA 读到的是旧值(Cache 未 Clean)。
- DMA 写入新数据后 CPU 读旧值:Cache 中仍是旧副本(Cache 未 Invalidate)。
- Cache 行对齐问题:Invalidate 操作以 32 字节行为单位,若缓冲区未对齐,会误伤相邻变量。
因此,必须遵循 “谁写谁 Clean,谁读谁 Invalidate” 的原则,并保证缓冲区按 Cache 行对齐。
二、Cache 行对齐:32 字节是硬性要求
Cortex-M7 的 D-Cache 行大小为 32 字节。SCB_InvalidateDCache_by_Addr 等函数内部按行操作,若地址未对齐,会:
- 向上取整到行首,向下取整到行尾,导致相邻数据被意外失效或回写。
- 若相邻变量是 DMA 正在使用的,可能引发数据竞争甚至 HardFault。
正确做法:
- 使用
__attribute__((aligned(32)))或ALIGN_32BYTES宏。 - 缓冲区大小最好是 32 的整数倍,避免末尾部分行被误操作。
#define DMA_BUF_SIZE 128
__attribute__((aligned(32))) uint8_t dma_buf[DMA_BUF_SIZE];
三、Clean 与 Invalidate 的时序
3.1 发送方向(CPU → 外设)
- CPU 填充缓冲区。
- Clean D-Cache(将最新数据写回 SRAM)。
- 启动 DMA 发送。
- DMA 完成后,若 CPU 要修改缓冲区,需再次 Clean 或直接操作非缓存区。
3.2 接收方向(外设 → CPU)
- 启动 DMA 接收前,Invalidate D-Cache(丢弃旧副本,避免 CPU 读到脏数据)。
- DMA 完成后,再次 Invalidate(确保 CPU 读到 DMA 写入的新数据)。
- 注意:Invalidate 前必须确保 CPU 没有未回写的脏数据,否则会丢失。
关键点:Invalidate 会丢弃 Cache 中未回写的数据,因此若 CPU 刚写过该区域,必须先 Clean 再 Invalidate。
四、完整示例:ADC + DMA 采集
以下代码基于 STM32H743,使用 ADC1 + DMA1_Stream0,采集 128 个样本。
#include "stm32h7xx.h"
#define ADC_BUF_SIZE 128
__attribute__((aligned(32))) static uint16_t adc_buf[ADC_BUF_SIZE];
volatile uint8_t adc_done = 0;
void ADC_DMA_Init(void)
{
// 1. 使能时钟
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// 2. 配置 DMA1_Stream0
DMA1_Stream0->CR &= ~DMA_SxCR_EN;
while (DMA1_Stream0->CR & DMA_SxCR_EN);
DMA1_Stream0->PAR = (uint32_t)&ADC1->DR;
DMA1_Stream0->M0AR = (uint32_t)adc_buf;
DMA1_Stream0->NDTR = ADC_BUF_SIZE;
DMA1_Stream0->CR = DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 |
DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC |
DMA_SxCR_CIRC | DMA_SxCR_TCIE;
DMA1_Stream0->FCR = 0;
// 3. 配置 ADC1
ADC1->CFGR = ADC_CFGR_DMAEN | ADC_CFGR_DMACFG;
ADC1->SQR1 = (ADC_BUF_SIZE - 1) << ADC_SQR1_L_Pos;
ADC1->SQR3 = 0; // 通道 0
ADC1->CR = ADC_CR_ADEN;
while (!(ADC1->ISR & ADC_ISR_ADRDY));
ADC1->CR |= ADC_CR_ADSTART;
// 4. 启动 DMA
DMA1_Stream0->CR |= DMA_SxCR_EN;
// 5. 使能中断
NVIC_EnableIRQ(DMA1_Stream0_IRQn);
}
void DMA1_Stream0_IRQHandler(void)
{
if (DMA1->LISR & DMA_LISR_TCIF0) {
DMA1->LIFCR = DMA_LIFCR_CTCIF0;
// 关键:DMA 写入后,Invalidate D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_buf, sizeof(adc_buf));
adc_done = 1;
}
}
// 主循环中处理数据
void Process_ADC(void)
{
if (adc_done) {
adc_done = 0;
// 此时 adc_buf 中已是 DMA 写入的最新数据
uint32_t sum = 0;
for (int i = 0; i < ADC_BUF_SIZE; i++) {
sum += adc_buf[i];
}
// ...
}
}
五、MPU 配置:更优雅的解决方案
若不想每次 DMA 都手动 Clean/Invalidate,可用 MPU 将 DMA 缓冲区配置为 Non-Cacheable。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
将 DMA 缓冲区放在该区域,即可免去手动维护 Cache 的麻烦。
六、注意事项与排查清单
- 对齐:缓冲区必须 32 字节对齐,大小建议为 32 的倍数。
- 时序:发送前 Clean,接收前 Invalidate,接收后再次 Invalidate。
- 原子性:Clean/Invalidate 操作本身可能被中断打断,建议在临界区或 DMA 完成中断中执行。
- 多缓冲区:若使用双缓冲,每个缓冲区都要独立维护 Cache。
- 调试:若出现 HardFault,检查是否因 Invalidate 误伤了栈或全局变量。
-
性能:频繁 Clean/Invalidate 会降低性能,可考虑 MPU 非缓存区或使用
SCB_CleanInvalidateDCache_by_Addr。
遵循以上原则,STM32H7 的 D-Cache 与 DMA 就能和谐共处,既发挥高性能,又保证数据一致性。