STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 操作顺序与典型踩坑
STM32H7 系列搭载 Cortex-M7 内核,主频高达 480MHz,并配备了 16KB 的 D-Cache 和 I-Cache。D-Cache 的引入大幅提升了 CPU 访问外部存储器的效率,但也给 DMA 传输带来了棘手的数据一致性问题。如果你在使用 DMA 时发现数据“偶尔”出错,很可能就是 D-Cache 在作祟。本文将深入剖析原理,给出正确的 Clean/Invalidate 操作顺序,并总结典型踩坑案例。
一、为什么 D-Cache 会导致 DMA 数据不一致?
Cortex-M7 的 D-Cache 是写回(Write-Back)且写分配(Write-Allocate)的。CPU 访问数据时,会先将数据读入 Cache 行(通常 32 字节)。当 CPU 写数据时,如果 Cache 命中,则只修改 Cache 中的内容,并标记为“脏”(Dirty),并不会立即写回主存。
而 DMA 控制器直接访问主存(SRAM 或外部 SDRAM),它看不到 Cache 中的最新数据。这就导致了两种典型的不一致场景:
- CPU 写,DMA 读:CPU 写入的数据还在 Cache 中(脏行),DMA 从主存读到的却是旧数据。
- DMA 写,CPU 读:DMA 将新数据写入主存,但 CPU 读到的却是 Cache 中缓存的旧数据。
解决这一问题的核心就是 Clean(清理) 和 Invalidate(无效化) 操作:
- Clean:将 Cache 中的脏行写回主存,使主存数据与 Cache 一致。
- Invalidate:将 Cache 行标记为无效,下次 CPU 读取时会从主存重新加载。
二、正确操作顺序:Clean 与 Invalidate 的时机
操作顺序取决于数据传输方向。记住一个原则:谁写数据,谁负责 Clean;谁读数据,谁负责 Invalidate。
2.1 CPU 写 → DMA 读(发送方向)
- CPU 准备好数据缓冲区。
- 执行 Clean 操作,将缓冲区对应的 Cache 行写回主存。
- 启动 DMA 发送。
- 等待 DMA 传输完成。
注意:Clean 操作必须在启动 DMA 之前执行。否则 DMA 可能读到旧数据。
2.2 DMA 写 → CPU 读(接收方向)
- 启动 DMA 接收。
- 等待 DMA 传输完成。
- 执行 Invalidate 操作,使缓冲区对应的 Cache 行无效。
- CPU 读取数据(此时会从主存重新加载最新数据)。
注意:Invalidate 必须在 DMA 完成之后执行。如果在 DMA 传输过程中执行 Invalidate,可能会丢弃 CPU 尚未写回的数据,导致数据损坏。
2.3 双向传输(如 SPI 全双工)
对于同时收发的情况,通常需要:
- 准备发送数据,执行 Clean。
- 启动 DMA 收发。
- 等待完成。
- 对接收缓冲区执行 Invalidate。
三、完整代码示例
以下代码基于 STM32H7 HAL 库,演示了 UART DMA 发送和接收的正确 Cache 维护操作。
#include "stm32h7xx_hal.h"
#define BUFFER_SIZE 64
/* 使用 32 字节对齐的缓冲区,确保 Cache 行对齐 */
aligned(32) uint8_t tx_buffer[BUFFER_SIZE];
aligned(32) uint8_t rx_buffer[BUFFER_SIZE];
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_tx;
DMA_HandleTypeDef hdma_usart1_rx;
/* 清理 D-Cache:将指定地址范围的数据写回主存 */
void clean_dcache(void *addr, uint32_t size)
{
uint32_t start = (uint32_t)addr;
uint32_t end = start + size;
/* 按 32 字节 Cache 行对齐 */
start &= ~0x1F;
SCB_CleanDCache_by_Addr((uint32_t *)start, end - start);
}
/* 无效化 D-Cache:丢弃指定地址范围的缓存内容 */
void invalidate_dcache(void *addr, uint32_t size)
{
uint32_t start = (uint32_t)addr;
uint32_t end = start + size;
start &= ~0x1F;
SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start);
}
/* DMA 发送示例 */
void uart_dma_send(uint8_t *data, uint16_t len)
{
memcpy(tx_buffer, data, len);
/* 1. 清理 Cache,确保主存数据最新 */
clean_dcache(tx_buffer, len);
/* 2. 启动 DMA 发送 */
HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
/* 3. 等待完成(实际项目中可用中断或轮询标志) */
while (huart1.gState != HAL_UART_STATE_READY) { }
}
/* DMA 接收示例 */
void uart_dma_receive(uint8_t *data, uint16_t len)
{
/* 1. 启动 DMA 接收 */
HAL_UART_Receive_DMA(&huart1, rx_buffer, len);
/* 2. 等待完成 */
while (huart1.RxState != HAL_UART_STATE_READY) { }
/* 3. 无效化 Cache,丢弃旧缓存 */
invalidate_dcache(rx_buffer, len);
/* 4. 读取数据 */
memcpy(data, rx_buffer, len);
}
四、典型踩坑与注意事项
-
缓冲区未对齐:Cache 操作以 32 字节行为单位。如果缓冲区起始地址或大小未对齐,Clean/Invalidate 可能影响相邻数据,导致意外错误。务必使用
__attribute__((aligned(32)))或aligned(32)声明缓冲区。 - Invalidate 时机错误:在 DMA 传输过程中执行 Invalidate,可能丢弃 CPU 刚写入但未 Clean 的数据。务必等待 DMA 完成后再操作。
- 忘记 Clean 导致发送旧数据:CPU 写缓冲区后直接启动 DMA,未执行 Clean,DMA 读到的是主存中的旧内容。
- 忘记 Invalidate 导致接收数据错乱:DMA 接收完成后,CPU 直接读缓冲区,读到的是 Cache 中的旧数据。
- 多缓冲区或链表传输:如果使用多个缓冲区,需对每个缓冲区分别执行 Clean/Invalidate。
- 中断与 Cache 操作冲突:在中断中操作 Cache 时,注意临界区保护,避免打断正在进行的 Cache 维护。
- 使用 MPU 配置内存属性:对于频繁 DMA 访问的区域,可配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题,但会牺牲部分性能。
五、总结
STM32H7 的 D-Cache 与 DMA 协同工作并不复杂,关键在于理解数据流向并正确使用 Clean 和 Invalidate。记住:发送前 Clean,接收后 Invalidate。同时注意缓冲区对齐和操作时机,就能避开绝大多数坑。希望本文能帮助你在 STM32H7 上写出稳定高效的 DMA 应用。