STM32H7 的 D-Cache 与 DMA 数据一致性:SCB_CleanDCache_by_Addr 的正确用法与踩坑记录
1. 为什么 D-Cache 会成为 DMA 的“猪队友”?
STM32H7 搭载 Cortex-M7 内核,主频高达 480MHz,为了匹配 CPU 的高速运算,芯片内部集成了 D-Cache(数据缓存)。CPU 访问数据时,会优先从 D-Cache 中读取,写数据时也先写入 D-Cache,再由硬件在适当的时候同步到主存(SRAM)。
而 DMA 控制器是“老实人”,它直接访问主存,完全不知道 D-Cache 的存在。这就导致了经典的一致性问题:
- CPU 写,DMA 读:CPU 将数据写入 D-Cache,但尚未回写到 SRAM,此时启动 DMA 发送,DMA 从 SRAM 读到的是旧数据。
- DMA 写,CPU 读:DMA 将新数据写入 SRAM,但 CPU 读取时命中 D-Cache 中的旧数据,导致读到“脏”数据。
解决这一问题的核心就是软件维护 Cache 一致性,即使用 SCB_CleanDCache_by_Addr、SCB_InvalidateDCache_by_Addr 等函数。
2. 关键函数:Clean、Invalidate 与 Clean&Invalidate
CMSIS 提供了三个核心函数:
-
SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中指定地址范围的数据写回到 SRAM。用于 CPU 写 → DMA 读 的场景。 -
SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):将 Cache 中指定地址范围的数据无效化。CPU 下次读取时会从 SRAM 重新加载。用于 DMA 写 → CPU 读 的场景。 -
SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize):先 Clean 再 Invalidate,用于双向传输或不确定的场景。
注意:这些函数的参数 addr 必须是 32 字节对齐的地址,dsize 必须是 32 字节的整数倍。否则函数内部会触发断言或行为异常。
3. 正确使用步骤与代码示例
3.1 发送数据(CPU → DMA)
#include "core_cm7.h"
#define BUFFER_SIZE 1024
#define CACHE_LINE 32
// 确保缓冲区按 32 字节对齐
__attribute__((aligned(CACHE_LINE))) uint8_t tx_buffer[BUFFER_SIZE];
void send_data_dma(void)
{
// 1. 填充数据到 tx_buffer
for (int i = 0; i < BUFFER_SIZE; i++) {
tx_buffer[i] = i & 0xFF;
}
// 2. 清理 D-Cache,将数据写回 SRAM
// 注意:地址和长度必须按 32 字节对齐
SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, BUFFER_SIZE);
// 3. 启动 DMA 传输
HAL_DMA_Start(&hdma_usart1_tx, (uint32_t)tx_buffer, (uint32_t)&USART1->TDR, BUFFER_SIZE);
}
3.2 接收数据(DMA → CPU)
__attribute__((aligned(CACHE_LINE))) uint8_t rx_buffer[BUFFER_SIZE];
void receive_data_dma(void)
{
// 1. 启动 DMA 接收
HAL_DMA_Start(&hdma_usart1_rx, (uint32_t)&USART1->RDR, (uint32_t)rx_buffer, BUFFER_SIZE);
// 2. 等待 DMA 传输完成(实际项目中建议用中断或轮询标志)
while (HAL_DMA_PollForTransfer(&hdma_usart1_rx, HAL_DMA_FULL_TRANSFER, 1000) != HAL_OK);
// 3. 无效化 D-Cache,确保 CPU 读取时从 SRAM 获取新数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, BUFFER_SIZE);
// 4. 处理数据
process_data(rx_buffer, BUFFER_SIZE);
}
3.3 双向传输(如 SPI 全双工)
__attribute__((aligned(CACHE_LINE))) uint8_t spi_tx[BUFFER_SIZE];
__attribute__((aligned(CACHE_LINE))) uint8_t spi_rx[BUFFER_SIZE];
void spi_transfer_dma(void)
{
// 准备发送数据
fill_tx_buffer(spi_tx, BUFFER_SIZE);
// 清理发送缓冲区
SCB_CleanDCache_by_Addr((uint32_t *)spi_tx, BUFFER_SIZE);
// 启动 SPI DMA 收发
HAL_SPI_TransmitReceive_DMA(&hspi1, spi_tx, spi_rx, BUFFER_SIZE);
// 等待完成...
// 无效化接收缓冲区
SCB_InvalidateDCache_by_Addr((uint32_t *)spi_rx, BUFFER_SIZE);
// 处理接收数据
process_data(spi_rx, BUFFER_SIZE);
}
4. 踩坑记录与注意事项
4.1 地址与长度未对齐
坑:直接传入任意地址和长度,例如 SCB_CleanDCache_by_Addr((uint32_t*)&buf[1], 100)。
现象:函数内部断言失败,或清理不完整,导致数据错乱。
解决:始终使用 __attribute__((aligned(32))) 定义缓冲区,并确保操作长度是 32 的倍数。若无法对齐,可手动扩展范围到 32 字节边界。
4.2 在 DMA 传输过程中操作 Cache
坑:在 DMA 还在搬运数据时,就调用 Invalidate 或 Clean。
现象:Cache 与 DMA 竞争访问,数据损坏。
解决:必须在 DMA 传输完全停止后,再进行 Cache 维护操作。对于循环模式 DMA,需特别小心,通常建议使用双缓冲或暂停 DMA。
4.3 忘记配置 MPU 导致 Cache 策略错误
坑:默认情况下,STM32H7 的 SRAM 区域可能被配置为 Write-Back Cache,但某些区域(如 DMA 缓冲区)更适合 Write-Through 或 Non-Cacheable。
解决:使用 MPU 将 DMA 缓冲区所在区域配置为 Non-Cacheable 或 Write-Through,可从根本上避免一致性问题。例如:
MPU_Region_InitTypeDef MPU_InitStruct = {0};
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // 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);
4.4 使用 SCB_InvalidateDCache 导致未提交数据丢失
坑:在 CPU 刚写完数据但未 Clean 的情况下,直接 Invalidate,会丢弃 Cache 中的新数据。
解决:若不确定 Cache 状态,使用 SCB_CleanInvalidateDCache_by_Addr,但要注意性能开销。
4.5 中断与 Cache 维护的原子性
坑:在 Cache 维护过程中被中断打断,中断服务程序又访问了同一缓冲区。
解决:在维护 Cache 前后关闭中断,或使用互斥锁保护。
5. 总结
STM32H7 的 D-Cache 是一把双刃剑,用好了性能飙升,用错了数据错乱。核心原则:
- CPU 写、DMA 读 → 先 Clean。
- DMA 写、CPU 读 → 先 Invalidate。
- 地址长度 32 字节对齐。
- DMA 停止后再维护 Cache。
- 善用 MPU 将 DMA 缓冲区设为 Non-Cacheable,一劳永逸。
掌握这些要点,你就能在 STM32H7 上驾驭 DMA 与 D-Cache,构建稳定高效的系统。