3. USB枚举过程:从物理连接到逻辑就绪
用户插进一个USB设备,然后呢?设备就自动能用了?
说白了,背后有一套完整的“握手”流程。USB协议规范里叫它枚举(Enumeration)。
从D+/D-线上那个电平跳变开始,到操作系统识别出设备类型、分配好驱动,一共要走五步。
我最早啃USB协议时,一直以为枚举就是“发几个描述符”那么简单。结果第一次搭逻辑分析仪抓波形,才发现这里面的时序坑多得很。今天我把每一步拆开讲,顺便聊聊我掉过的那些坑。
3.1 物理连接检测:上拉电阻“打招呼”
设备插进去的第一件事——不是通电,而是让主机知道“有东西进来了”。
USB总线在空闲时,D+和D-都是低电平(主机端15kΩ下拉)。设备端会在其中一根数据线上挂一个1.5kΩ上拉电阻:
- 全速/高速设备:上拉电阻接在D+上,空闲时D+被拉到约3.3V
- 低速设备:上拉电阻接在D-上,空闲时D-被拉到约3.3V
主机根端口的Hub检测到D+或D-电压从0跳到3V左右,就知道有新设备接入了。这时候HUB会把对应端口的电源打开,然后通知主控制器。
3.2 复位(Reset):让设备回到已知状态
主机检测到设备插入后,不会立刻发数据。它先让HUB对端口发送复位信号——把D+和D-同时拉低并保持至少10ms(standard要求)。
这10ms里设备看到总线连续低电平,就知道自己被复位了。
复位完成后,设备地址会回到默认地址0,所有端点也回到配置未设置状态。
为什么会复位这么久?
嗯,主要是给设备内部的锁相环、时钟恢复电路一个稳定的初始化窗口。我见过一款国产USB芯片,复位时间只要8ms就初始化完毕,但规范里写的是10ms,差一点就可能被某些严格的主机认定复位失败。所以我建议:设计固件时至少等12ms再响应后续事务,给自己留点余量。
3.3 地址分配:SET_ADDRESS 事务
复位后设备处于默认地址0。主机通过地址0发送SET_ADDRESS请求,把一个新的唯一地址分配给设备。这个地址范围是1~127(0保留)。
设备收到请求后,必须先完成状态阶段(ACK),然后才能切换到新地址。刚切换完的新地址还不能立刻用——主机需要等2个微帧(约125μs)才能开始用新地址通信。这就是地址生效延迟。
| 步骤 | 主机动作 | 设备响应 |
|---|---|---|
| 1 | 向地址0发SET_ADDRESS(新地址N) | 收到请求,内部记录地址N |
| 2 | 等待状态阶段(设备发ACK) | 发ACK,同时切换到地址N |
| 3 | 等待至少2个UTMI帧(约125μs) | 设备准备就绪 |
| 4 | 用新地址N发送下一个事务 | 用地址N响应 |
我遇到过一种情况:设备在切换地址后立即收到了主机发来的IN令牌,因为延迟不足导致令牌被忽略,然后主机重试几次后超时,枚举失败。所以设备固件里一定要实现这个延迟。标准里没有强制要求主机等待多久,但大多数主机驱动会等待1~2ms,保守一点总没错。
3.4 描述符获取:多轮“自我介绍”
地址分配完了,主机还不知道这是个啥设备。接下来就是一轮又一轮的描述符获取。
设备必须实现至少三种描述符:
- 设备描述符(Device Descriptor):18字节,包含USB版本、设备类、厂商ID、产品ID等。主机第一次只读前8字节,知道后续要读多大。
- 配置描述符(Configuration Descriptor):包含该配置下所有接口、端点的信息。主机先读9字节(配置描述符头),里面有个字段
wTotalLength告诉主机整个配置描述符集(包括接口、端点描述符)的总长度,然后主机再一口气读完。 - 接口描述符(Interface Descriptor):描述一个逻辑接口(如HID、CDC、Mass Storage)。每个配置可以有多个接口。
- 端点描述符(Endpoint Descriptor):描述每个端点的方向、类型、最大包大小、轮询间隔。
wTotalLength写错了),主机可能只读部分数据,导致枚举卡死。
我调试过一个奇怪的问题:设备能正常读到设备描述符,但配置描述符总是读不全。后来用USB分析仪一抓,发现第一次读9字节时设备返回了正确的 wTotalLength(假设0x29),但第二次主机发起读0x29字节的请求时,设备只响应了0x20字节就停止了。原因是端点FIFO在传输中途溢出了。解决方案很简单——把配置描述符的长度调整成端点最大包大小的整数倍,或者开启乒乓模式。
3.5 配置选择:SET_CONFIGURATION
主机读完所有描述符后,会选择一个合适的配置(通常是第一个配置)。发送SET_CONFIGURATION请求,参数是 bConfigurationValue(在配置描述符里定义)。
设备收到后,必须根据这个值使能对应的端点、分配缓冲区、进入配置状态。配置完成后设备就可以正常收发数据了。
这里有个易错点:同一个设备可以定义多个配置(比如一个全速配置、一个高速配置),但主机只会选其中一个。如果配置描述符里的供电参数(比如最大电流)写错了,主机可能拒绝配置。我记得有一次设计了一款USB充电设备,电流写成了500mA,主机算出来总线电流超标,直接配置失败。后来改成100mA才通过。
3.6 枚举全过程流程图
我把整个枚举流程画成了状态图,方便大家对照理解:
3.7 总结:枚举失败的常见“暗礁”
根据我多年的排查经验,枚举失败通常集中在以下几个环节:
- 上拉电阻错误:速率不匹配,主机检测不到插入。
- 复位时间不足:设备内部未就绪,响应第一个SET_ADDRESS时出错。
- 地址冲突:同一总线上两个设备响应了同一个SET_ADDRESS(极少见,但如果你做USB Hub测试会碰到)。
- 描述符长度错误:比如
bLength字段跟实际数据长度不符,主机读不全或读多了导致解析异常。 - 端点描述符中的bInterval太小:对于中断端点,最小间隔是1ms(全速),高速设备最小125μs。写错可能导致主机拒绝配置。
- 电流超限:配置描述符里
bMaxPower以2mA为单位,写0x32代表100mA。如果超过主机当前可提供的电流,配置会被拒绝。
好啦,这一步一步走过来,设备就从物理连接进入了逻辑就绪状态。
后续的通信、错误恢复、挂起/唤醒,都建立在这个成功枚举的基础上。