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,问题一目了然。

注意:高速设备在初始阶段也是以全速模式(D+ 上拉)被识别的。如果 D+ 上拉电阻失效,高速握手根本不会发生。

4.1.2 用示波器抓取枚举波形

我个人习惯在枚举失败时,先拿示波器抓 D+/D- 的波形。关键看几个点:

  1. 插入瞬间:D+ 是否从 0V 跳变到 3.3V(全速)或 0.4V(高速?)
  2. 复位信号:主机是否拉低 D+/D- 至少 10ms
  3. 帧起始包(SOF):复位后是否有连续的 1ms 间隔的 SOF 包
  4. 设备响应:主机发出 GetDescriptor 请求后,设备是否回复 ACK
信号阶段 正常波形特征 异常表现
插入检测 D+ 从 0V → 3.3V D+ 始终 0V,或跳变后立刻回落
总线复位 D+/D- 同时拉低 ≥10ms 复位时间不足,或根本没有复位
SOF 包 每 1ms 出现一次差分信号 无 SOF,或 SOF 间隔异常
设备响应 主机请求后设备回复 ACK 无响应,或回复 NAK/STALL
小技巧:抓波形时,把示波器时基设在 100ms/div,先看整体流程。如果发现复位后没有 SOF,问题大概率在主机侧(比如 USB 控制器挂了)。

4.2 第二层:USB 协议层握手

物理层信号正常后,接下来是协议层的枚举握手。主机和设备之间会交换一系列标准请求。我建议你打开 usbmon 或 Wireshark 抓 USB 包,看看枚举到了哪一步卡住了。

4.2.1 标准枚举流程

一个完整的 USB 枚举流程是这样的:

  1. GetDescriptor(Device):主机读取 8 字节设备描述符
  2. SetAddress:主机分配唯一地址
  3. GetDescriptor(Device) 完整:主机再次读取完整设备描述符
  4. GetDescriptor(Configuration):主机读取配置描述符
  5. SetConfiguration:主机选择配置

如果卡在第一步,说明设备连最基本的描述符都没返回。我曾经调试过一款相机,设备描述符里的 bMaxPacketSize0 填了 64,但实际端点只支持 8 字节包——主机读不完描述符,直接放弃枚举。

4.2.2 常见协议层错误

  • 设备无响应:主机发送请求后,设备没有 ACK。检查设备固件是否跑起来了,中断处理是否正常。
  • STALL 错误:设备回复 STALL,表示不支持该请求。常见于描述符索引错误。
  • CRC 错误:数据包 CRC 校验失败。通常是硬件信号质量问题,比如 D+/D- 走线过长。
  • 超时:主机等待设备响应超时。检查设备的中断服务程序是否被高优先级任务阻塞。
核心原则:协议层的问题,90% 可以通过抓包定位。不要靠猜,让数据说话。

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 配置,或者手动加载驱动试试。

避坑指南:我曾经遇到过一个问题——驱动加载了,但 probe 函数没被调用。后来发现是设备描述符里的 bDeviceClass 填了 0xFF(厂商自定义),但驱动只匹配了 bInterfaceClass。内核的匹配逻辑是:先匹配设备级别,再匹配接口级别。如果设备级别匹配失败,接口级别的匹配不会自动触发。

4.4 第四层:综合排查流程图

下面这张图总结了枚举失败的逐层排查路径。你可以把它打印出来贴在工位上。

USB 枚举失败逐层排查流程图 设备插入,枚举失败 第一层:物理层信号检查 D+/D- 电平、上拉电阻、示波器抓波形 物理层 正常? 否 修复硬件问题 是 第二层:协议层握手检查 抓 USB 包、检查枚举流程、分析 STALL/超时 协议层 正常? 否 修复固件/描述符 是 第三层:主机软件栈检查 usb_core、hub 驱动、驱动绑定 驱动绑定 成功? 否 修复驱动匹配 是 枚举成功

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 内得到处理。

经验总结:枚举失败的问题,90% 可以通过逐层排查找到根因。不要跳过物理层直接查驱动,也不要只看软件不看硬件。每一层都有它自己的“语言”——物理层看波形,协议层看包,软件层看日志。