4. USB控制传输:控制传输的四个阶段与标准设备请求
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊USB控制传输——这个在USB协议栈里最基础、也最容易被忽视的部分。
说实话,我刚入行那会儿,觉得控制传输不就是发几个命令嘛,有啥好研究的?直到有一次调试一个USB摄像头,死活枚举不过去,折腾了两天才发现是控制传输的Status阶段时序没处理好。嗯,从那以后我再也不敢小看它了。
4.1 控制传输的四个阶段
控制传输,说白了就是USB通信的"控制通道"。它由四个阶段组成,缺一不可。我习惯把这四个阶段记成:Setup → Data → Status,但Data阶段有时候可以没有。
| 阶段 | 方向 | 作用 |
|---|---|---|
| Setup阶段 | Host → Device | 发送8字节的请求包 |
| Data阶段(可选) | 双向 | 传输数据,最多64字节(全速) |
| Status阶段 | Device → Host | 确认传输结果 |
4.2 Setup阶段:请求的起点
每个控制传输都从Setup阶段开始。Host会发送一个8字节的Setup包,里面包含了请求的类型、方向和参数。
这个8字节的结构长这样:
typedef struct {
uint8_t bmRequestType; // 请求类型:方向、类型、接收者
uint8_t bRequest; // 请求码
uint16_t wValue; // 值字段
uint16_t wIndex; // 索引字段
uint16_t wLength; // 数据阶段长度
} usb_setup_packet_t;
我个人习惯把bmRequestType拆开来看:
- Bit 7:方向(0=Host到Device,1=Device到Host)
- Bit 6-5:类型(0=标准,1=类,2=厂商)
- Bit 4-0:接收者(0=设备,1=接口,2=端点,3=其他)
举个例子,0x80 表示Device到Host的标准设备请求,0x00 表示Host到Device的标准设备请求。
4.3 Data阶段:数据的搬运
Data阶段不是必须的。如果请求不需要数据(比如Set Address),那Data阶段就跳过,直接进Status。
需要数据时,Data阶段可以传输最多64字节(全速设备)或1024字节(高速设备)。传输方向由Setup包的bmRequestType决定。
我记得有一次做U盘项目,Get Descriptor返回的数据长度总是对不上。查了半天,发现是wLength字段我写死了,没根据实际描述符长度动态调整。这种坑,踩过一次就记住了。
4.4 Status阶段:确认收工
Status阶段是控制传输的收尾。Host发送一个零长度的IN或OUT事务,Device用ACK或NAK来回应。
- IN方向:Device返回一个零长度包,表示"我收到了,没问题"
- OUT方向:Host发送一个零长度包,Device用ACK确认
4.5 标准设备请求:USB的"基本指令集"
USB协议定义了一组标准设备请求,所有USB设备都必须支持。这些请求通过控制传输来发送。
常用的几个:
| 请求码 | 名称 | 作用 |
|---|---|---|
| 0x05 | Set Address | 给设备分配地址 |
| 0x06 | Get Descriptor | 获取设备描述符 |
| 0x07 | Set Configuration | 设置设备配置 |
| 0x08 | Get Configuration | 获取当前配置 |
| 0x09 | Set Interface | 设置接口备用设置 |
| 0x0A | Get Interface | 获取当前接口设置 |
4.6 Get Descriptor:枚举的第一步
设备上电后,Host第一个发的请求就是Get Descriptor。它会请求设备描述符,长度通常是18字节。
设备描述符的结构:
typedef struct {
uint8_t bLength; // 描述符长度(18)
uint8_t bDescriptorType; // 描述符类型(1)
uint16_t bcdUSB; // USB版本号(如0x0200)
uint8_t bDeviceClass; // 设备类
uint8_t bDeviceSubClass; // 设备子类
uint8_t bDeviceProtocol; // 设备协议
uint8_t bMaxPacketSize0; // 端点0最大包大小
uint16_t idVendor; // 厂商ID
uint16_t idProduct; // 产品ID
uint16_t bcdDevice; // 设备版本号
uint8_t iManufacturer; // 厂商字符串索引
uint8_t iProduct; // 产品字符串索引
uint8_t iSerialNumber; // 序列号字符串索引
uint8_t bNumConfigurations; // 配置描述符数量
} usb_device_descriptor_t;
这里有个细节:Host第一次请求时,只会请求前8个字节(因为还不知道端点0的最大包大小)。设备需要正确返回前8个字节,Host再根据bMaxPacketSize0重新请求完整描述符。
4.7 Set Address:给设备一个"身份证"
Set Address是枚举过程中第二个关键请求。Host会给设备分配一个唯一的地址(1-127)。
这个请求比较特殊:
- 它没有Data阶段
- 设备收到后,必须在Status阶段完成后才能切换到新地址
- 切换地址的时机:Status阶段完成后,设备立即使用新地址
我记得有一次调试,设备在Set Address后没有及时切换地址,导致后续请求全部超时。查了协议才发现,地址切换必须在Status阶段完成后立即生效,不能有延迟。
4.8 其他常用请求
Set Configuration:激活一个配置。设备可以有多个配置,但同一时间只能激活一个。
Get Configuration:返回当前激活的配置号。我习惯在调试时用这个来确认配置是否生效。
Set Interface:切换接口的备用设置。比如音频设备可能有不同的采样率设置。
Get Interface:返回当前接口的备用设置号。
4.9 实战中的注意事项
控制传输看似简单,但实际移植时容易踩坑。我总结了几点:
- Setup包解析要严谨:每个字段都要按协议解析,特别是wLength和方向
- Data阶段长度控制:不要超过wLength指定的长度
- Status阶段响应要快:超时会导致Host重试或复位
- STALL的处理:不支持的命令要返回STALL,不要假装成功
- 地址切换时机:Set Address后立即生效,不能有延迟
好了,关于控制传输的四个阶段和标准设备请求,今天就聊到这里。下一章我们会深入讨论描述符的结构和解析,到时候我会分享一些我在实际项目中遇到的奇葩问题。
记住:控制传输是USB通信的基石,搞懂了它,后面的批量传输、中断传输、同步传输都会轻松很多。