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的标准设备请求。

我的经验:调试时经常遇到Setup包解析错误。我建议你在固件里加个打印,把收到的8字节原样dump出来,对照协议一看就明白问题出在哪了。

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确认
注意:Status阶段的时序非常严格。我曾经遇到过Device在Status阶段返回了STALL,结果Host直接复位总线,整个枚举过程重来。所以,确保你的固件在Status阶段能正确响应。

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重新请求完整描述符。

避坑指南:我曾经在移植USB栈时,Get Descriptor返回了完整18字节,但Host只请求了8字节。结果我的固件一次性发了18字节,Host只收了前8个,后面的数据就乱了。正确的做法是:根据wLength字段决定返回多少数据。

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 实战中的注意事项

控制传输看似简单,但实际移植时容易踩坑。我总结了几点:

  1. Setup包解析要严谨:每个字段都要按协议解析,特别是wLength和方向
  2. Data阶段长度控制:不要超过wLength指定的长度
  3. Status阶段响应要快:超时会导致Host重试或复位
  4. STALL的处理:不支持的命令要返回STALL,不要假装成功
  5. 地址切换时机:Set Address后立即生效,不能有延迟
我的调试技巧:在固件里加一个控制传输的日志功能,把每个Setup包的内容、Data阶段的数据、Status阶段的结果都打印出来。这样一旦出问题,看日志就能定位到是哪个阶段出了问题。

好了,关于控制传输的四个阶段和标准设备请求,今天就聊到这里。下一章我们会深入讨论描述符的结构和解析,到时候我会分享一些我在实际项目中遇到的奇葩问题。

记住:控制传输是USB通信的基石,搞懂了它,后面的批量传输、中断传输、同步传输都会轻松很多。