STM32H7 的 D-Cache 与 DMA 数据一致性排查:地址对齐、Clean/Invalidate 时序与常见踩坑
1. 为什么 D-Cache 和 DMA 会打架?
STM32H7 基于 Cortex-M7 内核,主频高达 480MHz,为了弥补 CPU 与内存之间的速度差距,引入了 D-Cache(数据缓存)。CPU 访问数据时,会先查 Cache,命中则直接读写,不会立即同步到物理内存(SRAM/SDRAM)。
而 DMA(直接存储器访问)是独立于 CPU 的硬件单元,它直接读写物理内存,完全绕过 Cache。这就导致了经典的一致性问题:
- CPU 写,DMA 读:CPU 写入的数据可能还在 Cache 中(写回模式),DMA 读到的却是内存中的旧数据。
- DMA 写,CPU 读:DMA 将新数据写入内存,但 CPU 读到的却是 Cache 中的旧数据。
解决思路只有两个:Clean(清理) 和 Invalidate(无效化)。
- Clean:将 Cache 中已修改的数据写回物理内存。
- Invalidate:将 Cache 中的对应行标记为无效,下次读取时强制从内存加载。
2. 地址对齐:Cache Line 的隐形陷阱
Cortex-M7 的 D-Cache 采用 32 字节 Cache Line。所有 Clean/Invalidate 操作都是以 Cache Line 为单位进行的。如果你的 DMA 缓冲区地址或长度没有按 32 字节对齐,就会误伤相邻数据。
错误示例:
uint8_t dma_buf[100]; // 起始地址可能不是 32 字节对齐
假设 dma_buf 起始地址为 0x20000004,长度为 100 字节。当你对 dma_buf 执行 Invalidate 时,实际会影响到 0x20000000~0x2000007F 共 128 字节的范围,其中 0x20000000~0x20000003 这 4 个字节可能存放着其他重要变量,被意外无效化,导致数据丢失。
正确做法:
- 使用
__attribute__((aligned(32)))强制 32 字节对齐。 - 缓冲区大小建议为 32 的整数倍。
__attribute__((aligned(32))) uint8_t dma_buf[128];
3. Clean/Invalidate 的时序:方向决定顺序
这是最容易出错的地方。根据数据传输方向,操作顺序截然不同:
3.1 CPU 写 → DMA 读(发送数据)
- CPU 填充缓冲区。
- Clean 缓冲区(确保数据写回内存)。
- 启动 DMA 发送。
memcpy(dma_buf, src, len);
SCB_CleanDCache_by_Addr((uint32_t *)dma_buf, len);
HAL_UART_Transmit_DMA(&huart1, dma_buf, len);
3.2 DMA 写 → CPU 读(接收数据)
- 启动 DMA 接收。
- 等待 DMA 完成(或半传输中断)。
- Invalidate 缓冲区(丢弃 Cache 旧值)。
- CPU 读取数据。
HAL_UART_Receive_DMA(&huart1, dma_buf, len);
// 等待完成...
SCB_InvalidateDCache_by_Addr((uint32_t *)dma_buf, len);
process_data(dma_buf, len);
关键点:Invalidate 必须在 DMA 完成之后、CPU 读取之前执行。如果在 DMA 传输过程中 Invalidate,可能把 DMA 还未写入的数据行无效化,导致数据错乱。
4. 完整代码示例:UART DMA 回环测试
以下代码基于 STM32H743,使用 UART DMA 接收不定长数据,并正确处理 Cache。
#include "stm32h7xx_hal.h"
#define BUF_SIZE 128
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
volatile uint8_t rx_done = 0;
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
void UART_DMA_Init(void) {
// 省略 GPIO、UART 初始化,重点配置 DMA
hdma_usart1_rx.Instance = DMA1_Stream0;
hdma_usart1_rx.Init.Request = DMA_REQUEST_USART1_RX;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_NORMAL;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}
void Start_Receive(void) {
// 启动前先 Invalidate,确保 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) {
if (huart->Instance == USART1) {
// 传输完成后 Invalidate,丢弃 Cache 旧值
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
rx_done = 1;
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
// 使能 D-Cache(通常已在 SystemInit 中完成)
SCB_EnableDCache();
UART_DMA_Init();
Start_Receive();
while (1) {
if (rx_done) {
rx_done = 0;
// 此时 rx_buf 中的数据是有效的
// 处理数据...
Start_Receive(); // 重新启动接收
}
}
}
5. 常见踩坑与排查方法
- 坑1:忘记 Clean 导致 DMA 发送旧数据。现象:串口发送的数据总是上一次的。排查:在启动 DMA 前加 Clean。
- 坑2:Invalidate 时机错误。现象:接收数据随机错乱。排查:确保 Invalidate 在 DMA 完成中断中执行,而非之前。
-
坑3:缓冲区未对齐。现象:相邻变量被意外修改。排查:用
__attribute__((aligned(32)))并检查链接脚本。 - 坑4:多缓冲区或链表操作。现象:DMA 描述符被 Cache 缓存。排查:对描述符所在内存也进行 Clean/Invalidate,或将其放在非缓存区域(MPU 配置)。
-
坑5:中断中调用 Cache 维护函数。注意:
SCB_CleanDCache_by_Addr等函数执行时间较长,在高速中断中可能影响实时性,建议评估或使用 MPU 将 DMA 缓冲区配置为 Write-Through 模式。
调试技巧:
- 使用
SCB_InvalidateDCache()全局无效化(简单粗暴,但影响性能)。 - 通过 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable,一劳永逸,但会降低 CPU 访问速度。
- 利用 ITM/SWO 打印 Cache 状态,或使用调试器查看内存与 Cache 内容差异。
6. 总结
STM32H7 的 D-Cache 与 DMA 共存需要开发者手动维护一致性。核心原则:CPU 写后 Clean,DMA 写后 Invalidate,地址对齐 32 字节,时序严格遵循方向。掌握这些要点,就能避开大多数数据错乱的坑,充分发挥 H7 的高性能。