FEATURED · 精选文章

USB_SETUP_REQ结构体详解:从枚举失败到控制传输彻底搞懂

发布时间 / 2026/9/11 10:26:33
来源 / 创域科博编辑部
栏目 / 资讯中心
USB_SETUP_REQ结构体详解:从枚举失败到控制传输彻底搞懂 USB 这个东西寄存器、描述符、端点、管道、类、协议栈……一搜资料全是术语。以前我在调一个 STM32 的 USB 设备枚举时反复失败抓包抓到手软最后发现问题是出在固件里对USB_SETUP_REQ的解析漏了一个字段。那次之后我把 setup 包从头到尾啃了一遍发现只要把这个结构体彻底搞明白USB 协议的一大半谜团就解开了。这篇博文就是那段时间的笔记整理写给正在和设备枚举死磕的人也希望你能少走我趟过的坑。1. USB_SETUP_REQ 不是“一个请求”而是“一类请求”的入口1.1 从名字拆起SETUP、REQ 与控制传输很多第一次接触USB_SETUP_REQ的人会误以为它是一个“功能函数”或者“某个具体命令”比如GET_DESCRIPTOR之类的。实际上它不是它是一块数据结构准确说是 USB 控制传输中 setup 事务在软件层面对应的结构体。在不少单片机 USB 协议栈里尤其 ST 早期的 USB 库它长这样typedef struct _USB_SETUP_REQ { uint8_t bmRequestType; uint8_t bRequest; uint16_t wValue; uint16_t wIndex; uint16_t wLength; } USB_SETUP_REQ;bmRequestType和bRequest各占 1 字节wValue、wIndex、wLength各占 2 字节加在一起正好是 8 字节。一个 setup 包在 USB 总线上也就是 8 字节不多不少。这 8 字节不是随便拼的USB 2.0 协议规范里写得明明白白主机发起的每个控制请求都靠这 8 字节表达“你是谁、要什么、给多少”。SETUP表示事务类型REQ是 request 的缩写合在一起就是“setup 阶段的请求描述”。它只出现在控制传输里。控制传输是 USB 四种传输类型中最特殊的一种它不像 bulk、interrupt、isochronous 那样为大数据吞吐服务而是负责设备枚举、配置、状态查询这些“管理工作”。1.2 一个 setup 包到底占多少字节、每个字节干嘛拿最经典的GET_DESCRIPTOR请求举例比如主机第一次获取设备描述符总线上实际发送的 8 字节可能是0x80 0x06 0x00 0x01 0x00 0x00 0x12 0x000x80bmRequestTypebit7 为 1 表示这是设备到主机的方向即 INbit6-5 为 00 表示标准请求bit4-0 为 00000 表示接收方是设备。0x06bRequest等于GET_DESCRIPTOR。0x00 0x01wValue的低字节是描述符索引 0高字节是描述符类型 1设备描述符。注意字节序总线上先传低字节。0x00 0x00wIndex对设备描述符而言是 0。0x12 0x00wLength0x0012 正是 18 字节即设备描述符的标准长度。这 8 字节表达的意思就是“设备把你的设备描述符18 字节发给我”。主机可以在枚举阶段连续发好几个类似的 setup只是改一下wValue里的描述符类型和索引就能依次拿到设备描述符、配置描述符、字符串描述符等。1.3 为什么我建议把 USB_SETUP_REQ 当成“单子”而不是“数据”刚开始调协议栈时我总把 setup 包当“数据”看待下意识想把它存下来、转发出去。后来一个做驱动开发的前辈跟我说你应该把它看成一张“派工单”主机在单子上写明要干什么设备端拿到单子第一件事不是执行而是拆单、分单、然后决定让哪个“工人”去干。为什么要这么想因为在控制传输里setup 包之后还有数据阶段和状态阶段设备端对 setup 的处理结果直接决定后面怎么回包。如果只是把它当普通数据很容易忽略bmRequestType里的方向位导致发数据的方向搞反。把USB_SETUP_REQ当成一张“带方向、带长度、带对象”的单子你分析每个字段的时候思路会清晰很多——先看是读还是写再看操作对象是谁最后看要读/写多少字节。2. 枚举流程里的 USB_SETUP_REQ主从之间的对账2.1 从主机视角看枚举一次完整的“户口登记”我经常把 USB 枚举比作主机给设备办户口新设备插上之后主机会先给它一个地址SET_ADDRESS然后问它叫什么、住哪、家里几口人GET_DESCRIPTOR设备描述符接着问它配置清单配置描述符最后按清单办事SET_CONFIGURATION。每一次问答都是通过一个USB_SETUP_REQ发起的。典型枚举顺序一般是主机发送GET_DESCRIPTOR请求设备描述符的前 8 字节有的主机会一次要求 64 字节但设备返回时实际只有 18 字节。主机重新复位总线再SET_ADDRESS把设备地址从 0 改到一个新地址。主机用新地址再发一次GET_DESCRIPTOR这次可以完整拿 18 字节。主机继续GET_DESCRIPTOR拿配置描述符、字符串描述符。主机发送SET_CONFIGURATION让设备进入配置完成状态然后才轮到类请求和真正的数据传输。这里面有个很容易忽略的细节SET_ADDRESS是唯一比较特殊的控制请求它在 setup 阶段之后的数据阶段是零长度的——主机发完 8 字节 setup 包设备回复一个 ACK等状态阶段完成之后新地址才真正生效。如果固件在处理SET_ADDRESS时提前把本地地址改了可能导致后续控制传输的应答地址对不上设备直接消失。2.2 设备端如何处理连续的 SETUP 请求一个设备从插入到枚举完成中间收到的USB_SETUP_REQ远不止上面五六个实际调试时我数过有的主机光枚举阶段就会发 20 多个控制请求。这些请求不是“问一个答一个”的简单循环因为每个请求可能带有不同的数据阶段长度而且协议栈处理每个请求时需要切换内部状态机。以 ST 的 USB 库为例它内部维护了DeviceState、EndpointState这些状态收到 setup 包后先解析到USB_SETUP_REQ然后按bmRequestType和bRequest分发。标准请求如GET_DESCRIPTOR由Standard_Req类函数处理类请求比如 CDC 的SET_LINE_CODING交给类内部函数处理厂商请求则由用户回调处理。这个分发过程如果写得太简单很容易把各种请求之间的先后顺序和状态切换搞乱。2.3 常见枚举失败和 USB_SETUP_REQ 的关系设备插上电脑没反应、设备管理器里出现“未知设备”、或者枚举到一半设备掉线这些现象背后大概率能在USB_SETUP_REQ的处理里找到原因。我遇到过几种典型的情况设备描述符的第 7、8 字节bMaxPacketSize0写得不对比如最大包长度写错主机后续 setup 包的响应就会错乱。SET_CONFIGURATION之后没有更新内部配置状态导致主机后续类请求被忽略。处理GET_DESCRIPTOR时返回的长度比请求的wLength短但设备端没有正确结束数据阶段主机等不到状态阶段的 ACK。这些问题的修复往往不复杂但要定位就得先把USB_SETUP_REQ每一个字段和主机的心智模型对齐因为主机和设备对“单子”的理解必须一致任何一位理解有偏差枚举就卡住。3. 类型码决定“谁来处理”标准请求、类请求、厂商请求3.1 bmRequestType 的位域拆解bmRequestType这个字节是控制请求的路由器它用 bit 位划定了处理方向、请求类型和接收方。我习惯拆成三段看bit7数据传输方向。0 表示主机到设备OUT1 表示设备到主机IN。bit6-5请求类型。00 是标准请求01 是类请求10 是厂商请求11 是保留。bit4-0接收方。0 表示设备1 表示接口2 表示端点3 表示其他剩下的保留。这是整个 USB 控制请求中最容易出错的字段之一因为很多人只看bRequest是什么却忘了bmRequestType里的类型位决定了这个请求应该走哪套处理逻辑。举个例子同样是bRequest 0x06如果bmRequestType 0x80就是标准请求GET_DESCRIPTOR如果是 0xA1那就是类请求GET_REPORTHID 类。bRequest数值相同含义截然不同。3.2 标准请求的快速盘点GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION 等标准请求定义在 USB 规范第九章一共 11 个。我做项目时不需要背全但有几个必须烂熟于心GET_DESCRIPTOR (0x06)读各种描述符wValue高字节为类型低字节为索引。SET_ADDRESS (0x05)设置设备地址。SET_CONFIGURATION (0x09)让设备启用某个配置wValue低字节是配置值。GET_CONFIGURATION (0x08)读当前配置值。SET_INTERFACE (0x0B)选择备用接口设置。CLEAR_FEATURE (0x01)/SET_FEATURE (0x03)对设备、接口或端点设置或清除特性。GET_STATUS (0x00)读状态。它们的共同特点是bmRequestType的类型位都是 00。收到这类请求时设备固件通常调用公共的协议栈处理函数而不是业务代码。3.3 类请求和厂商请求的典型场景类请求是在标准请求之上由具体设备类规范定义的。比如 HID 设备常见的有GET_REPORT (0x01)、SET_REPORT (0x09)CDC ACM 设备常见SET_LINE_CODING (0x20)、GET_LINE_CODING (0x21)。类请求的bRequest取值范围和设备类相关bmRequestType的接收方通常是接口bit4-0 0x01因为同一个设备可能有多个接口每个接口各管各的类请求。厂商请求则由设备厂商自由定义典型例子就是 USB 转串口芯片内部的 EEPROM 读写、固件升级等。比如 FT231X 这类 USB 转 UART 芯片就有一套厂商专用请求用来读芯片版本、配置 IO 等。这类请求不会出现在 USB 规范里想逆向就得靠官方的驱动、文档或者抓包分析。我自己的经验是遇到类请求和厂商请求时先看bmRequestType的接收方位再看wIndex指向接口还是端点最后才看wValue和wLength。如果处理顺序反过来很容易把请求发错给别的接口。4. 抓包视角下的 USB_SETUP_REQ把看不见的握手变成看得见的记录4.1 抓包工具与接入方式很多做单片机的人都是从单片机端调试 USB很少去看主机侧到底发了什么。但遇到枚举类问题不看主机侧的包只能盲猜。抓包工具我这边常用的有三类逻辑分析仪配合 USB 解码插件、专用的 USB 协议分析仪、以及软件抓包工具。软件方向的方案对 Windows 来说有前辈们经常提起的 Bus Hound是个老牌工具能看到控制传输的 setup 内容Linux 下一般用 usbmon 加 Wireshark 就能把 USB 包抓下来不需要额外硬件。抓包时注意接线和接入点。如果只是观察总线上的一组设备通信在设备端串联一个 USB 分析仪是最干净的做法。假如你只是想看主机枚举流程用软件抓包方案就能看到所有控制请求只是看不到电气层的时序。4.2 一段典型枚举抓包的逐包解读我拿一次虚拟的抓包记录来演示假设主机向一个 HID 设备发起枚举抓包软件里会看到类似下面的控制传输序列序号方向请求wValuewIndexwLength1INGET_DESCRIPTOR (Device)0x01000x0000642OUTSET_ADDRESS0x00030x000003INGET_DESCRIPTOR (Device)0x01000x0000184INGET_DESCRIPTOR (Config)0x02000x00002555INGET_DESCRIPTOR (String)0x03020x04092556OUTSET_CONFIGURATION0x00010x00000第一行wValue 0x0100是很多新手的迷惑点。按字节序拆分低字节0x00是描述符索引 0高字节0x01是描述符类型设备描述符。后面的wLength是主机期望的最大长度设备实际返回多少由设备决定但绝不能超过wLength。第三行的wLength 18看起来是 18 字节这通常是主机在第一次拿到设备描述符的前 8 字节知道bMaxPacketSize0之后再次请求完整描述符。而配置描述符请求里常见 255 或 512 这种较大的wLength是因为设备实际返回的配置描述符长度要到wTotalLength字段里才知道主机一次请求把整个配置描述符集合拿回去再自行解析。4.3 抓包中最值得关注的两个时间点第一个值得关注的时间点是SET_ADDRESS之后。从抓包里能看到某些低速设备在SET_ADDRESS之后会有一个短暂停顿这是正常的因为设备需要时间切换地址。如果设备在SET_ADDRESS请求之后没有响应问题多半出在固件的地址更新逻辑上。第二个时间点是SET_CONFIGURATION之后。如果设备在设置配置之后又冒出一个GET_DESCRIPTOR请求别觉得奇怪有些驱动会对配置描述符做二次确认。这段抓包记录里第 6 行之后如果接着出现GET_DESCRIPTOR (Device Qualifier)说明你的设备声明了自己是高速设备而实际上枚举在 Full-Speed 下运行主机想确认设备是否支持其他速度。5. 在真实固件里处理 USB_SETUP_REQ两份代码的对照写法5.1 STM32 USB Device Library 中 Setup 阶段的标准写法STM32 生态里有两类常见代码。一类是早期的标准外设库USB 中断里经常会看到USB_SETUP_REQ这个类型名另一类是后来主推的 STM32 USB Device Library在 v2.x 版本里描述控制请求用的是USBD_SetupReqTypedef名字不同但字段几乎一样。以 v2.2.1 这类版本为例设备收到 setup 包后USB 中断回调会把(uint8_t *)pdev-pData里的 8 字节填充到USBD_SetupReqTypedef结构体然后进入USBD_StdDevReq、USBD_StdEPReq这一系列处理函数。标准请求的分发核心是一个大switch按bRequest值匹配GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION等分支。我自己写的处理流程会把它再拆细一点void Setup_Req_Handler(USB_SETUP_REQ *req) { if (req-bmRequestType 0x80) { // IN 方向设备准备数据 handle_in_request(req); } else { // OUT 方向主机准备数据 handle_out_request(req); } }这样写的好处是先把方向定了后面的数据阶段和状态阶段才能选对端点和发送方向。5.2 Linux 内核视角usb_ctrlrequest 与选择器函数在 Linux 内核里对应的结构体叫usb_ctrlrequest定义在include/uapi/linux/usb/ch9.h。结构体成员也是bRequestType、bRequest、wValue、wIndex、wLength。内核 USB 核心在枚举设备时会用这些字段构造各种标准请求比如usb_get_descriptor里就是构造一个GET_DESCRIPTOR的usb_ctrlrequest然后通过usb_control_msg发出去。如果自己写内核驱动向设备发厂商请求时也是构造这个结构体。常用于usb_control_msg的封装int ret usb_control_msg(dev, usb_rcvctrlpipe(dev, 0), req-bRequest, req-bRequestType, req-wValue, req-wIndex, buf, req-wLength, timeout);注意这里的usb_rcvctrlpipe和usb_sndctrlpipe是根据bmRequestType里的方向位二选一。如果方向判断反了就会得到一个-EPIPE或-EREMOTEIO。5.3 我习惯做的三层处理框架实际写固件时我不喜欢在一个函数里把所有逻辑堆完而是把USB_SETUP_REQ的处理拆成三层第一层解析层只做字段拆分、方向判断、合法范围检查。第二层分发层按bmRequestType的类型位把请求分给标准请求处理器、类请求处理器或厂商请求处理器。第三层执行层具体业务逻辑比如把某个字符串描述符填充到缓冲区、设置端点地址。用这种框架换芯片或者换 USB 协议栈时只需要改第一层和部分第二层业务代码基本不用动。比如同一个 HID 设备固件从 STM32F1 迁移到 CH32V307标准请求和 HID 类请求的逻辑几乎可以原样搬过去只要适配新的 USB 外设寄存器和端点 API。6. 调试 USB_SETUP_REQ 最容易翻车的四个细节6.1 wLength 比实际数据长或短wLength表示主机期望的数据字节数设备端能返回的数据比它短但不能比它长。在控制传输的数据阶段如果设备端返回的数据长度超过了wLength主机端会直接判定传输错误。反过来如果设备端返回的长度比wLength短主机将以“短包”作为传输结束标志这也是合法的。容易出现的问题在于不少新手在处理GET_DESCRIPTOR时直接把整个描述符数组成员填充到发送缓冲区不管wLength是多少。比如主机只想要字符串描述符前几个字节你把整包都发出去就有超长风险。稳妥做法是计算min(描述符实际长度, wLength)然后按这个值发送。6.2 状态阶段不结束导致总线挂起控制传输的第三个阶段是状态阶段方向和数据阶段相反。比如数据阶段是设备往主机发数据IN那么状态阶段就是主机往设备发一个零长度包OUT设备要 ACK 这个包整个控制传输才算完成。很多固件处理完数据阶段发完数据之后忘了把状态阶段准备好总线上就会一直等最终主机超时报错。我自己调试时遇到过一次 USB 设备在枚举到一半之后“消失”抓包发现设备描述符已经正确返回了但从那之后主机再发任何 setup 包都没有应答。查到最后是EP0的接收状态没有在传输完成后重新 arm导致后续 setup 包根本进不了中断。因此处理完一个USB_SETUP_REQ之后记得把端点 0 的状态恢复到可接收状态。6.3 描述符在 setup 处理之后才返回USB 控制请求从主机发出到设备返回数据实际上有一个不小的时序要求。尤其对于GET_DESCRIPTOR设备不能把描述符准备好就卡住主机。正确做法是在解析 setup 包时先准备好描述符地址和长度登记到端点传输描述里然后在数据阶段直接搬运。有些协议栈需要你在中断里执行整套流程中断函数内不宜做耗时很长的操作。只要USB_SETUP_REQ解析完毕就应立即启动数据阶段传输把实际数据的准备工作提前到 setup 包解析阶段完成。如果等到中断退出后才慢慢查表、组包就可能面临 USB 帧起始超时的风险在高负载的主机上还会导致枚举不稳定。6.4 把 USB 转串口芯片和协议栈搞混的排查教训最后分享一个我实际踩过的坑。当时在调一块板子芯片通过一个 USB 转 UART 桥接芯片和电脑通信。现象是驱动装不上设备管理器里报“设备描述符请求失败”。我一上来怀疑是桥接芯片的固件问题围着芯片转了半天甚至想重新烧驱动。后来抓包一看主机发出的第一个GET_DESCRIPTOR请求根本没有得到任何响应总线上连 ACK 都没有。问题根本不在 USB 转串口芯片的 firmware而是桥接芯片上游的 UART 逻辑没有上电导致整个 USB 设备核心没有起来。所以当你看到一个 USB 设备枚举异常先不要急着怀疑是这个设备里的某个外设或驱动先确认最上游的 USB 设备核心是否真的给USB_SETUP_REQ回了包。抓包里只要能看到设备对第一个GET_DESCRIPTOR有响应问题才往上层找。我现在调试新 USB 设备时已经养成了一个习惯无论是 STM32、CH32 还是 ESP32-S3上手第一步先用抓包工具记录枚举阶段的所有USB_SETUP_REQ把这 8 字节的数据对着规范一个字一个字读一遍再动手改代码。很多时候问题并不是“算法不够难”而是对这张单子的理解出现了偏差。把USB_SETUP_REQ吃透USB 设备开发的拦路虎至少能少一半。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