跳转到内容
新建笔记

STM32H743 USB DMA:内存可达性、缓存与枚举排查

STM32H743 的 USB 内部 DMA 能否正常工作,至少取决于控制器与硅片版本、DMA 对内存的可达性、缓冲区对齐、缓存一致性和 USB 中间件配置。启用 DMA 后无法枚举,是排查入口,不能仅凭这个现象判定一定是 D-Cache。

本文复核原笔记中的解释和代码,并按 ST 文档整理排查方法。没有提供原工程、链接映射、抓包和目标板测试结果,因此下面是核验指南,不是对某块板卡已经完成的故障归因。

1. 分开理解控制器、PHY、速度与 DMA

跳转到“1. 分开理解控制器、PHY、速度与 DMA”

STM32H743 参考手册 RM0433 将两个 USB 实例描述为 OTG_HS1 和 OTG_HS2;HAL/Cube 工程中的实例名字可能使用 USB_OTG_HS、USB_OTG_FS。其中 HS1 可通过外部 ULPI PHY 实现高速,HS2 的引脚接口只能实现全速。运行速度为 FS 不等于没有内部 DMA。

同时,普通 DMA 传输与 scatter/gather 功能不是同一个能力。RM0433 的实现表还按实例和硅片修订区分 scatter/gather,不能把某个修订版的功能套到全部 H7 型号。设计前应核对具体料号、修订号、RM0433 及勘误。参见 ST RM0433。

2. 第一个检查:DMA 能否访问这块内存

跳转到“2. 第一个检查:DMA 能否访问这块内存”

CPU 能读取某个地址,不代表 USB 内部 DMA 能读取它。ST 的 USB DMA 指南指出,H743 所属架构中的 USB DMA 无法访问 DTCM;如果缓冲区被链接到 0x20000000 附近的 DTCM,只关闭缓存也不会增加 DMA 到该内存的通路。

先在 .map 文件和调试器中检查所有实际传给 USB DMA 的地址,包括数据缓冲、EP0 控制传输相关缓冲以及中间件临时对象。不要只移动 UserRxBufferFS、UserTxBufferFS,就假定整条枚举路径都已迁移。见 ST USB DMA 配置指南。

原文的 MPU 代码从 0x24000000 起设置 512 KB 非缓存区,只会改变该地址范围的 CPU 访问属性,不会把位于 DTCM 的变量搬过去。对象放在哪里由链接脚本、段属性、分配器或栈位置决定;MPU 和链接布局必须一起检查。

3. 第二个检查:CPU 与 DMA 是否看到同一份数据

跳转到“3. 第二个检查:CPU 与 DMA 是否看到同一份数据”

H743 的 L1 D-Cache 是 16 KB,I-Cache 也是 16 KB;原文把两者相加后的 32 KB 当成了 D-Cache 大小。参见 STM32H743 产品规格。

缓存行大小是另一个概念:Cortex-M7 缓存行是 32 字节。原代码 aligned(32) 表示 32 字节对齐,不能称为“32 位对齐”;32 位仅为 4 字节。见 ST AN4839 缓存说明。

两种典型的不一致如下:

发送:CPU 写入缓存 → SRAM 仍是旧值 → USB DMA 读出旧数据
接收:USB DMA 写 SRAM → CPU 缓存仍是旧值 → CPU 读出旧数据

clean 把脏缓存行写回内存;invalidate 使缓存行失效,下次读取时重新取内存。接收完成后如果错误地 clean 一份过期的脏缓存,反而可能覆盖 DMA 刚写入的数据。

4. 三种处理路线及适用边界

跳转到“4. 三种处理路线及适用边界”
路线要解决的问题仍然需要检查
关闭 USB 内部 DMA用 CPU/FIFO 路径作为调试基线或最终实现吞吐、CPU 占用、最坏延迟是否满足实际需求
为 DMA 保留非缓存区避免该区 CPU 数据缓存一致性维护内存可达、链接布局、MPU 覆盖范围、所有 DMA 对象
缓存区内显式维护保留其他数据的缓存收益缓存行独占、清理时序、所有权交接、长度与对齐

