一、为什么 DCache 会让 DMA 数据“失控”?
STM32H7 主频高达 480MHz,为了匹配内核速度,芯片内置了 L1 Cache(I-Cache 和 D-Cache)。当 CPU 访问内存时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存(SRAM),不经过 Cache。
这就导致两个经典问题:
- CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 里(Write-Back 模式),内存中还是旧数据,DMA 发送出去的是错误内容。
- DMA 写,CPU 读:DMA 把新数据写入内存,但 CPU 读到的却是 Cache 中的旧副本。
要解决这个问题,必须手动维护 Cache 与内存的一致性。
二、地址对齐:Cache Line 的硬性约束
Cortex-M7 的 D-Cache 按 Cache Line 管理,每条 Line 大小为 32 字节。所有 Clean/Invalidate 操作都以 32 字节为最小单位。
关键规则:
- DMA 缓冲区地址必须 32 字节对齐。
- 缓冲区大小最好是 32 字节的整数倍。
- 如果缓冲区跨越了多个 Cache Line,操作时必须覆盖所有涉及的 Line。
错误示例:
uint8_t dma_buf[100]; // 地址可能未对齐,大小也不是32的倍数
正确做法:
// 使用编译器属性强制32字节对齐
__attribute__((aligned(32))) uint8_t dma_buf[128];
// 或者使用 C11 标准
#include <stdalign.h>
alignas(32) uint8_t dma_buf[128];
注意:即使缓冲区起始地址对齐,如果大小不是 32 的倍数,末尾的 Cache Line 仍可能包含其他变量。Invalidate 时会把其他变量的修改丢弃,导致隐蔽 Bug。
三、Clean 与 Invalidate:顺序决定成败
3.1 核心概念
- Clean:将 Cache 中“脏”数据写回内存。
- Invalidate:将 Cache 中的内容标记为无效,下次读取时从内存重新加载。
3.2 两种典型场景
场景一:CPU 发送数据给 DMA(如 UART 发送)
- CPU 填充缓冲区。
- Clean 缓冲区对应的 Cache Line,确保数据写入内存。
- 启动 DMA 发送。
// 填充数据
memcpy(dma_buf, src, len);
// 清理 D-Cache,将数据写回内存
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, len);
// 启动 DMA
HAL_UART_Transmit_DMA(&huart1, dma_buf, len);
场景二:DMA 接收数据给 CPU(如 UART 接收)
- 启动 DMA 接收前,Invalidate 缓冲区,丢弃 Cache 中的旧数据。
- 等待 DMA 完成。
- 再次 Invalidate,确保 CPU 读取到 DMA 写入的新数据。
// 启动接收前,无效化 Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, len);
HAL_UART_Receive_DMA(&huart1, dma_buf, len);
// 等待完成...
// 接收完成后,再次无效化
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, len);
// 现在可以安全读取 dma_buf
3.3 顺序陷阱
- 先 Invalidate 再 Clean 是危险的:如果缓冲区有未写回的数据,Invalidate 会直接丢弃,造成数据丢失。
- DMA 接收前必须 Invalidate:否则 CPU 可能读到 Cache 中的旧数据,覆盖 DMA 写入的新数据。
- DMA 发送前必须 Clean:否则 DMA 读到的是内存中的旧数据。
四、完整代码示例:UART DMA 回环测试
#include "stm32h7xx_hal.h"
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
void DMA_Send(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
// 清理 D-Cache,确保数据写入内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
void DMA_Receive_Start(void) {
// 无效化 D-Cache,丢弃旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
// DMA 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
// 再次无效化,确保 CPU 读取到新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
// 处理数据...
}
五、常见踩坑与注意事项
-
坑1:忘记对齐。未对齐的缓冲区会导致 Clean/Invalidate 操作影响到相邻变量,引发随机错误。务必使用
__attribute__((aligned(32)))。 - 坑2:Invalidate 前未 Clean。如果缓冲区有 CPU 写入的数据尚未写回,Invalidate 会直接丢弃,造成数据丢失。
- 坑3:DMA 传输中访问缓冲区。DMA 工作时 CPU 不应读写该缓冲区,否则 Cache 状态混乱。
-
坑4:使用
SCB_CleanInvalidateDCache()全局操作。该函数会影响整个 Cache,可能破坏其他数据,应使用by_Addr版本。 - 坑5:忽略 MPU 配置。可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题。例如:
MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // SRAM 地址
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
- 坑6:中断中调用 Cache 维护函数。这些函数执行时间较长,可能影响实时性,建议在 DMA 完成回调中处理。
六、总结
STM32H7 的 DCache 与 DMA 协同工作,核心就是 对齐、Clean、Invalidate 三件事。牢记:
- DMA 发送前 Clean,接收前和接收后 Invalidate。
- 缓冲区 32 字节对齐,大小取 32 的倍数。
- 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
掌握这些原则,就能让 H7 的性能与稳定性兼得。