STM32H7 的 D-Cache 与 DMA 数据一致性:Clean/Invalidate 的误用场景与实测踩坑记录
STM32H7 系列凭借 Cortex-M7 内核和高达 480MHz 的主频,成为高性能嵌入式应用的首选。然而,启用 D-Cache 后,DMA 传输的数据一致性成为许多开发者的噩梦。本文将深入探讨 D-Cache 与 DMA 的协作机制,分析 Clean/Invalidate 的误用场景,并提供完整的解决方案和实测经验。
1. 原理:为什么 D-Cache 会与 DMA 冲突?
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据会被缓存到 D-Cache 中,而 DMA 控制器直接访问物理内存(SRAM/SDRAM),不经过 Cache。因此:
- CPU 写数据到内存:数据可能只停留在 Cache 中,未写入物理内存。此时启动 DMA 发送,DMA 读到的是旧数据。
- DMA 写数据到内存:DMA 将新数据写入物理内存,但 CPU 可能仍从 Cache 中读取旧数据。
解决方法是使用 Cache 维护操作:
- Clean(清理):将 Cache 中的脏数据写回物理内存。
- Invalidate(无效化):将 Cache 中的对应行标记为无效,强制 CPU 下次读取时从物理内存加载。
2. 误用场景与正确做法
2.1 场景一:CPU 写 → DMA 读(发送)
错误做法:写完数据后直接启动 DMA,未执行 Clean。
正确做法:在启动 DMA 前,对数据缓冲区执行 Clean 操作。
// 假设 buffer 是 CPU 写入的数据,准备通过 DMA 发送
SCB_CleanDCache_by_Addr((uint32_t *)buffer, sizeof(buffer));
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, buffer, sizeof(buffer));
2.2 场景二:DMA 写 → CPU 读(接收)
错误做法:DMA 接收完成后直接读取数据,未执行 Invalidate。
正确做法:在 DMA 接收完成中断中,先 Invalidate 对应缓冲区,再读取数据。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 无效化 Cache,确保 CPU 读取到 DMA 写入的新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer));
// 处理数据
process_data(rx_buffer);
}
2.3 场景三:双向传输(如 SPI 全双工)
错误做法:只做 Clean 或只做 Invalidate。
正确做法:发送前 Clean TX 缓冲区,接收后 Invalidate RX 缓冲区。
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, sizeof(tx_buffer));
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buffer, rx_buffer, size);
// 在完成回调中
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, sizeof(rx_buffer));
3. 配置步骤与完整代码示例
3.1 启用 D-Cache
在 main() 初始化阶段启用 D-Cache:
void Enable_DCache(void)
{
SCB_EnableICache(); // 启用指令 Cache
SCB_EnableDCache(); // 启用数据 Cache
}
3.2 配置 MPU 保护 DMA 缓冲区(可选但推荐)
为避免手动维护 Cache,可将 DMA 缓冲区配置为 Write-Through 或 Non-Cacheable。
void MPU_Config(void)
{
HAL_MPU_Disable();
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 地址
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
MPU_InitStruct.Number = MPU_REGION_NUMBER0;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
3.3 完整 DMA 发送示例(UART)
#define BUFFER_SIZE 128
uint8_t tx_buffer[BUFFER_SIZE] __attribute__((aligned(32))); // 32字节对齐
void Send_Data_DMA(void)
{
// 填充数据
for (int i = 0; i < BUFFER_SIZE; i++) {
tx_buffer[i] = i;
}
// Clean D-Cache
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, BUFFER_SIZE);
// 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE);
}
3.4 完整 DMA 接收示例(UART)
uint8_t rx_buffer[BUFFER_SIZE] __attribute__((aligned(32)));
void Start_Receive_DMA(void)
{
// 启动 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// Invalidate D-Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
// 处理数据
Process_Data(rx_buffer);
// 重新启动接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
}
4. 注意事项与踩坑记录
-
地址对齐:
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr要求地址 32 字节对齐,长度也建议为 32 的倍数。否则可能误操作相邻数据。 - Invalidate 的风险:如果缓冲区中有 CPU 尚未写回的数据,Invalidate 会丢弃这些数据。因此,对于 DMA 接收缓冲区,应确保在 Invalidate 前 CPU 没有写入操作。
- DMA 描述符:如果使用 DMA 双缓冲或链表模式,每个缓冲区都需要单独维护 Cache。
- 性能影响:频繁的 Clean/Invalidate 会降低性能,建议将 DMA 缓冲区放在 Non-Cacheable 区域(通过 MPU 配置)。
-
实测踩坑:在 STM32H743 上,曾遇到 UART DMA 发送数据错乱,原因是
tx_buffer未对齐,Clean 操作影响了相邻变量。对齐后问题解决。 -
调试技巧:使用
SCB_InvalidateDCache()全局无效化可快速验证是否为 Cache 问题,但会严重影响性能,仅用于调试。
5. 总结
D-Cache 与 DMA 的数据一致性是 STM32H7 开发中的关键点。正确使用 Clean 和 Invalidate,或通过 MPU 配置 Non-Cacheable 区域,可以确保数据传输可靠。牢记:CPU 写后 DMA 读要 Clean,DMA 写后 CPU 读要 Invalidate。希望本文的踩坑记录能帮助你少走弯路。