一、为什么 DCache 会成为 DMA 的“猪队友”?
Cortex-M7 为了提升性能,引入了数据缓存(DCache)。CPU 访问内存时,若数据在 Cache 中则直接命中,避免访问慢速的 SRAM。但 DMA 控制器是独立于 CPU 的总线主设备,它直接读写物理内存,不经过 Cache。这就导致:
- CPU 写数据到 Cache,但尚未写回内存(Write-Back 模式),DMA 从内存读到的却是旧数据。
- DMA 将新数据写入内存,但 CPU 读到的仍是 Cache 中的旧副本。
这就是典型的数据一致性问题。STM32H7 默认开启 DCache,若不处理,DMA 传输将随机出错。
二、地址对齐:不仅仅是 4 字节
Cache 操作以 Cache Line 为单位,Cortex-M7 的 Cache Line 为 32 字节。使用 SCB_CleanDCache_by_Addr 等函数时,地址必须按 32 字节对齐,否则可能误伤相邻数据。
关键规则:
- DMA 缓冲区起始地址必须 32 字节对齐(推荐使用
__attribute__((aligned(32))))。 - 缓冲区大小最好是 32 字节的整数倍,避免 Clean/Invalidate 时波及无关变量。
- 若无法对齐,需手动计算覆盖范围,但强烈建议对齐。
// 推荐定义方式
__attribute__((aligned(32))) uint8_t dma_buffer[1024];
三、Clean 与 Invalidate 的时机
- Clean:将 Cache 中已修改的数据写回内存。用于 CPU 写、DMA 读 的场景(如发送数据)。
- Invalidate:丢弃 Cache 中的内容,强制 CPU 下次从内存读取。用于 DMA 写、CPU 读 的场景(如接收数据)。
- Clean+Invalidate:两者结合,用于双向传输或不确定场景。
正确操作顺序:
-
发送(CPU→DMA):
- CPU 填充缓冲区。
- 执行
SCB_CleanDCache_by_Addr(buf, len)。 - 启动 DMA 发送。
-
接收(DMA→CPU):
- 启动 DMA 接收前,执行
SCB_InvalidateDCache_by_Addr(buf, len)(防止 Cache 中有旧数据)。 - 等待 DMA 完成。
- 再次执行
SCB_InvalidateDCache_by_Addr(buf, len),确保 CPU 读取最新数据。
- 启动 DMA 接收前,执行
注意: Invalidate 会丢弃未写回的数据,若缓冲区在 DMA 传输期间被 CPU 修改,将丢失修改。因此接收缓冲区在 DMA 期间不应被 CPU 写入。
四、完整代码示例(以 UART DMA 接收为例)
#include "stm32h7xx.h"
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
volatile uint8_t dma_done = 0;
void uart_dma_init(void) {
// 初始化 UART 和 DMA(略)
// 启动接收前无效化 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) {
// 无效化 Cache,确保 CPU 读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
dma_done = 1;
}
// 发送示例
__attribute__((aligned(32))) uint8_t tx_buf[] = "Hello DMA";
void uart_send(void) {
// 清理 Cache,将 CPU 写入的数据同步到内存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, sizeof(tx_buf));
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf));
}
五、踩坑记录与注意事项
- 坑1:忘记 Invalidate 导致接收数据错乱。DMA 接收完成后,CPU 读到的仍是 Cache 中的旧数据。务必在 DMA 完成回调中执行 Invalidate。
-
坑2:缓冲区未对齐,Clean 时破坏相邻变量。曾有一个项目,
uint8_t buf[10]未对齐,Clean 32 字节导致其他变量被意外写回,引发随机死机。 - 坑3:在 DMA 传输过程中操作缓冲区。若 CPU 在 DMA 接收期间写缓冲区,Invalidate 会丢弃这些写入,导致数据丢失。应使用双缓冲或确保无冲突。
-
坑4:使用
SCB_CleanInvalidateDCache()全局操作。该函数影响整个 Cache,可能降低性能,且在多任务环境中可能引发问题。尽量使用按地址操作。 - 坑5:忽略 MPU 配置。若将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable,可简化一致性处理,但会牺牲性能。根据场景权衡。
最佳实践:
- 将 DMA 缓冲区放在非缓存区域(通过 MPU 配置),一劳永逸。
- 若必须使用 Cache,严格遵循 Clean/Invalidate 时机,并确保 32 字节对齐。
- 使用
SCB_InvalidateDCache_by_Addr前,确认地址和长度已按 32 字节对齐。 - 在 RTOS 中,注意 Cache 操作与任务调度的互斥。
六、总结
STM32H7 的 DCache 与 DMA 是一对需要精心协调的搭档。理解 Cache Line、对齐要求以及 Clean/Invalidate 的适用场景,是保证数据一致性的关键。实际开发中,建议优先考虑 MPU 配置非缓存区域,若使用 Cache 则严格遵循操作顺序,并利用调试工具验证数据流。避开这些坑,才能充分发挥 H7 的高性能。