为什么 STM32H7 的 DMA 会“丢数据”?
STM32H7 主频高达 480MHz,为了匹配内核速度,Cortex-M7 引入了 D-Cache(数据缓存)。CPU 访问数据时先查 Cache,命中则直接返回,不访问 SRAM。而 DMA 是独立于 CPU 的总线主设备,它直接读写 SRAM,不经过 Cache。
这就带来一个经典问题:
- CPU 写数据到缓冲区,数据可能还在 Cache 中,未写回 SRAM,DMA 读到的就是旧数据。
- DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧副本,看不到新数据。
因此,DMA 与 D-Cache 共存时,必须做一致性维护。
一致性问题的三个层面
1. 地址对齐与 Cache Line
Cortex-M7 的 D-Cache Line 大小通常为 32 字节。Cache 维护操作(clean/invalidate)以 Cache Line 为单位。
- 如果缓冲区起始地址或长度不是 32 字节对齐,clean/invalidate 可能影响相邻变量,造成“误伤”。
- 例如:一个 10 字节的 DMA 接收缓冲区,若未对齐,invalidate 时会把同一 Cache Line 中其他变量也失效,导致数据丢失。
建议:DMA 缓冲区使用 __attribute__((aligned(32))) 强制 32 字节对齐,长度也尽量按 32 字节对齐。
2. Cache 维护操作:Clean 与 Invalidate
- Clean:将 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读的场景。
- Invalidate:丢弃 Cache 中的副本,强制下次从 SRAM 读取。用于 DMA 写、CPU 读的场景。
- Clean+Invalidate:两者都做,用于双向传输。
注意:invalidate 前必须确保 Cache 中没有未写回的数据,否则会丢失 CPU 的修改。
3. MPU 配置:把 DMA 缓冲区设为 Non-Cacheable
更彻底的做法是用 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through。这样 CPU 访问该区域时不经过 D-Cache,从根本上避免一致性问题。
- 优点:无需手动维护 Cache,代码简单。
- 缺点:CPU 访问该区域变慢,适合 DMA 频繁但 CPU 计算不多的缓冲区。
完整排查链路
- 确认 DMA 缓冲区地址是否 32 字节对齐。
- 确认是否在 DMA 传输前后正确执行了 Cache 维护。
- 确认 MPU 是否将 DMA 区域配置为 Non-Cacheable。
- 确认 DMA 传输方向与 Cache 操作匹配。
- 使用调试器查看 SRAM 与 Cache 内容,验证一致性。
代码示例:ADC + DMA + D-Cache 安全采集
以下示例使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,并保留手动维护接口。
#include "stm32h7xx.h"
#define ADC_DMA_BUFFER_SIZE 256
/* 32 字节对齐,避免 Cache Line 误伤 */
__attribute__((aligned(32)))
uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
/* MPU 配置:将 adc_dma_buffer 所在区域设为 Non-Cacheable */
void MPU_Config_DMA_NonCacheable(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
HAL_MPU_Disable();
/* 区域基地址必须按区域大小对齐,这里取缓冲区地址向下对齐到 32 字节 */
uint32_t base = ((uint32_t)adc_dma_buffer) & ~0x1F;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = base;
MPU_InitStruct.Size = MPU_REGION_SIZE_256B; /* 根据实际大小调整 */
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键:Non-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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
/* 若未使用 MPU,可手动维护 Cache */
void DMA_Buffer_Clean(void *addr, uint32_t size)
{
SCB_CleanDCache_by_Addr((uint32_t *)addr, size);
}
void DMA_Buffer_Invalidate(void *addr, uint32_t size)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size);
}
/* ADC + DMA 初始化 */
void ADC_DMA_Init(void)
{
/* 1. 配置 MPU,使缓冲区 Non-Cacheable */
MPU_Config_DMA_NonCacheable();
/* 2. 初始化 DMA,目标地址为 adc_dma_buffer */
/* ... HAL_DMA_Init ... */
/* 3. 启动 ADC DMA 循环采集 */
/* HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buffer, ADC_DMA_BUFFER_SIZE); */
}
/* 若使用 Cacheable 缓冲区,DMA 启动前需 Clean,DMA 完成后需 Invalidate */
void ADC_DMA_Start_WithCache(void)
{
/* CPU 可能修改过缓冲区,先 Clean 写回 */
DMA_Buffer_Clean(adc_dma_buffer, sizeof(adc_dma_buffer));
/* 启动 DMA */
/* HAL_ADC_Start_DMA(...); */
}
void ADC_DMA_Complete_Callback(void)
{
/* DMA 写入新数据,Invalidate 使 CPU 读取 SRAM 新值 */
DMA_Buffer_Invalidate(adc_dma_buffer, sizeof(adc_dma_buffer));
/* 此时 CPU 读取 adc_dma_buffer 才是最新数据 */
}
注意事项
- 对齐是前提:DMA 缓冲区必须 32 字节对齐,否则 Cache 维护会误伤相邻数据。
- Clean 与 Invalidate 顺序:CPU 写后 DMA 读,先 Clean;DMA 写后 CPU 读,先 Invalidate。
- MPU 区域大小:MPU 区域大小必须是 2 的幂,且基地址按大小对齐。
- 不要对同一区域同时使用 MPU Non-Cacheable 和手动维护:二者选其一,避免重复操作。
- DMA 描述符与缓冲区分离:若使用链表,描述符本身也要注意对齐和 Cache 维护。
- 调试技巧:在调试器中查看 SRAM 地址内容,对比 CPU 读取值,可快速定位一致性问题。
小结
STM32H7 的 D-Cache 与 DMA 一致性问题的核心是“CPU 与 DMA 看到的内存视图不同”。解决思路有三层:地址对齐、Cache 维护、MPU 配置。实际项目中,推荐将 DMA 缓冲区用 MPU 设为 Non-Cacheable,简单可靠;若必须使用 Cacheable 内存,则严格按方向执行 Clean/Invalidate,并保证 32 字节对齐。掌握这条完整链路,就能让 H7 的高性能与 DMA 的实时性兼得。