4. 枚举失败定位
枚举失败,说白了就是你的工业相机插到主机上,主机完全没反应——lsusb 看不到,dmesg 没日志,设备管理器里连个问号都没有。我遇到过不少工程师一上来就翻驱动代码,其实很多时候问题出在更底层。
这一章,咱们从最底层的 D+/D- 电平开始,一层一层往上查。我的习惯是:先看硬件,再看协议,最后查软件。顺序搞反了,容易白费功夫。
4.1 第一层:物理层信号检查
USB 枚举的第一步,是主机通过 D+/D- 检测设备插入。你想想看,如果这两根线的电平都不对,后面所有软件分析都是空中楼阁。
4.1.1 全速/低速设备的上拉电阻
USB 设备端必须在上电后,通过 1.5kΩ 上拉电阻将 D+ 或 D- 拉高:
- 全速/高速设备:上拉 D+
- 低速设备:上拉 D-
主机检测到 D+ 或 D- 被拉高,就知道有设备插入了。我曾在项目中遇到过一批相机,D+ 上拉电阻虚焊,导致主机永远检测不到插入事件。用示波器一量,D+ 电平始终是 0V,问题一目了然。
4.1.2 用示波器抓取枚举波形
我个人习惯在枚举失败时,先拿示波器抓 D+/D- 的波形。关键看几个点:
- 插入瞬间:D+ 是否从 0V 跳变到 3.3V(全速)或 0.4V(高速?)
- 复位信号:主机是否拉低 D+/D- 至少 10ms
- 帧起始包(SOF):复位后是否有连续的 1ms 间隔的 SOF 包
- 设备响应:主机发出 GetDescriptor 请求后,设备是否回复 ACK
| 信号阶段 | 正常波形特征 | 异常表现 |
|---|---|---|
| 插入检测 | D+ 从 0V → 3.3V | D+ 始终 0V,或跳变后立刻回落 |
| 总线复位 | D+/D- 同时拉低 ≥10ms | 复位时间不足,或根本没有复位 |
| SOF 包 | 每 1ms 出现一次差分信号 | 无 SOF,或 SOF 间隔异常 |
| 设备响应 | 主机请求后设备回复 ACK | 无响应,或回复 NAK/STALL |
4.2 第二层:USB 协议层握手
物理层信号正常后,接下来是协议层的枚举握手。主机和设备之间会交换一系列标准请求。我建议你打开 usbmon 或 Wireshark 抓 USB 包,看看枚举到了哪一步卡住了。
4.2.1 标准枚举流程
一个完整的 USB 枚举流程是这样的:
- GetDescriptor(Device):主机读取 8 字节设备描述符
- SetAddress:主机分配唯一地址
- GetDescriptor(Device) 完整:主机再次读取完整设备描述符
- GetDescriptor(Configuration):主机读取配置描述符
- SetConfiguration:主机选择配置
如果卡在第一步,说明设备连最基本的描述符都没返回。我曾经调试过一款相机,设备描述符里的 bMaxPacketSize0 填了 64,但实际端点只支持 8 字节包——主机读不完描述符,直接放弃枚举。
4.2.2 常见协议层错误
- 设备无响应:主机发送请求后,设备没有 ACK。检查设备固件是否跑起来了,中断处理是否正常。
- STALL 错误:设备回复 STALL,表示不支持该请求。常见于描述符索引错误。
- CRC 错误:数据包 CRC 校验失败。通常是硬件信号质量问题,比如 D+/D- 走线过长。
- 超时:主机等待设备响应超时。检查设备的中断服务程序是否被高优先级任务阻塞。
4.3 第三层:主机控制器与 Hub 驱动
如果物理层和协议层都正常,但主机仍然无法识别设备,问题可能出在主机侧的软件栈。咱们从 usb_core 到 hub 驱动,一层一层看。
4.3.1 usb_core 的枚举流程
Linux 内核中,usb_core 负责管理 USB 总线和设备枚举。关键函数调用链如下:
hub_irq() → hub_events() → hub_port_connect_change()
→ usb_alloc_dev() → usb_set_device_state()
→ usb_get_device_descriptor() → usb_set_address()
→ usb_get_device_descriptor() → usb_get_configuration()
→ usb_set_configuration() → device_add()
如果 hub_port_connect_change() 没有被触发,说明 hub 驱动根本没检测到端口状态变化。检查 /sys/kernel/debug/usb/usbmon 的日志,看看是否有端口中断上报。
4.3.2 驱动加载与绑定
设备枚举成功后,内核会根据 idVendor 和 idProduct 匹配驱动。如果匹配失败,设备会处于 unbound 状态。
用 lsusb -t 可以查看设备是否绑定了驱动:
# 正常情况
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=ehci-pci/2p, 480M
|__ Port 2: Dev 2, If 0, Class=Vendor Specific Class, Driver=uvcvideo, 480M
# 异常情况(未绑定驱动)
|__ Port 2: Dev 2, If 0, Class=Vendor Specific Class, Driver=(none), 480M
如果显示 (none),说明内核没有找到匹配的驱动。检查你的 modprobe 配置,或者手动加载驱动试试。
bDeviceClass 填了 0xFF(厂商自定义),但驱动只匹配了 bInterfaceClass。内核的匹配逻辑是:先匹配设备级别,再匹配接口级别。如果设备级别匹配失败,接口级别的匹配不会自动触发。
4.4 第四层:综合排查流程图
下面这张图总结了枚举失败的逐层排查路径。你可以把它打印出来贴在工位上。
4.5 实战案例:一次完整的枚举失败定位
最后,分享一个我印象很深的案例。某款工业相机在客户现场频繁出现无法识别的问题,换了几台主机都一样。
第一步:物理层检查
用示波器抓 D+ 波形,发现插入后 D+ 确实被拉高了,但 2ms 后突然掉到 0V。这说明设备侧的上拉电阻没问题,但设备在复位后掉电了。
第二步:协议层检查
抓 USB 包发现,主机发送了复位信号后,设备没有回复任何数据包。进一步检查发现,设备在复位期间会触发一个看门狗复位,导致 MCU 重启。
第三步:软件栈检查
查看 dmesg 日志,发现每次枚举失败时都有一条 usb 1-1: reset high-speed USB device number 2 using ehci-pci 的日志。这说明主机检测到了设备,但设备在复位后没有响应。
根因:固件中的 USB 中断处理函数被一个高优先级的定时器任务抢占,导致设备无法及时响应主机的 GetDescriptor 请求。主机超时后发起复位,看门狗被触发,MCU 重启——死循环。
解决方案:降低定时器任务的优先级,确保 USB 中断能在 50ms 内得到处理。