原文直接断言“CDC 无 DMA 性能完全够用”,没有采样率、数据量或测试依据。CDC 是设备类,不是吞吐量需求;应测量目标负载后选择。

在可被 USB DMA 访问的 SRAM 中划出一块专用区,通过链接段放入 DMA 对象,再用 MPU 将同一区域设为合适的非缓存普通内存。MPU 区域大小和基地址必须满足硬件对齐要求,且不能与其他区域、堆、栈重叠。

原草案将整个 AXI SRAM 512 KB 配为非缓存,可用于有明确布局的对照实验,但会影响该区全部对象的性能,不宜未经检查直接作为通用推荐配置。设置 DisableExec 时也应依据用途;单纯数据缓冲区不需要允许执行。

MPU 应在规划好的启动阶段配置;若修改已有缓存区属性,需要先按平台要求停止访问并处理缓存。只有调用 HAL_MPU_ConfigRegion() 的片段,没有启用顺序、链接段和区域冲突说明,不是完整方案。

路线 B:缓存维护与所有权

跳转到“路线 B:缓存维护与所有权”

把发送和接收看作 CPU 与 DMA 之间的所有权转移:

发送:CPU 填数据 → clean → 屏障 → DMA 读取 → 完成 → CPU 才能改写
接收:CPU 准备独占缓冲 → 清理/失效 → 屏障 → DMA 写入
→ 完成 → invalidate → CPU 读取 → 再次交给 DMA

缓存维护必须覆盖完整缓存行,但不能误伤邻近对象。因此接收缓冲区的起点和分配大小都按 32 字节规划,并让其独占所涉及的缓存行;不能只把任意地址向下取整,再对包含无关脏数据的区域执行 invalidate。

下面只是缓存维护层的示意,需在实际 USB 中间件的交接位置接入。.usb_dma 必须由链接脚本放到 USB DMA 可达且允许缓存的 SRAM;如果放在非缓存区,则不应再把这组操作当成必要条件。

#include <stdint.h>
#include "stm32h7xx.h"
enum { USB_BUFFER_BYTES = 128 }; /* 分配大小为 32 的整数倍 */
__attribute__((section(".usb_dma"), aligned(32)))
static uint8_t usb_tx[USB_BUFFER_BYTES];
__attribute__((section(".usb_dma"), aligned(32)))
static uint8_t usb_rx[USB_BUFFER_BYTES];
/* 调用前由 CPU 独占;之后交给 DMA,直到完成前不能修改。 */
static void prepare_tx(void) {
SCB_CleanDCache_by_Addr((uint32_t *)usb_tx, sizeof usb_tx);
__DSB();
}
/* 调用前确保前一次接收已完成,CPU 不再使用该缓冲。 */
static void prepare_rx(void) {
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)usb_rx, sizeof usb_rx);
__DSB();
}
/* 仅在 DMA 已完成且停止写入后调用,随后 CPU 才能解析有效字节。 */
static void finish_rx(void) {
SCB_InvalidateDCache_by_Addr((uint32_t *)usb_rx, sizeof usb_rx);
__DSB();
}

此片段没有启动 USB 传输,也没有证明这些两个数组覆盖了全部枚举缓冲。CMSIS 函数签名、USB 接收长度和端点最大包长应按工程版本检查;有的接收路径会按包长准备接收空间,实际分配不能只按应用想要的有效字节数估算。

5. 按证据定位枚举失败

跳转到“5. 按证据定位枚举失败”
  1. 记录完整料号、芯片修订、Cube/HAL/CMSIS/USB 中间件版本及链接脚本。
  2. 建立不启用 DMA 的对照,记录主机枚举错误、USB 中断和返回状态。
  3. 启用 DMA 后先核对缓冲实际地址、硬件所需对齐和内存可达性。
  4. 再比较非缓存区或正确缓存维护的行为,避免同时修改多个无关设置。
  5. 检查 EP0 的 SETUP、描述符及短包路径,不只验证后续 CDC 数据传输。
  6. 验证持续传输、取消、复位、拔插、边界长度与高负载,测量吞吐和 CPU 负载。

原文的“关闭 DMA 正常/开启 DMA 必失败/非缓存必正常”表已改为实验变量。是否正常必须来自对应工程的测试记录,不能从开关组合直接推导。