USB枚举过程详解:设备插入检测、复位操作、地址分配、描述符请求

USB枚举,说白了就是主机和设备之间的「握手仪式」。

每次设备插上去,主机都要走一遍这个流程——确认你是谁、你是什么类型、你需要多少带宽。我刚开始做USB驱动时,总觉得枚举是个黑盒子,直到有一次调试一个U盘死活不识别,才逼着自己把每个字节都啃透了。

今天咱们就把这个流程拆开揉碎,一个一个看。

1. 设备插入检测

设备插上去,主机怎么知道的?

USB接口有4根线:VBUS(电源)、D+、D-、GND。设备插入时,D+或D-上的电平会发生变化。

  • 全速/高速设备:设备内部把D+线拉高(1.5kΩ上拉电阻)
  • 低速设备:设备内部把D-线拉高

主机端的根集线器(Root Hub)会持续监测这两根数据线的电平。一旦检测到电平变化,就知道有设备插入了。

我个人的小经验:调试时如果设备插上没反应,先量一下D+或D-的电平。我遇到过好几次,是上拉电阻虚焊导致的。

2. 复位操作

检测到设备后,主机不会马上跟你聊天。它先做一件事——复位。

复位是怎么做的?主机把D+和D-同时拉低,持续至少10ms。设备检测到这个信号后,会把自己恢复到默认状态:

  • 地址恢复为0(默认地址)
  • 进入默认状态(Default State)
  • 准备好接收控制传输

复位结束后,设备会进入「地址状态」之前的那个阶段——我们叫它「默认状态」。这时候设备只能用地址0跟主机通信。

注意:复位期间设备不能响应任何请求。我曾经踩过坑,在复位还没结束时就去发请求,结果设备根本不搭理我。

3. 地址分配

复位完成后,主机要给设备分配一个唯一的地址。为什么?因为总线上可能挂多个设备,每个设备必须有自己的「门牌号」。

分配地址的流程是这样的:

  1. 主机发送SET_ADDRESS请求(控制传输,默认地址0)
  2. 请求中包含新地址(比如1、2、3...)
  3. 设备收到后,保存这个地址
  4. 设备返回状态阶段(Status Stage)
  5. 之后所有通信都使用新地址

这里有个细节:SET_ADDRESS请求的Data Stage是空的,地址信息放在Setup包中。设备必须在状态阶段完成后才能切换地址。

关键点:地址0是保留地址,只能用于枚举阶段。枚举完成后,设备必须使用非零地址。

4. 描述符请求

地址分配完了,主机开始问设备:「你是谁?你有哪些功能?」

描述符就是设备的「身份证」。主机通过一系列GET_DESCRIPTOR请求来获取这些信息。

4.1 设备描述符(Device Descriptor)

这是第一个被请求的描述符。主机先要18个字节,看看基本信息。

设备描述符包含:

  • bLength:描述符长度(固定18)
  • bDescriptorType:描述符类型(1表示设备描述符)
  • bcdUSB:USB规范版本号(如0x0200表示USB 2.0)
  • bDeviceClass:设备类(0表示在接口描述符中定义)
  • bDeviceSubClass:子类
  • bDeviceProtocol:协议
  • bMaxPacketSize0:端点0的最大包大小(8、16、32、64)
  • idVendor:厂商ID(VID)
  • idProduct:产品ID(PID)
  • bcdDevice:设备版本号
  • iManufacturer:厂商字符串描述符索引
  • iProduct:产品字符串描述符索引
  • iSerialNumber:序列号字符串描述符索引
  • bNumConfigurations:配置描述符数量
避坑指南:我曾经遇到一个设备,bMaxPacketSize0填的是64,但实际端点0只能发8字节。结果主机读描述符时数据被截断,枚举失败。后来发现是设备固件写错了。

4.2 配置描述符(Configuration Descriptor)

拿到设备描述符后,主机接着请求配置描述符。配置描述符告诉主机:这个设备需要多少电流、有多少个接口。

配置描述符结构:

  • bLength:9字节
  • bDescriptorType:2(配置描述符)
  • wTotalLength:整个配置的总长度(包括接口、端点等)
  • bNumInterfaces:接口数量
  • bConfigurationValue:配置值(用于SET_CONFIGURATION)
  • iConfiguration:配置字符串索引
  • bmAttributes:属性(自供电、远程唤醒等)
  • bMaxPower:最大电流(单位2mA)

注意:主机第一次请求时,通常只读前9字节,拿到wTotalLength后,再读完整的数据。

4.3 接口描述符(Interface Descriptor)

配置描述符后面跟着接口描述符。一个配置可以有多个接口,每个接口代表一个功能。

接口描述符包含:

  • bLength:9字节
  • bDescriptorType:4(接口描述符)
  • bInterfaceNumber:接口编号
  • bAlternateSetting:备用设置
  • bNumEndpoints:端点数量(不包括端点0)
  • bInterfaceClass:接口类(如HID、Mass Storage)
  • bInterfaceSubClass:子类
  • bInterfaceProtocol:协议
  • iInterface:接口字符串索引
举个例子:一个USB键盘,通常只有一个接口(HID类)。但一个USB摄像头可能有视频接口和音频接口,每个接口都有自己的描述符。

4.4 端点描述符(Endpoint Descriptor)

接口描述符后面跟着端点描述符。每个非0端点都需要一个描述符。

端点描述符结构:

  • bLength:7字节
  • bDescriptorType:5(端点描述符)
  • bEndpointAddress:端点地址(位7:方向,0=OUT,1=IN;位3-0:端点号)
  • bmAttributes:传输类型(0=控制,1=等时,2=批量,3=中断)
  • wMaxPacketSize:最大包大小
  • bInterval:轮询间隔(中断端点用,单位ms或125μs)

我举个例子:一个批量传输的IN端点,地址可能是0x81(端点1,方向IN),最大包大小64字节。

注意:端点0没有描述符!它是默认存在的控制端点。我见过有人给端点0也写描述符,结果主机解析出错。

5. 枚举完成后的操作

所有描述符都读完后,主机已经掌握了设备的全部信息。接下来:

  1. 主机加载对应的驱动程序
  2. 驱动发送SET_CONFIGURATION请求,选择一个配置
  3. 设备进入配置状态(Configured State)
  4. 开始正常的数据传输

嗯,到这里,枚举流程就走完了。你想想看,整个过程其实就是一个「问-答」模式:主机问一句,设备答一句。但每个环节都有坑,尤其是描述符的长度和内容,错一个字节设备就认不出来。

我个人的调试习惯:用USB分析仪抓包,把枚举过程的每个事务都看一遍。哪个请求没响应、哪个描述符长度不对,一目了然。比看代码快多了。

最后说一句:枚举是USB通信的基础。你把这个流程吃透了,后面写驱动、调硬件,心里就有底了。