一、为什么 Cache 会让 DMA 数据“错乱”?

STM32H7 的 Cortex-M7 内核包含 16KB 的 D-Cache(数据缓存)。当 CPU 访问内存时,数据会被缓存到 D-Cache 中,后续访问直接命中缓存,无需访问慢速的 SRAM。但 DMA 控制器直接访问物理内存,不经过 Cache。这就导致两种典型问题:

  • CPU 写数据,DMA 读:CPU 写入的数据可能还在 Cache 中(Write-Back 模式),未同步到 SRAM,DMA 读到的是旧数据。
  • DMA 写数据,CPU 读:DMA 将新数据写入 SRAM,但 CPU 可能从 Cache 中读到旧数据(Cache 中仍保留着之前的副本)。

以太网和 USB 传输大量使用 DMA,若未正确处理 Cache,会出现发送数据错误、接收丢包、校验失败等现象。

二、Cache 操作原理与 API

CMSIS 提供了两个核心函数(定义在 core_cm7.h):

  • SCB_InvalidateDCache_by_Addr(void *addr, int32_t dsize):失效指定地址范围的 Cache 行。丢弃 Cache 中的内容,下次 CPU 读取时强制从 SRAM 加载。用于 DMA 写入后,CPU 读取前。
  • SCB_CleanDCache_by_Addr(void *addr, int32_t dsize):回写指定地址范围的 Cache 行。将 Cache 中已修改的数据写回 SRAM。用于 CPU 写入后,DMA 读取前。

注意:地址必须 32 字节对齐(Cache 行大小),长度建议向上取整到 32 字节倍数。

三、以太网传输中的实战处理

以 LwIP + ETH 外设为例,发送和接收分别处理。

3.1 发送(CPU 写,DMA 读)

CPU 填充发送缓冲区后,必须 Clean 操作,确保数据写入 SRAM。

// 假设 pbuf 指向待发送数据,len 为长度
void eth_send_packet(uint8_t *buf, uint32_t len) {
    // 1. 确保数据已写入缓冲区
    memcpy(tx_buffer, buf, len);
    // 2. 回写 D-Cache,将数据同步到 SRAM
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buffer, len);
    // 3. 启动 DMA 发送
    ETH->DMATDLAR = (uint32_t)tx_buffer;
    // ... 设置描述符并启动
}

3.2 接收(DMA 写,CPU 读)

DMA 接收完成后,CPU 读取前必须 Invalidate,丢弃 Cache 中的旧数据。

void eth_rx_process(void) {
    // 1. 检查 DMA 接收完成标志
    if (rx_desc->Status & ETH_DMARXDESC_OWN) return;
    // 2. 失效 D-Cache,确保 CPU 读取到 DMA 写入的新数据
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, rx_len);
    // 3. 处理数据
    process_packet(rx_buffer, rx_len);
    // 4. 重新将描述符交给 DMA
    rx_desc->Status = ETH_DMARXDESC_OWN;
}

四、USB 传输中的实战处理

以 USB Device 的 CDC 或 MSC 类为例,同样遵循“写后 Clean,读前 Invalidate”原则。

4.1 USB 发送(CPU 写,DMA 读)

void usb_send_data(uint8_t *data, uint32_t len) {
    memcpy(usb_tx_fifo, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)usb_tx_fifo, len);
    // 触发 USB 端点发送
    USB_EPStartXfer(&hpcd_USB_OTG_FS, &ep_tx);
}

4.2 USB 接收(DMA 写,CPU 读)

void usb_receive_callback(uint8_t *buf, uint32_t len) {
    SCB_InvalidateDCache_by_Addr((uint32_t *)buf, len);
    // 处理接收数据
    handle_usb_data(buf, len);
}

五、完整示例:以太网发送与接收的 Cache 管理

以下代码基于 STM32H7 HAL 库,展示一个简化的以太网数据收发流程。

#include "stm32h7xx_hal.h"

#define ETH_BUF_SIZE 1524
__attribute__((aligned(32))) uint8_t tx_buf[ETH_BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[ETH_BUF_SIZE];

// 发送函数
void eth_transmit(uint8_t *data, uint32_t len) {
    if (len > ETH_BUF_SIZE) return;
    memcpy(tx_buf, data, len);
    // 回写 D-Cache
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    // 配置 DMA 描述符并启动发送
    ETH->DMATDLAR = (uint32_t)tx_buf;
    // ... 其他寄存器操作
}

// 接收中断处理
void ETH_IRQHandler(void) {
    if (ETH->DMASR & ETH_DMASR_RS) { // 接收成功
        uint32_t len = rx_desc->ControlBufferSize & 0x1FFF;
        // 失效 D-Cache
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);
        // 处理数据
        process_eth_packet(rx_buf, len);
        // 重新挂载接收描述符
        rx_desc->Status = ETH_DMARXDESC_OWN;
        ETH->DMASR = ETH_DMASR_RS; // 清除标志
    }
}

六、注意事项与避坑指南

  • 地址对齐:Cache 操作地址必须 32 字节对齐,长度建议为 32 的倍数。否则可能误伤相邻数据。
  • 避免频繁操作:Cache 失效/回写有一定开销,尽量批量处理,减少调用次数。
  • 使用 MPU 配置:可将 DMA 缓冲区所在内存区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题(但会牺牲性能)。
  • 不要对同一区域同时 Clean 和 Invalidate:顺序错误会导致数据丢失。牢记:CPU 写后 Clean,DMA 写后 Invalidate。
  • 中断优先级:确保 DMA 中断优先级合理,避免在 Cache 操作过程中被更高优先级中断打断而引发竞态。
  • 调试技巧:若数据异常,可临时关闭 D-Cache 验证是否为一致性问题。

掌握 Cache 与 DMA 的协同规则,能让 STM32H7 在高速通信中既跑得快又稳如磐石。