STM32H7 的 D-Cache 与 DMA 一致性:地址对齐、Clean/Invalidate 时机与实测数据损坏复现
STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。但为了弥合 CPU 与存储器之间的速度鸿沟,Cortex-M7 引入了 D-Cache(数据缓存)。当 DMA 直接访问内存时,D-Cache 的存在会导致 CPU 与 DMA 看到的数据不一致,从而引发难以调试的数据损坏问题。本文将深入剖析这一问题的根源,并给出完整的解决方案。
一、为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。这意味着:
- CPU 写数据:先写入 Cache,并不立即同步到主存。
- CPU 读数据:若 Cache 命中,直接返回 Cache 内容,不访问主存。
- DMA 传输:直接读写主存(SRAM、外设缓冲区),完全绕过 Cache。
因此,当 CPU 修改了位于 Cache 中的缓冲区,然后启动 DMA 发送时,DMA 从主存读到的是旧数据;反之,DMA 将新数据写入主存后,CPU 若从 Cache 读取,得到的仍是旧数据。这就是数据一致性问题。
二、地址对齐:Cache Line 的硬性要求
Cortex-M7 的 D-Cache Line 大小为 32 字节。所有 Cache 维护操作(Clean/Invalidate)都以 Cache Line 为单位。如果缓冲区地址或大小未按 32 字节对齐,维护操作会波及相邻数据,导致意外损坏。
关键规则:
- DMA 缓冲区起始地址必须 32 字节对齐。
- 缓冲区大小应为 32 字节的整数倍。
- 若无法满足,可将缓冲区放在非缓存区域(如通过 MPU 配置)。
在代码中,使用 __attribute__((aligned(32))) 确保对齐:
// 定义 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];
三、Clean 与 Invalidate 的时机
- Clean(清理):将 Cache 中已修改的数据写回主存。用于 CPU 写、DMA 读 的场景(如发送数据)。
- Invalidate(无效化):丢弃 Cache 中的内容,强制下次读取时从主存加载。用于 DMA 写、CPU 读 的场景(如接收数据)。
- Clean & Invalidate:两者结合,用于双向传输或不确定数据状态的场景。
黄金法则:
- DMA 发送前:对缓冲区执行 Clean 操作。
- DMA 接收后:对缓冲区执行 Invalidate 操作。
- 操作地址必须 32 字节对齐,长度向上取整到 32 字节倍数。
四、实测:数据损坏复现
以下代码演示一个典型的错误场景:CPU 准备数据后启动 DMA 发送,但未执行 Clean 操作。
// 错误示例:未 Clean 导致 DMA 发送旧数据
__attribute__((aligned(32))) uint8_t tx_buffer[64];
void send_data_wrong(void) {
for (int i = 0; i < 64; i++) {
tx_buffer[i] = i; // CPU 写入数据,仅更新 Cache
}
// 启动 DMA 发送(假设已配置好)
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buffer, (uint32_t)&USART1->TDR, 64);
// 实际 DMA 从主存读取,可能得到旧数据或部分旧数据
}
现象:通过串口接收到的数据与预期不符,部分字节为 0 或随机值。这是因为 CPU 写入的数据还停留在 Cache 中,未同步到主存。
修正:在启动 DMA 前调用 SCB_CleanDCache_by_Addr。
void send_data_correct(void) {
for (int i = 0; i < 64; i++) {
tx_buffer[i] = i;
}
// 清理 Cache,将数据写回主存
SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, 64);
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buffer, (uint32_t)&USART1->TDR, 64);
}
类似地,DMA 接收后必须 Invalidate,否则 CPU 可能读到 Cache 中的旧数据。
五、完整配置步骤与代码
1. 启用 D-Cache
在 main() 初始化阶段启用:
void enable_dcache(void) {
SCB_EnableICache(); // 启用指令 Cache
SCB_EnableDCache(); // 启用数据 Cache
}
2. 配置 MPU(可选)
若某些区域不适合缓存(如外设寄存器),可通过 MPU 配置为 Device 或 Strongly-ordered 内存。
3. DMA 发送流程
__attribute__((aligned(32))) uint8_t tx_buf[128];
void dma_send(uint8_t *data, uint16_t len) {
memcpy(tx_buf, data, len);
// 确保长度对齐到 32 字节
uint32_t aligned_len = (len + 31) & ~31;
SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, aligned_len);
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buf, (uint32_t)&USART1->TDR, len);
}
4. DMA 接收流程
__attribute__((aligned(32))) uint8_t rx_buf[128];
void dma_receive_start(void) {
// 接收前可先 Invalidate,确保缓冲区无脏数据
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&USART1->RDR, (uint32_t)rx_buf, sizeof(rx_buf));
}
// DMA 接收完成中断中
void DMA_RxComplete_Callback(void) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf));
// 此时 CPU 读取 rx_buf 才是最新数据
}
六、注意事项
- 对齐是前提:任何 Cache 维护操作前,务必确保地址和长度 32 字节对齐。
- 避免频繁维护:Clean/Invalidate 有一定开销,可在批量传输时集中处理。
- 中断安全:在中断中调用 Cache 维护函数时,注意不要与主循环冲突。
-
使用 CMSIS 函数:
SCB_CleanDCache_by_Addr等函数已处理好对齐和屏障,优先使用。 - 调试技巧:若怀疑 Cache 问题,可暂时关闭 D-Cache 验证,但会牺牲性能。
通过理解 D-Cache 与 DMA 的交互机制,并严格遵守对齐与维护时机,你就能在 STM32H7 上既享受 Cache 带来的性能提升,又确保数据传输的可靠性。