一、为什么 H7 上 DMA 会“偷走”你的数据?
STM32H7 的 Cortex-M7 内核带有 D-Cache(数据缓存)和 I-Cache。当 CPU 访问内存时,数据可能被缓存在 D-Cache 中,而 DMA 直接访问物理内存(SRAM/SDRAM)。如果 CPU 写数据后未回写(Clean),DMA 读到的就是旧值;如果 DMA 写数据后 CPU 仍读缓存中的旧值,就需要 Invalidate。
典型现象:
- ADC/DMA 采集的数据,CPU 读出来全是 0 或不变。
- 串口 DMA 发送的数据错位、重复。
- 以太网收包后协议栈解析异常。
根本原因:Cache 与 DMA 看到的内存视图不一致。
二、MPU 配置:给 DMA 缓冲区“开小灶”
最稳妥的方案是用 MPU 将 DMA 缓冲区配置为 Non-Cacheable 或 Write-Through,让 CPU 和 DMA 看到同一份数据。
2.1 MPU 区域规划
- 区域 0:0x30000000(SRAM1),大小 128KB,属性 Normal Non-Cacheable。
- 区域 1:0x30020000(SRAM2),大小 128KB,属性 Normal Write-Through。
2.2 配置代码(基于 HAL)
#include "stm32h7xx_hal.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 配置 SRAM1 为 Non-Cacheable */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 配置 SRAM2 为 Write-Through */
MPU_InitStruct.BaseAddress = 0x30020000;
MPU_InitStruct.Number = MPU_REGION_NUMBER1;
MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
注意:MPU 配置必须在 main() 最开始、使能 Cache 之前调用。
三、Clean / Invalidate 的正确使用
如果不想牺牲性能,可将缓冲区设为 Cacheable,但必须在 DMA 传输前后手动维护一致性。
3.1 关键 API
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 数据写回内存。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 数据,强制从内存重读。 -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。
3.2 方向决定操作
-
CPU 写 → DMA 读(如发送):先
Clean。 -
DMA 写 → CPU 读(如接收):先
Invalidate。 -
双向:先
CleanInvalidate。
3.3 实战代码:ADC DMA 采集
#define ADC_BUF_SIZE 256
__attribute__((aligned(32))) uint16_t adc_buf[ADC_BUF_SIZE];
void ADC_DMA_Start(void)
{
/* 启动前 Invalidate,确保 CPU 不读旧缓存 */
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_SIZE);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/* 传输完成后 Invalidate,再读取数据 */
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
for (int i = 0; i < ADC_BUF_SIZE; i++) {
process(adc_buf[i]);
}
}
易错点:
- 地址必须 32 字节对齐,否则
SCB_*函数可能无效或触发 HardFault。 - 长度必须是 32 的倍数,建议用
__attribute__((aligned(32)))。 - 不要在 DMA 传输过程中 Invalidate,否则可能丢失未回写数据。
四、排查步骤与注意事项
4.1 快速定位
- 确认 DMA 缓冲区地址是否落在 MPU 配置的 Non-Cacheable 区域。
- 若为 Cacheable,检查传输前后是否调用了正确的 Clean/Invalidate。
- 用
SCB_InvalidateDCache()全局失效测试,若问题消失则确认是 Cache 问题。 - 检查链接脚本,确保缓冲区未被分配到 DTCM(DTCM 不经过 Cache,但 DMA 无法访问)。
4.2 注意事项
- DTCM 与 DMA:DTCM 地址 0x20000000 不能被大多数 DMA 访问,必须放在 AXI SRAM 或 SDRAM。
- 多缓冲区:每个缓冲区独立维护,不要跨区域操作。
- 中断优先级:DMA 完成中断中操作 Cache 要快,避免影响实时性。
- 性能权衡:Non-Cacheable 简单但降低 CPU 访问速度;Clean/Invalidate 高效但易错。
五、总结
STM32H7 的 Cache 与 DMA 一致性没有“银弹”,推荐优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠。若必须用 Cacheable,务必牢记:CPU 写后 Clean,DMA 写后 Invalidate,地址对齐 32 字节。掌握这些,你就能避开 H7 上最经典的“数据错乱”坑。