STM32H7 的 DCache 与 DMA 数据一致性:地址对齐、Clean/Invalidate 时机与实测踩坑
一、为什么 DCache 会让 DMA 数据“出错”?
STM32H7 的 Cortex-M7 内核带有一级数据缓存(DCache),默认写回(Write-Back)、写分配(Write-Allocate)。CPU 访问内存时,数据可能只停留在 Cache 中,并未写入实际 RAM。而 DMA 控制器直接访问物理内存,不经过 Cache。这就导致:
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 里,DMA 从 RAM 读到的却是旧数据。
- DMA 写,CPU 读:DMA 把新数据写入 RAM,但 CPU 读到的却是 Cache 中的旧数据。
解决思路只有两个:Clean(将 Cache 写回 RAM)和 Invalidate(丢弃 Cache 内容,强制从 RAM 重读)。但操作时机和地址对齐有严格要求。
二、地址对齐:32 字节是硬性要求
Cortex-M7 的 Cache 行大小为 32 字节。SCB_CleanDCache_by_Addr 和 SCB_InvalidateDCache_by_Addr 要求地址按 32 字节对齐,长度也建议为 32 的整数倍。否则可能误伤相邻数据,导致其他变量被意外清除或写回。
正确做法:DMA 缓冲区使用 __attribute__((aligned(32))) 强制对齐,且长度向上取整到 32 的倍数。
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t dma_tx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32))) uint8_t dma_rx_buf[DMA_BUF_SIZE];
如果缓冲区是动态分配的,需手动对齐地址和长度:
uint32_t aligned_addr = (uint32_t)buf & ~0x1F; // 向下对齐到 32 字节
uint32_t aligned_size = ((uint32_t)buf + len + 31) & ~0x1F; // 向上取整
三、Clean 与 Invalidate 的正确时机
3.1 CPU 发送数据给 DMA(TX)
- CPU 填充缓冲区。
- Clean 缓冲区,确保数据写入 RAM。
- 启动 DMA 发送。
- DMA 传输完成中断中,无需额外操作(但若缓冲区会被复用,建议再次 Clean 或等待)。
void dma_send(uint8_t *data, uint16_t len) {
memcpy(dma_tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, len);
HAL_DMA_Start(&hdma_memtomem, (uint32_t)dma_tx_buf, (uint32_t)dest, len);
}
3.2 DMA 接收数据给 CPU(RX)
- 启动 DMA 接收前,Invalidate 缓冲区,丢弃可能存在的旧 Cache 行。
- 启动 DMA。
- DMA 完成中断中,再次 Invalidate 缓冲区,确保 CPU 读到最新数据。
void dma_receive_start(void) {
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
HAL_DMA_Start_IT(&hdma, (uint32_t)src, (uint32_t)dma_rx_buf, DMA_BUF_SIZE);
}
void HAL_DMA_ConvCpltCallback(DMA_HandleTypeDef *hdma) {
SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, DMA_BUF_SIZE);
// 此时 dma_rx_buf 中的数据是有效的
}
关键点:Invalidate 必须放在 DMA 启动前和完成后各一次。启动前是为了防止 Cache 中的脏数据在 DMA 写入后被意外写回覆盖新数据;完成后是为了让 CPU 读取到 DMA 写入的新数据。
四、实测踩坑记录
坑 1:忘记 Clean,DMA 发送旧数据
现象:串口 DMA 发送字符串,第一次正常,修改缓冲区后再次发送,收到的还是旧内容。
原因:CPU 修改的数据在 Cache 中,DMA 从 RAM 读旧值。
解决:发送前调用 SCB_CleanDCache_by_Addr。
坑 2:Invalidate 时机错误,数据被覆盖
现象:DMA 接收完成后立即 Invalidate,但偶尔数据错乱。 原因:在 DMA 传输过程中,CPU 可能访问了缓冲区,导致 Cache 中产生脏行。DMA 完成后 Invalidate 会丢弃这些脏行,但若脏行在 DMA 写入之后才写回,就会覆盖 DMA 数据。 解决:确保 DMA 传输期间 CPU 不访问该缓冲区,或在启动 DMA 前先 Clean 再 Invalidate。
坑 3:地址未对齐,相邻变量被清除
现象:Invalidate 后,一个无关的全局变量值变为 0。 原因:缓冲区未按 32 字节对齐,Invalidate 操作覆盖了相邻变量所在的 Cache 行。 解决:强制 32 字节对齐,并确保长度是 32 的倍数。
坑 4:MPU 配置不当,Cache 策略冲突
现象:即使正确 Clean/Invalidate,数据仍偶尔出错。 原因:MPU 将 DMA 缓冲区配置为 Write-Back 但未正确设置共享属性。 解决:将 DMA 缓冲区所在内存区域配置为 Non-Cacheable 或 Write-Through,可彻底避免一致性问题(但会牺牲性能)。
五、最佳实践总结
- 对齐:DMA 缓冲区必须 32 字节对齐,长度取整到 32 的倍数。
- TX:CPU 写完后 Clean,再启动 DMA。
- RX:启动 DMA 前 Invalidate,DMA 完成后再次 Invalidate。
- 避免 CPU 访问:DMA 传输期间不要读写缓冲区。
- MPU 辅助:对性能要求不高的缓冲区,直接配置为 Non-Cacheable 最省心。
-
使用 CMSIS 函数:
SCB_CleanDCache_by_Addr、SCB_InvalidateDCache_by_Addr是标准做法。
六、完整示例:串口 DMA 回环测试
#include "stm32h7xx.h"
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
void uart_dma_init(void) {
// 初始化 UART 和 DMA(略)
}
void uart_send_dma(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
void uart_receive_dma(void) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUF_SIZE);
// 处理 rx_buf 数据
uart_send_dma(rx_buf, BUF_SIZE); // 回环发送
uart_receive_dma(); // 重新启动接收
}
七、结语
DCache 与 DMA 的一致性问题看似复杂,但只要掌握 对齐、Clean/Invalidate 时机、避免并发访问 三原则,就能稳定运行。建议在项目初期就规划好 DMA 缓冲区的内存布局,必要时用 MPU 将缓冲区设为 Non-Cacheable,可大幅降低调试成本。