一、为什么 DMA 与 D-Cache 会打架?
Cortex-M7 的 D-Cache 是**写回(write-back)**策略:CPU 写数据时只更新 Cache,不立即写回 SRAM。而 DMA 直接访问物理 SRAM,它看不到 Cache 里的新数据。
- CPU 写、DMA 读:数据还在 Cache 里,DMA 读到旧值。
- DMA 写、CPU 读:DMA 更新了 SRAM,但 CPU 读的是 Cache 里的旧副本。
解决思路只有两个:要么让该区域不走 Cache(MPU 配置为 Device/Strongly Ordered),要么在恰当时机手动 clean/invalidate。
二、MPU 配置:给 DMA 缓冲区划一块“安全区”
最稳妥的做法是把 DMA 缓冲区所在 SRAM 区域通过 MPU 配置为 Non-cacheable。以 STM32H743 的 0x30000000(D2 SRAM)为例:
#include "stm32h7xx.h"
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000;
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);
}
-
IsCacheable = NOT_CACHEABLE:CPU 访问直接读写 SRAM,与 DMA 一致。 -
IsShareable = SHAREABLE:保证多总线主控(CPU、DMA)之间的一致性。 - 注意:MPU 配置必须在
main()最开头、使能 Cache 之前调用。
三、clean 与 invalidate 的时机
如果不想牺牲性能(比如大块数据搬运),可以让缓冲区走 Cache,但必须手动维护一致性。CMSIS 提供两个函数:
-
SCB_CleanDCache_by_Addr(addr, size):把 Cache 里的脏数据写回 SRAM。 -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,下次读从 SRAM 取。
核心口诀:
-
CPU 写 → DMA 读:先
Clean,再启动 DMA。 -
DMA 写 → CPU 读:DMA 完成后先
Invalidate,再读数据。 - 地址必须 32 字节对齐,长度建议按 32 字节向上取整。
#define BUF_SIZE 256
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];
// CPU 准备数据发给 DMA 前
SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, BUF_SIZE * 2);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
// DMA 完成回调中
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, BUF_SIZE * 2);
// 现在可以安全读取 adc_buf
}
四、完整实战:ADC + DMA + D-Cache
#include "stm32h7xx.h"
#include "stm32h7xx_hal.h"
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
#define ADC_BUF_LEN 128
__attribute__((aligned(32))) uint16_t adc_dma_buf[ADC_BUF_LEN];
void SystemClock_Config(void);
void MX_DMA_Init(void);
void MX_ADC1_Init(void);
int main(void)
{
HAL_Init();
MPU_Config(); // 先配 MPU
SCB_EnableICache();
SCB_EnableDCache(); // 再开 Cache
SystemClock_Config();
MX_DMA_Init();
MX_ADC1_Init();
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buf, ADC_BUF_LEN);
while (1)
{
HAL_Delay(500);
// 若 MPU 已配 Non-cacheable,可直接读;否则需先 Invalidate
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_dma_buf, ADC_BUF_LEN * 2);
uint32_t sum = 0;
for (int i = 0; i < ADC_BUF_LEN; i++)
sum += adc_dma_buf[i];
uint16_t avg = sum / ADC_BUF_LEN;
(void)avg;
}
}
五、注意事项与避坑指南
-
对齐是硬性要求:
SCB_*DCache_by_Addr的地址和长度必须 32 字节对齐,否则触发 HardFault。 - 不要对同一区域重复 Invalidate:若 CPU 刚写过该区域且未 Clean,Invalidate 会丢失数据。
- MPU 与手动维护二选一:混用容易出错,建议 DMA 缓冲区统一走 Non-cacheable 区域。
- DMA 描述符也要注意:若使用链表模式,描述符所在内存同样需 Non-cacheable 或手动 Clean。
-
调试时先关 Cache:定位诡异问题时,先
SCB_DisableDCache()排除缓存因素。
掌握 MPU 配置与 clean/invalidate 时机,就能在 STM32H7 上既享受 D-Cache 的高性能,又保证 DMA 数据万无一失。