前阵子折腾一块 STM32H573VITxQ 的板子,USB 插上电脑以后设备管理器纹丝不动,连个"未知设备"都不给。按 F4 时代的老经验查供电、查上拉、查描述符,折腾了大半天,最后发现根因居然在 H5 系列新增的安全机制上。这篇文章想把 H573VITxQ 这类带 TrustZone 的中高端 MCU 上 USB 不工作的排查路径完整梳理一遍,从硬件供电、时钟树到安全属性、中间件配置,把每个环节的坑都摊开讲。如果你也在调 H5 系列 USB,或者正准备用这颗芯片做 CDC 虚拟串口、HID、MSC 之类的设备,这篇文章应该能帮你省下一整天的排查时间。
1. 现象分层:先确定 USB 死在哪个环节
1.1 三种典型表现,对应三种完全不同的排查方向
USB 连上电脑后不工作,其实有好几种完全不同的"死法",每一种对应的排查范围差别很大。我把最常见的现象分成三类,如果你正在调板子,先对号入座:
| 现象 | 你的直接感受 | 根因大概率在哪 |
|---|---|---|
| 完全无反应 | 设备管理器没有任何新设备,连声音都没有 | 硬件供电、D+ 上拉、VBUS 检测、时钟使能 |
| 未知设备 | 弹出"无法识别的 USB 设备",设备管理器出现 Unknown Device | 枚举协议流程,描述符回复异常,EP0 问题 |
| 反复断开重连 | 设备能识别,但过一会就掉,或者数据传输时死掉 | FIFO 配置、时钟精度、电气完整性、驱动问题 |
这个分类不是随便分的,它对应的底层机制完全不同。
第一类"完全无反应",本质是主机根本没有检测到有设备插入。USB 设备检测靠的是 D+ / D- 线上的上拉:全速设备会在 D+ 线上通过 1.5kΩ 电阻上拉到 3.3V,主机看到 D+ 电平拉高,才知道有设备接入了。如果你的固件压根没有启用这个上拉,或者 USB 外设的供电根本没通,主机就完全感知不到你。
第二类"未知设备",说明主机已经检测到了 D+ 上拉,开始走枚举流程了,但设备在响应主机请求时出了问题。设备无法正确回复设备描述符,或者回复的数据格式不对,主机只能给你一个 Unknown Device 的结果。
第三类"反复断开",通常是高速信号质量、FIFO 瓶颈或者时钟漂移。枚举能过说明基本链路是通的,问题出在量或者时序上。
1.2 为什么 H573 的排查路径不能照搬 F1/F4
很多老手调 USB 时习惯打开 F4 的例程直接抄,但 STM32H573VITxQ 这颗芯片和 F1/F4 有一个本质区别:它用的是 Cortex-M33 内核,且整个系统引入了 TrustZone 安全架构。
M33 内核不是简单地在 M4 基础上加了点指令,而是多了一整个安全状态机。芯片上电后,外设到底属于"安全世界"还是"非安全世界",决定了你的普通应用代码能不能访问它的寄存器。很多 USB 不工作的问题,根源不在 USB 协议本身,而是你的代码压根没碰到 USB 外设——访问被安全机制挡掉了。
另外,H573 的 USB 控制器是带高速能力的 OTG_HS 控制器,它既可以通过内部全速 PHY 跑 Full Speed,也可以外接 ULPI PHY 跑 High Speed。CubeMX 里配置错一个选项,USB 就完全不工作,而且不会报任何错误。这些 F4 上没有的"新坑",后面我会逐一展开。
2. H573 特有的第一道坎:TrustZone 外设安全归属
2.1 你以为写寄存器了,其实总线层就给你拦了
这块可能出乎很多人意料。H573 出厂或者经过某些烧录流程之后,option bytes 里的 TZEN 位可能是置位的。TZEN=1 意味着 TrustZone 被启用,芯片复位后,大部分外设默认归属为安全(Secure)资源。
这时候如果你跑的是一个普通的非安全(Non-secure)应用——不用任何安全启动、不用 TrustZone 的基础裸机程序——你去访问 USB 外设的寄存器,总线层的安全过滤器会直接拦截,寄存器写进去的数据等于石沉大海。外设时钟开了,GPIO 配了,但 USB 核心压根没被真正初始化。
我实际遇到的情况是:前一任调试者给板子烧过带安全启动的固件,TZEN 被置1了,我后来直接烧普通固件进去,USB 完全没反应。查了供电、查了引脚、查了时钟,最后读 option bytes 才发现是 TZEN 的问题。
2.2 怎么判断 TZEN 是不是罪魁祸首
判断方法很简单,用 STM32CubeProgrammer 连上 ST-LINK,连接到目标芯片后,切到Option Bytes页面,找到TZEN位看看当前状态。
如果 TZEN=1,而你根本不需要 TrustZone 安全特性,那就把它关掉,然后做一次全片擦除,再重新烧录你的普通固件。具体操作步骤我给你列一下:
- 打开 STM32CubeProgrammer,通过 ST-LINK 连接目标板;
- 点击Option Bytes标签,找到 TZEN 字段;
- 将 TZEN 清0,点击Apply;
- 执行Full chip erase(全片擦除),这一步会清掉 flash 里之前所有内容;
- 重新烧录你的应用固件,跑起来再看 USB。
提示:调整 option bytes 前一定先备份原有固件和配置,尤其是从别人手里转过来的板子。有些板子 RDP(读保护)级别设成了 1 甚至 2,改 TZEN 前需要先把 RDP 降级,过程会触发全片擦除。RDP2 级别的芯片基本没法通过标准工具降级,这种情况比较麻烦,最好先确认板子的来源状态。
2.3 如果你确实要用 TrustZone:USB 外设的安全属性要单独配
有些项目需要用 TrustZone 做安全隔离,不愿意关掉。那你就得通过 GTZC(Global TrustZone Controller)把 USB 外设的安全属性配置成非安全(Non-secure),否则非安全端的应用代码永远访问不了 USB。
这个配置涉及几个层面的设置,不只是外设本身:
- GTZC1 的 SECCFGR 寄存器:把 USB_OTG_HS 对应的位清零,表示该外设属于非安全世界;
- 中断控制器:USB 中断也要配置到非安全中断组,否则 CPU 在非安全状态收不到中断;
- Flash/SRAM 的属性:USB DMA 要访问的缓冲区内存也需要允许非安全访问。
在 CubeMX 里如果开了 TrustZone 相关选项,会生成对应的安全配置代码,你需要找到GTZC_Config相关的初始化函数,确认 USB 外设被加到了非安全列表里。这块就不是简单关掉 TZEN 那么省事了,建议非必要不碰。
3. 48MHz 时钟树:USB 没有它什么都白搭
3.1 H573 的 USB 时钟不是"自动就有"的
STM32 上 USB 全速设备需要精确的 48MHz 时钟,这是 USB 协议规定的。但 H573 在时钟树上和 F4 不太一样,它没有独立的 HSI48 RC 振荡器可用。F4 上你可以直接选 HSI48 当 USB 时钟,H5 上则必须通过 PLL 锁相环倍频出来。
也就是说,USB 的 48MHz 必须来自 PLL1、PLL2 或者 PLL3 的某个输出分频。CubeMX 的时钟配置页里,你会看到一个USB Clock Mux或者类似的选项,里面默认可能会选某个 PLL 的输出。
很多新手在这个环节翻车,是因为看了别人工程直接照抄,但板载晶振频率不同,PLL 分频系数完全不匹配,导致 48MHz 压根没有生成。最直接的检查方式是打开 CubeMX 的Clock Configuration页面,找到 USB 外设那一行,看它的时钟频率是不是 48MHz。如果是红色或者显示 0,说明 PLL 配置有问题。
3.2 一个标准的 HSE 25MHz 晶振配置案例
绝大多数开发板会用 25MHz 外部晶振(HSE)。下面这套 PLL1 参数是我在 H573 上常用的,能同时跑出 240MHz 的系统主频和 48MHz 的 USB 时钟:
- 时钟源:HSE 25MHz
- PLL1 M = 5,得到 5MHz 的 PLL 参考时钟
- PLL1 N = 96,VCO 输出 480MHz
- PLL1 P = 2,系统时钟 240MHz
- PLL1 Q = 10,USB 时钟 480 / 10 = 48MHz
在 CubeMX 的 Clock Configuration 页面里,把 PLL1 的源选成 HSE,M 填 5,N 填 96,在 USB 时钟源下拉框里选 PLL1_Q,页面会实时显示 USB 那一路变成了 48MHz,而且字体是蓝色,说明时钟树满足要求。
3.3 没有 HSE 的板子:HSI 方案能做,但精度有风险
如果你的板子省掉了外部晶振,只能靠内部 HSI16 做 PLL 源,数学上也能凑出 48MHz:HSI16(16MHz)→ M=2 → 8MHz → N=48 → 384MHz → Q=8 → 48MHz。
但这里有个隐患:USB 全速设备对时钟精度要求是 48MHz ±0.25%,而内部 RC 振荡器的出厂精度一般在 ±1% 量级,温度和电压变了还会漂。内部 RC 做串口、I2C 这类低速外设没问题,但做 USB 枚举可能时好时坏,有时能识别,有时完全没反应。
所以我的建议很直接:如果你准备做 USB 设备,板子上最好放一颗外部晶振,或者至少确认 HSI16 在目标工作温度范围内满足精度。省一颗晶振的钱,换来的可能是无休止的 USB 掉线排查,不值。
4. 硬件与引脚:VDDUSB、PA11/PA12、VBUS Sense 三件套
4.1 VDDUSB 没接通,USB 收发器直接罢工
这是最容易被忽略的硬件问题,尤其是从 F103 转过来的人。STM32F1 的 USB 收发器供电和 MCU 主电源共用,但 H573 的 USB 收发器有一个独立供电引脚 VDDUSB。
VDDUSB 需要接 3.0~3.6V 的外部电源,并且要加 100nF 去耦电容。如果这个引脚悬空或者供电电压不对,USB 物理层的模拟电路就不会工作。表现就是:固件看起来一切正常,D+ 上拉也开启了,但示波器量 D+ 电平就是拉不起来,或者只有很微弱的电压。
我之前见过一块板子,为了做低功耗设计,把 VDDUSB 单独接到一个 load switch 控制,结果 load switch 默认关断,USB 插上电脑完全没反应。所以调试 USB 之前,先拿万用表量一下 VDDUSB 引脚对地电压是不是 3.3V。
4.2 CubeMX 里配成了 High Speed,但板上没有外部 PHY
H573 的 USB 控制器名称是USB_OTG_HS,和很多 F4 上的 USB_OTG_FS 不一样。这个控制器本身有高速能力,但要在高速模式下工作,芯片上必须外接一颗 ULPI PHY 芯片,比如常见的 USB3300、USB3320,通过 ULPI 并行接口连接。
问题来了:如果你在 CubeMX 里把 USB_OTG_HS 的 Mode 选成了High Speed,而你的板子上根本没有外部 ULPI PHY,USB 当然不会工作。HAL 库会去试图初始化 ULPI 接口的 IO 和时序,但你的硬件上压根没这个东西。
H573VITxQ 的 LQFP100 封装上,如果你用内部全速 PHY,就应该用PA11(USB_DM)和 PA12(USB_DP)这两个引脚直连 USB 座子。CubeMX 配置里,USB_OTG_HS 的 Speed 选项要选Full Speed(Internal Phy),而不是 High Speed。
这个坑隐蔽就隐蔽在:很多教程和例程基于 F429 这类有独立 OTG_FS 的芯片,F429 上选 OTG_FS 直接就是全速。H573 没有独立的 OTG_FS,只有一个 OTG_HS,你必须主动告诉它"我要在全速模式下用内部 PHY",否则它默认按高速外接 PHY 去初始化。
4.3 VBUS sensing 开着,但 PA9 没接 VBUS 信号
这是另一个高频翻车点,尤其在自供电(Self-powered)设备上。STM32 的 OTG 控制器在设备模式下可以配置 VBUS 检测功能,通过专门的 VBUS 引脚(一般是 PA9)检测 USB 总线上是否有 5V 电源,以此判断是否连接了主机。如果启用了 VBUS sensing,HAL_PCD_Start() 之后,控制器会等待 VBUS 有效才把 D+ 上拉。
很多自供电设备的电路设计,USB 座子只接了 D+、D-、GND,VBUS 引脚根本没连到 MCU。这时候如果你忘了关 VBUS sensing,USB 控制器永远等不到"主机接入"这个事件,后续所有枚举流程都不会启动。表现就是:插上电脑,设备管理器完全没反应,D+ 也量不到上拉电压。
解决办法有两种:
- 硬件上:把 USB 座子的 VBUS 通过电阻分压后接到 PA9,让 MCU 能检测到 5V;
- 软件上:如果你的设计不需要 VBUS 检测,就把 CubeMX 里 USB_DEVICE 中间件配置中的 VBUS sensing 选项禁用掉。生成代码后,对应的 HAL 配置里
hpcd.Init.VbusSensingEnable会是 0。
从简化设计和降低故障率的角度,自供电设备我一般直接关掉 VBUS sensing,让 USB 设备不管插没插主机,都认为"已连接",等着主机来枚举。
4.4 别忘了 D+ / D- 的物理连接细节
这块是排查完全无反应时顺手要查的。PA11 是 DM,PA12 是 DP,这两个脚如果接反了,主机永远无法识别。有些 PCB 布局人员会把 DM/DP 网络标号搞混,我在实际项目里真遇到过。
另外,FS 模式下有些设计会在 D+ / D- 上各串一个 22Ω 的电阻,用来抑制振铃和过冲,一般放在 MCU 引脚和 USB 座子之间。走线尽量等长、并行走、避免过孔,这个在低速全速设备上要求不算苛刻,但别太离谱就行。
5. 中间件与 HAL 配置:USB 不工作的高频故障点
5.1 中间件选错了:裸机 USB_Device 还是 USBX
如果你在 CubeMX 里启用了 USB 外设,还会面临中间件的选择。H573 可以搭配 ST 官方的 USB Device Library(裸机),也可以搭配 Azure RTOS ThreadX 生态里的 USBX。
如果你没有用 RTOS,就用裸机的USB_Device中间件。如果你用了 ThreadX 并且想用 USBX 的协议栈,就选 USBX。两者代码结构和初始化方式完全不同,选错的话项目根本编译不过,或者即使编译过了,初始化流程也很拧巴。
大多数项目用裸机 USB_Device 中间件就够了,CDC 虚拟串口、HID、MSC 这些类都是现成的。对于 H573 这种主频 240MHz、Flash 2MB 的芯片,跑 USB 设备库的负载非常小,不值得为 USB 单独引入 ThreadX 生态。
5.2 端点与 FIFO 分配的坑
H573 的 OTG_HS 控制器内部有一个共享的 FIFO RAM,设备模式下要手动把这块 RAM 划分给接收 FIFO 和各个端点的发送 FIFO。
CubeMX 生成的代码里一般会有一段类似下面的逻辑分配 FIFO:
HAL_PCDEx_SetRxFiFo(&hpcd, 0x80); HAL_PCDEx_SetTxFiFo(&hpcd, 0, 0x40); HAL_PCDEx_SetTxFiFo(&hpcd, 1, 0x40);如果你用的是 CubeMX 默认生成的配置,一般不会出问题。但有些同学自己改过描述符或者端点,FIFO 分配没跟着改,就可能出现"枚举能过,但一收发数据就死机"或者"枚举到一半 USB 控制器报错"。
举个实际例子:CDC 虚拟串口通常会用到 3 个端点——EP0(控制传输,64 字节)、一个批量 OUT 端点、一个批量 IN 端点。如果你把 EP0 的 TX FIFO 配得太小,而主机发来的控制请求数据超过 FIFO 容量,USB 控制器会忙于处理溢出,导致 GET_DESCRIPTOR 回复超时,主机直接判定为 Unknown Device。
遇到这类问题,最简单的做法是先把 FIFO 分配恢复成 CubeMX 默认值,验证 USB 是否恢复正常,再去折腾优化。
5.3 中断路径:IRQHandler 到底跑没跑
USB 是中断驱动很强的外设,枚举过程的每一个状态变化都是靠中断通知 CPU 的。如果中断处理路径断了,USB 必然不工作。
H573 的 USB 全局中断入口是USB_OTG_HS_IRQHandler,它内部应该调用HAL_PCD_IRQHandler(&hpcd)。在stm32h5xx_it.c里应该能看到这个函数。如果你项目里这部分代码缺失或者没启动 NVIC 中断,USB 外设没有任何办法与 CPU 交互,枚举自然无从谈起。
排查技巧:在HAL_PCD_IRQHandler入口打一个断点,插上 USB,看断点有没有触发。如果一次都没触发,说明中断链路有问题——可能是 NVIC 没使能、中断优先级配置问题、或者 TrustZone 把中断划到了安全组但你运行在非安全状态。如果断点频繁触发,说明 USB 控制器在正常工作,问题可能在协议层。
5.4 一个典型的 CDC 虚拟串口配置示例
如果你正打算用 H573 做一个 USB 转串口设备,下面是 CubeMX 里最基础的配置路径:
- Connectivity → USB_OTG_HS:勾选 Device_Only,Speed 选 Full Speed(Internal Phy);
- Middleware and Software Packs → USB_DEVICE:Class for FS IP 选 Communication Device Class(Virtual Port COM);
- Clock Configuration:确认 USB 那一路显示 48MHz;
- 生成代码后,
main.c里调用MX_USB_DEVICE_Init()。
这样生成出来的固件,插上电脑后 Windows 应该识别为一个 COM 口。但注意,如果你直接用了 CubeMX 生成的原生 CDC 代码,它默认是回显模式——你从电脑发什么,它回什么。实际项目里通常要改CDC_Receive_FS()回调函数,把收到的数据转发到 UART 或者自己的业务逻辑。
CDC 代码有个经典大坑:CDC_Receive_FS()里处理完数据后,必须重新调用一次CDC_Receive_FS()来接收下一包数据。很多新手忘了这个,结果设备只能收一次数据,之后就"死了",然后误以为是 USB 的问题,其实只是接收缓冲区没有重新武装。
6. 实测链路:从 D+ 上拉量到枚举失败
6.1 示波器量 D+ 是最快的"心搏检查"
软件层面查了一圈都没问题的时候,就该拿出示波器了。USB 排查的第一件事,就是把探头点在 PA12(D+)上,然后插 USB。
如果是全速设备,空闲状态下 D+ 应该被拉到 3.0~3.3V 左右的高电平。量到这个高电平,说明两件事:一是 USB 设备硬件基本是活的,二是设备的上拉已经启用,主机应该能检测到设备接入。如果 D+ 一直是 0V,那就是设备端压根没有主动"报告"主机自己存在。
如果 D+ 有高电平,但设备管理器依然没有动静,把 D- 也挂上探头,观察插入瞬间总线上的波形。主机检测到设备后会先拉低 D+ 大概 10ms 做一次复位,后面开始发 SETUP 包。逻辑分析仪如果有 USB 解码功能,可以直接抓 D+/D- 两个通道的波形,把枚举报文解析出来看。
6.2 枚举流程里最容易出错的几个环节
USB 枚举流程,说白了就是主机和设备之间的几次"对话"。完整流程包括:主机复位总线 → 设备默认地址 0 → 主机发 GET_DESCRIPTOR(设备描述符)→ 设备回复 18 字节设备描述符 → 主机分配地址 → 再次 GET_DESCRIPTOR 读取完整配置 → 设置配置 → 设备进入配置状态。
在这个流程里,最容易出问题的是设备描述符回复。只要设备描述符回复的数据格式不对、字节数不对、或者回复超时,主机就直接放弃,表现为 Unknown Device。
设备描述符里有一个特别关键的字段是bMaxPacketSize0,它表示端点 0 的最大包长。FS 模式下一般填 64。如果这个值和代码里实际配置的端点 0 FIFO 大小不一致,主机和设备对包长的理解不一样,枚举也会失败。
6.3 抓包工具怎么选:UsbTreeView、Bus Hound 和 Wireshark
Windows 上排查 USB 枚举问题,我常用的工具是这几个:
- UsbTreeView(USB Device Tree Viewer):能直观看到主机识别到的 USB 设备树,包括设备状态、描述符内容、当前速度,特别适合判断设备是否进入了配置状态;
- Bus Hound:可以抓取总线层面的传输包,能看到主机发出来的 SETUP 包和设备回的数据或者 STALL 握手,定位协议层错误非常精准;
- Wireshark:在 Linux 下配合 usbmon 内核模块可以抓 USB 包,Windows 下配合 USBPcap 也能用,界面熟悉,就是配置稍微麻烦一点。
我的排错习惯是:先用 UsbTreeView 看设备有没有出现在总线上、状态是什么。如果设备树里压根没有,回到硬件和 D+ 上拉;如果有设备但状态异常,再用 Bus Hound 抓包,看枚举过程停在哪一步。
6.4 HAL 错误码到底告诉你什么
H5 的 USB HAL 库在发生异常时会把错误类型记到hpcd.ErrorCode字段里,比如HAL_PCD_ERROR_UNDEFINED_EP、HAL_PCD_ERROR_NAK、HAL_PCD_ERROR_CRC。但这些错误码很多时候不是根因,而是结果——你看到的是 USB 传输层报错,但真正的原因可能是时钟不稳、供电劣化、或者 FIFO 不够。
我建议在HAL_PCD_IRQHandler后面加一个错误打印或者断点