一、为什么 Cache 会成为 DMA 的“猪队友”?
STM32H7 搭载 Cortex-M7,主频可达 480MHz,为了弥补 CPU 与存储器之间的速度鸿沟,芯片内部集成了 L1 数据 Cache(D-Cache) 和 指令 Cache(I-Cache)。Cache 的工作逻辑很简单:CPU 访问内存时,先把数据从主存读到 Cache 行(通常 32 字节),之后读写都优先命中 Cache,从而减少访问延迟。
但 DMA 是“独立于 CPU 的搬运工”,它直接访问物理内存,不经过 Cache。这就导致两种典型问题:
- 读旧数据:CPU 刚更新了内存,数据还在 Cache 里没写回主存,DMA 却从主存读到旧值。
- 写丢失:DMA 把新数据写入主存,但 CPU 访问时命中了 Cache 中的旧数据,看不到更新。
一句话总结:Cache 和 DMA 看到的内存可能不是同一份。
二、STM32H7 的 Cache 维护机制
Cortex-M7 提供了维护 Cache 一致性的指令,HAL 库封装为以下函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 内容写回主存(Clean),用于 CPU 写、DMA 读 的场景。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从主存读取(Invalidate),用于 DMA 写、CPU 读 的场景。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先写回再无效,用于双向传输。
关键细节:操作地址必须按 32 字节对齐,长度也建议向上取整到 32 字节,否则会误伤相邻数据。
三、实战配置:MPU + Cache 维护
最稳妥的方案是 MPU 配置 DMA 缓冲区为 Write-Through 或 Non-Cacheable,从根源避免一致性问题。以下以 ADC + DMA 采集为例。
3.1 MPU 配置(推荐)
#include "stm32h7xx_hal.h"
// 定义 DMA 缓冲区,强制 32 字节对齐
__attribute__((aligned(32))) uint16_t adc_dma_buf[256];
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_Init = {0};
MPU_Init.Enable = MPU_REGION_ENABLE;
MPU_Init.Number = MPU_REGION_NUMBER0;
MPU_Init.BaseAddress = (uint32_t)adc_dma_buf;
MPU_Init.Size = MPU_REGION_SIZE_1KB; // 根据缓冲区大小调整
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL0;
MPU_Init.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_Init.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
MPU_Init.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_Init.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:不缓存
MPU_Init.IsBufferable = MPU_ACCESS_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.2 手动维护 Cache(灵活方案)
若不想用 MPU,必须在 DMA 传输前后手动维护:
// ADC 采集完成回调(DMA 写内存,CPU 读)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 无效化 Cache,确保 CPU 读到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, sizeof(adc_dma_buf));
// 此时再处理 adc_dma_buf 数据
}
// 发送数据前(CPU 写内存,DMA 读)
void UART_DMA_Send(uint8_t *data, uint16_t len)
{
// 将 Cache 内容写回主存,确保 DMA 读到最新数据
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
HAL_UART_Transmit_DMA(&huart1, data, len);
}
四、完整示例:ADC + DMA + Cache 维护
#include "stm32h7xx_hal.h"
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
__attribute__((aligned(32))) uint16_t adc_buf[128];
volatile uint8_t adc_ready = 0;
void ADC_DMA_Init(void)
{
// ADC 配置省略,重点看 DMA
hdma_adc1.Instance = DMA1_Stream0;
hdma_adc1.Init.Request = DMA_REQUEST_ADC1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 128);
}
// DMA 半传输/完成回调中处理数据
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, 64 * sizeof(uint16_t));
adc_ready = 1;
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)&adc_buf[64], 64 * sizeof(uint16_t));
adc_ready = 2;
}
五、实战排查清单
遇到 DMA 数据错乱时,按以下顺序排查:
-
确认缓冲区对齐:DMA 缓冲区必须 32 字节对齐,使用
__attribute__((aligned(32)))。 - 检查 MPU 配置:DMA 缓冲区是否被配置为 Non-Cacheable 或 Write-Through。
- 核对维护时机:CPU 读之前 Invalidate,CPU 写之后 Clean,顺序不能反。
- 注意地址范围:维护的地址和长度必须覆盖整个 DMA 传输区域,且 32 字节对齐。
- 区分读写方向:外设到内存用 Invalidate,内存到外设用 Clean,双向用 CleanInvalidate。
- 中断优先级:DMA 中断优先级不要高于系统关键中断,避免维护操作被抢占导致时序错乱。
六、注意事项
-
不要频繁全 Cache 维护:
SCB_InvalidateDCache()会清空整个 D-Cache,严重影响性能,务必使用按地址维护的版本。 - 多缓冲区场景:若使用双缓冲,每个缓冲区都要独立维护,不能只维护首地址。
- DMA 与 CPU 竞争:维护 Cache 期间若 DMA 仍在传输,可能读到半新半旧数据,建议在 DMA 传输完成中断中操作。
- 调试时关闭 Cache:初期调试可暂时关闭 D-Cache 验证功能,确认无误后再开启并加入维护逻辑。
掌握 Cache 与 DMA 的协作规则,是 STM32H7 高性能应用开发的必修课。合理使用 MPU 配置 + 按地址维护,既能享受 Cache 带来的性能红利,又能保证数据一致性。