为什么 H7 上 DMA 会“丢数据”?
STM32H7 的 Cortex-M7 内核带有 L1 数据 Cache(D-Cache),默认写回(Write-Back)策略。当 CPU 写数据到 SRAM 时,数据可能只停留在 Cache 中,并未真正写入物理内存;当 CPU 读数据时,也可能直接从 Cache 拿到旧值,而忽略了 DMA 刚刚搬运到内存的新数据。
DMA 是“绕过 Cache”直接访问物理内存的。因此,CPU 与 DMA 看到的内存视图可能不一致,导致:
- CPU 写的数据 DMA 读不到(还在 Cache 里)
- DMA 写的数据 CPU 读不到(Cache 里是旧值)
解决手段就是两条指令:
-
SCB_CleanDCache_by_Addr():把 Cache 中的脏数据写回物理内存 -
SCB_InvalidateDCache_by_Addr():丢弃 Cache 中的旧数据,强制下次从内存重新加载
核心原则:谁写谁 Clean,谁读谁 Invalidate
记住一句话:Clean 用于“我写完了,交给别人读”;Invalidate 用于“别人写完了,我要读”。
具体到 DMA 场景:
- CPU 发送数据给外设(TX):CPU 先写缓冲区 → Clean → 启动 DMA
- CPU 接收外设数据(RX):启动 DMA 前 Invalidate(防止 Cache 有旧值)→ DMA 完成后再次 Invalidate → CPU 读数据
注意:Invalidate 会丢弃未写回的数据,所以绝不能对 CPU 刚写过但未 Clean 的缓冲区做 Invalidate,否则数据直接丢失。
配置步骤与完整代码
1. 使能 Cache 与 MPU 配置
H7 默认开启 D-Cache。建议用 MPU 将 DMA 缓冲区所在区域配置为 Write-Through 或 Non-Cacheable,可大幅简化一致性处理。
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 = 0x30000000; // D2 SRAM
MPU_Init.Size = MPU_REGION_SIZE_64KB;
MPU_Init.SubRegionDisable = 0x00;
MPU_Init.TypeExtField = MPU_TEX_LEVEL1;
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_NOT_BUFFERABLE;
HAL_MPU_ConfigRegion(&MPU_Init);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
2. 发送(TX)流程:Clean 后启动 DMA
#define TX_LEN 256
alignas(32) uint8_t tx_buf[TX_LEN]; // 必须 32 字节对齐
void UART_DMA_Send(void)
{
for (int i = 0; i < TX_LEN; i++) tx_buf[i] = i;
// 1. 将 Cache 中的新数据写回物理内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, TX_LEN);
// 2. 启动 DMA
HAL_UART_Transmit_DMA(&huart1, tx_buf, TX_LEN);
}
3. 接收(RX)流程:Invalidate 两次
#define RX_LEN 256
alignas(32) uint8_t rx_buf[RX_LEN];
void UART_DMA_StartRecv(void)
{
// 启动前 Invalidate,清除 Cache 中可能存在的旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_LEN);
}
// DMA 完成回调中再次 Invalidate
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, RX_LEN);
// 此时 CPU 读取 rx_buf 才是 DMA 写入的最新数据
ProcessData(rx_buf, RX_LEN);
}
常见坑点与注意事项
-
地址与长度必须 32 字节对齐:
SCB_*Cache_by_Addr要求地址对齐到 32 字节,长度建议向上取整到 32 的倍数,否则可能误伤相邻数据。 - Invalidate 前必须确保没有未 Clean 的写操作:否则会丢弃 CPU 刚写的数据。RX 缓冲区在启动 DMA 前若被 CPU 写过,应先 Clean 再 Invalidate。
- DMA 描述符/链表也要注意:如果使用带描述符的 DMA,描述符本身也需放在非缓存区或手动 Clean。
- 多缓冲区(Double Buffer):每个缓冲区独立 Clean/Invalidate,不要图省事对整个大区域操作。
- 性能权衡:频繁 Clean/Invalidate 有开销。对高吞吐场景,优先用 MPU 配置 Non-Cacheable 区域,从根源避免一致性问题。
-
调试陷阱:在调试器下查看内存可能触发 Cache 行为异常,建议用
__DSB()确保操作完成后再观察。
小结
STM32H7 的 Cache 与 DMA 一致性并不复杂,关键是牢记 “CPU 写→Clean,CPU 读→Invalidate”,并保证地址对齐。若项目对性能要求不极致,直接用 MPU 把 DMA 缓冲区设为 Non-Cacheable 是最省心的方案。掌握这两招,H7 的 DMA 就能稳定跑起来。