一、问题现象:偶发数据错位,重启就好?
在 STM32H7 上使用 DMA 搬运 ADC 采样、串口收发或外设数据时,常遇到:
- 数据偶尔错位、丢包,概率性出现,重启后可能消失;
- 调试器单步运行正常,全速运行就出错;
- 关闭 D-Cache 后一切正常,开启后问题复现。
这类现象几乎都指向 D-Cache 与 DMA 的一致性问题。Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的,CPU 访问的是 Cache,而 DMA 直接访问物理内存(SRAM)。两者看到的数据可能不一致。
二、原理:为什么 Cache 会让 DMA 数据“错位”
2.1 写回与写分配
- 写回:CPU 写数据只更新 Cache,不立即写回 SRAM,直到该 Cache Line 被替换或手动 Clean。
- 写分配:CPU 写未命中时,先把该行从 SRAM 读入 Cache,再修改。
2.2 DMA 与 Cache 的冲突场景
- 发送方向(CPU 写 → DMA 读):CPU 把数据写入缓冲区,数据可能还在 Cache 中未写回 SRAM,DMA 从 SRAM 读到的就是旧数据。
- 接收方向(DMA 写 → CPU 读):DMA 把新数据写入 SRAM,但 CPU 的 Cache 中可能还缓存着该地址的旧数据,CPU 读到的仍是旧值。
2.3 Clean 与 Invalidate 的正确语义
- Clean(清理):把 Cache 中已修改的数据写回 SRAM。用于 CPU 写、DMA 读 之前。
- Invalidate(无效化):丢弃 Cache 中的内容,强制下次从 SRAM 重新读取。用于 DMA 写、CPU 读 之前。
- Clean+Invalidate:既写回又无效化,用于双向缓冲区或不确定场景。
关键:Invalidate 会丢弃未写回的数据!如果 CPU 刚写过缓冲区就 Invalidate,数据会丢失,这正是“误用”的根源。
三、排查步骤:定位一致性 bug
-
确认 D-Cache 是否开启:读取
SCB->CCR的 DC 位,或检查SCB_EnableDCache()是否被调用。 - 确认 DMA 缓冲区地址与大小:是否按 32 字节对齐(Cache Line 大小),大小是否为 32 字节整数倍。
-
检查操作顺序:
- 发送前是否对缓冲区做了
SCB_CleanDCache_by_Addr()? - 接收后是否做了
SCB_InvalidateDCache_by_Addr()? - 是否在 CPU 写之后、DMA 启动之前错误地调用了 Invalidate?
- 发送前是否对缓冲区做了
- 用调试器观察 SRAM 与 Cache:在 DMA 启动前后分别查看物理内存,对比 CPU 读到的值。
- 临时关闭 D-Cache 验证:若问题消失,基本可确认是一致性问题。
四、正确配置与完整代码示例
4.1 缓冲区定义:对齐到 Cache Line
#include "stm32h7xx.h"
#define BUF_SIZE 256
#define CACHE_LINE 32
/* 按 32 字节对齐,避免跨 Cache Line 操作 */
__attribute__((aligned(CACHE_LINE))) uint8_t dma_tx_buf[BUF_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t dma_rx_buf[BUF_SIZE];
4.2 发送方向:CPU 写 → Clean → 启动 DMA
void dma_send(uint8_t *data, uint16_t len)
{
/* 1. CPU 填充数据到缓冲区 */
memcpy(dma_tx_buf, data, len);
/* 2. 将 Cache 中已修改的数据写回 SRAM */
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
/* 3. 启动 DMA 传输 */
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)dma_tx_buf,
(uint32_t)&USART1->TDR, len);
}
4.3 接收方向:DMA 完成 → Invalidate → CPU 读
/* DMA 接收完成回调 */
void dma_rx_complete_callback(uint16_t len)
{
/* 1. 无效化 Cache,丢弃旧数据,强制从 SRAM 重新读取 */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, len);
/* 2. 此时 CPU 读取 dma_rx_buf 才能拿到 DMA 写入的新数据 */
process_data(dma_rx_buf, len);
}
4.4 双向缓冲区:Clean + Invalidate
void dma_bidirectional(uint16_t len)
{
/* 发送前:Clean 确保 SRAM 数据最新 */
SCB_CleanDCache_by_Addr((uint32_t *)dma_tx_buf, len);
HAL_DMA_Start(&hdma, (uint32_t)dma_tx_buf,
(uint32_t)dma_rx_buf, len);
/* 等待传输完成... */
/* 接收后:Invalidate 确保 CPU 读到新数据 */
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_rx_buf, len);
}
五、注意事项与避坑指南
-
地址与长度必须 32 字节对齐:
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr要求地址 32 字节对齐,长度建议为 32 的整数倍,否则可能影响相邻数据。 - Invalidate 前确保 CPU 不再写该缓冲区:否则未写回的数据会丢失,导致“数据错位”。
- 不要在 DMA 传输过程中操作 Cache:传输中 Clean/Invalidate 可能破坏正在传输的数据。
- 使用 MPU 配置 Non-Cacheable 区域:对于频繁 DMA 的缓冲区,可将其配置为 Non-Cacheable,从根本上避免一致性问题,但会牺牲性能。
- DMA 描述符与缓冲区分离:若使用链表式 DMA,描述符本身也需注意 Cache 一致性。
- 中断优先级与 Cache 操作:Cache 维护操作本身不是原子的,避免在中断中频繁调用。
- 调试时关闭优化:高优化等级可能重排指令,掩盖或放大问题,排查时建议先降优化。
六、总结
STM32H7 的 D-Cache 是性能利器,也是 DMA 一致性的“暗礁”。核心原则只有一条:CPU 写、DMA 读之前 Clean;DMA 写、CPU 读之前 Invalidate。牢记操作顺序、对齐要求,并在必要时用 MPU 将 DMA 缓冲区设为 Non-Cacheable,就能彻底告别偶发数据错位。遇到诡异问题时,先问自己:Cache 维护做对了吗?