ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

USB主从架构与端点模型详解:从包结构到枚举流程

USB主从架构与端点模型详解:从包结构到枚举流程 做了这么多年嵌入式我有个特别深的感触搞单片机的朋友玩I2C、SPI、UART那叫一个得心应手示波器一夹、逻辑分析仪一挂几分钟就能定位问题。可一到USB就开始发怵总觉得这玩意儿像个黑盒子硬件上电就能用一旦出问题就不知道从哪里下嘴。上篇咱们把USB从诞生到普及的历史捋了一遍这篇进入正题把USB的整体架构和端点通信模型一层层剥开讲清楚数据到底是怎么在一条总线上“排着队”走的。这篇文章适合两类人。一类是正在做USB设备端的固件工程师比如你要调一个自定义HID设备、一个USB转串口或者一个U盘主控理解架构能帮你少走很多弯路。另一类是写上位机或者调试驱动时被USB问题折磨的工程师——你不需要会写固件但你需要知道设备在枚举时到底发生了什么才能从一堆抓包数据里找到蛛丝马迹。我会尽量把话说得通俗但该给的技术细节一个也不会少。1. 整个总线只有一个“指挥官”USB主从架构的真相1.1 为什么USB长得像一棵倒挂的树USB的拓扑结构说穿了就是一棵以主机Host为根、向下分叉的树。主机里有根HubRoot Hub根Hub下面可以接设备也可以接外置Hub外置Hub再往下接更多设备。整个结构是这样的主机在最顶上Hub是中间的分叉节点设备是末端的叶子。理论上一个根Hub下面最多可以挂127个设备因为USB协议只给了7位地址空间地址0还留作默认地址所以实际可用地址是1到127。很多人第一次看这个拓扑会问为什么不用总线型或者星型这里有个物理上的硬约束——USB的电气信号传输距离有限线缆一长信号就衰减而且一个端口驱动能力有限所以必须靠Hub做信号中继和供电分发。Hub本质上是一个“信号放大器端口扩展器”它把主机发下来的包广播到所有下游端口又负责把下游设备返回的数据包转发给主机。这就引出了USB一个极其重要的特性这条总线是共享的而且只有一个“指挥官”。任何设备都不能主动向总线上发数据所有通信都必须由主机发起。哪怕鼠标动了、键盘按了设备也只能干等着直到主机发一个IN令牌包来问它“你有没有数据要给我”它才能把数据放上总线。这种模式叫“主机轮询”Host Polling和以太网那种“谁想发就发冲突了再退避”的对等模型完全不是一回事。1.2 主从架构的得与失简单可靠但主动不了USB选择这种严格的主从架构我当时第一次接触时觉得“这也太憋屈了”设备跟个受气包似的。但后来仔细想想这恰恰是USB能做得这么便宜、这么皮实的关键。因为有了明确的主从关系总线上的冲突问题从根本上被避免了。以太网要搞CSMA/CD那一套来应对碰撞而USB根本不需要——主机统一调度同一时刻只会有一个设备在传输数据包不会“撞车”。这个特性带来的直接好处是协议实现简单设备端可以做得非常便宜一个几毛钱的单片机都能挂USB不需要像以太网那样复杂的仲裁机制硬件成本低确定性好主机可以精确控制带宽分配这对音视频等实时性要求高的场景特别关键但代价也很明显——设备端永远没办法主动发起通信。比如你想让设备在某个事件发生时立刻通知主机在USB的世界里做不到“立刻”它只能等主机下一次轮询到它时才能上报。所以USB的中断传输本质上也是轮询只不过轮询间隔可以设得很短让人感觉像“中断”一样。这个特性在设计中会带来一些连锁反应。举个例子很多USB设备需要“异步通知”功能比如一个USB转串口芯片收到了串口数据它想赶紧告诉主机。但由于设备不能主动发数据它只能靠主机不断轮询它的中断端点来取数据。所以你在写上位机时会发现读取这类设备时经常要开一个线程循环去读这个“主动读”的动作本质上就是在配合USB这种轮询机制。1.3 带宽是大家的超了就得排队既然是一条共享总线带宽就是所有人抢着用的。USB 2.0高速模式的理论带宽是480Mbps注意这个数字是“总线带宽”不是“某个设备的专享带宽”。同一时刻总线上只能有一个传输在跑其他设备都得等着。这个感觉有点像老式电话线的“分时复用”——每个人都在一条线上说话轮到谁谁才能开口。这就带来一个在系统设计上很重要的认知当你决定用USB连接多个设备时总带宽是需要做预算的。一个USB摄像头如果占了200Mbps一个U盘再跑个150Mbps留给其他设备的带宽就不多了。尤其要注意的是USB协议为了保证实时性会优先分配等时传输和中断传输的带宽剩下的才给批量传输用。所以如果你接了一个不间断传输数据的等时音频设备U盘的读写速度会明显被压下来。这不是故障而是协议本身的带宽调度策略。2. 数据在线上长什么样从差分信号到包封装2.1 一根线怎么既传数据又传时钟看USB 2.0的线序四根线VBUS、GND、D、D-其中D/D-是一对差分信号线。数据不是用“高低电平”单端传输的而是用“两根线上的电压差”来表示逻辑状态。这样做的最大好处是抗干扰能力强——共模噪声会同时叠加在两根线上做差之后噪声就被抵消了。但这里有个更巧妙的设计USB数据线上没有单独的时钟线时钟是编码在数据里的。USB用的是NRZI编码Non Return to Zero Inverted规则是遇到逻辑0电平翻转遇到逻辑1电平保持不变。也就是说接收端不是靠边沿来对齐时钟的而是靠数据的翻转来恢复时钟。但这就带来一个问题——如果连续传输多个1电平就一直不变接收端没法知道过了多少个位。所以USB规定了一个“位填充”Bit Stuffing机制连续出现6个逻辑1之后发送端必须强制插入一个逻辑0让电平翻转一下这样接收端就能靠这个翻转重新同步时钟。我第一次看这个编码机制的时候想到一个生活类比——这就像两个人约好每隔一段时间必须say一句话哪怕没话说也要“喂”一声就是为了让对方知道“我还活着、你还在线上”。USB的位填充就是这个“喂”一声的动作。2.2 SYNC、PID、地址和端点号一次USB传输的“信封”USB协议里所有数据传输都被封装成“包”Packet。一个包的结构从前往后大致是SYNC字段一串固定的同步序列让接收端对齐时钟和位流PIDPacket Identifier包类型标识告诉接收端“我是谁”——是令牌包、数据包还是握手包地址和端点字段令牌包才有包含7位设备地址和4位端点号数据字段数据包才有承载实际数据CRC校验检错用的令牌包用CRC5数据包用CRC16这里要注意PID只有4位但因为后面还要跟一个逐位取反的副本用于校验所以实际在线上是8位。协议的冗余设计比你想象中要多这也是USB可靠性高的重要原因。在USB世界里包一共分三大类类型常见PID谁发的作用令牌包IN、OUT、SETUP、SOF主机发起一个事务指定设备和端点数据包DATA0、DATA1、DATA2、MDATA主机或设备承载实际数据握手包ACK、NAK、STALL、NYET设备或主机确认/忙/错误/未就绪事务Transaction就是由这种包按固定顺序拼出来的。主机先发一个令牌包“点名”然后数据阶段传数据最后握手阶段告诉对方“收到了”或“没收到”。三个包合起来才算完成一次事务。2.3 一个批量读事务的完整过程拿最常见的批量读来举例。假设主机想从一个U盘读取数据整个过程是这样的主机发一个IN令牌包里面写着“设备地址3端点号2我要向你要数据”设备收到后如果数据准备好了就把数据打包成DATA1包发回给主机主机正确收到数据后发一个ACK握手包告诉设备“数据完整收到你可以发下一批了”如果设备数据没准备好它会回一个NAK握手包主机就知道“现在没数据我改天再来”然后这个事务就结束了。NAK不是错误是设备在说“我现在忙但没坏”。如果这个寄存器设备发现端点的Halt特性被设置了它就会回STALL这个就属于错误状态了需要主机介入处理。区分NAK和STALL是踩坑高发区很多人在抓包软件里看到STALL就以为设备挂了其实很多时候是设备固件里设了STALL来表示“这个请求我不支持”比如HID设备收到一个它不认识的类特定请求时就可以回STALL。理解了这个“令牌-数据-握手”的三段式结构后面看抓包就轻松多了因为你一眼就能看出哪段是主机发起的、哪段是设备响应的、哪段是确认收尾的。3. 端点是USB世界的“门牌号”理解端点与管道模型3.1 端点不是一根线而是设备内部的一小块“缓冲区”端点是USB协议里最核心、也最容易让人误解的概念。很多人习惯把端点想象成“物理引脚”或者“接口”其实完全不是。端点是设备内部的一个数据缓冲区是设备与主机之间数据传输的“收发窗口”。主机通过端点号和数据方向来寻址这个窗口往里写数据或从里面取数据。每个端点都有四个关键属性端点号0~15、方向IN/OUT、传输类型控制/批量/中断/等时、最大包大小。这些属性不是凭空定的而是设备在枚举时通过端点描述符告诉主机的。比如一个全速批量端点最大包大小通常是64字节一个高速批量端点最大包大小是512字节。这里要特别强调一个很多人都会踩的坑除了端点0一个端点号通常只对应一个方向。也就是说端点1的输出和端点1的输入实际是两个不同的端点。你在写固件时要是想做一个既能收又能发的功能得同时配置1-IN端点和1-OUT端点并分别分配缓冲区。我看到不少新手在配置寄存器时只配了一个方向然后调试半天发现数据通道不通其实就是没搞清楚这个规则。用个生活化的方式来理解端点就像小区楼下的收件柜。每个柜子有编号端点号有的柜子是“只收件”OUT有的是“只取件”IN有的柜子大批量能放大量包裹有的柜子小中断一次只能放一个有的柜子有响应确认批量/中断/控制收了要回执有的柜子没有等时放了就走丢件不管。3.2 管道已经约定好传输方式的访问通道管道Pipe是主机视角下的概念。主机不是直接往端点里读写数据的而是先跟端点建立一条“管道”之后的数据传输都沿着这条管道走。管道可以理解成“主机与设备端点之间的一条逻辑连接”它把端点的各种属性端点号、方向、传输类型、最大包大小都封装好了上层软件只需要对着管道读写而不用每次都重复指定这些参数。管道分两类控制管道双向的专门走控制传输默认管道0在设备一上电时就建立好了用来做枚举和标准请求流管道单向的可以是中断、批量或等时传输数据按流的方式流动没有命令语义“流管道”这个词值得琢磨一下。它强调这几种传输类型是“数据流模式”的一次发一批数据发了就完了没有“请求-应答”的协议语义嵌套在里面。真正有命令语义的是控制传输它像一套“带外信号”专门用来配置设备。3.3 端点0所有设备都必须有的“公共服务窗口”端点0是USB里最特殊的端点。它在设备上电、还没有分配地址之前就已经存在了主机通过默认地址0向端点0发送控制请求。USB规范要求所有设备必须支持端点0的控制传输端点0是双向的只有它一个端点“既做IN又做OUT”。端点0的最大包大小是设备自己声明的低速设备是8字节全速设备可以是8/16/32/64字节中的一个高速设备固定64字节。设备在枚举最开始必须先响应一个请求让主机知道“我的端点0最大包是多少”然后主机会重新复位设备给设备分配一个正式地址再继续读取完整的信息。把端点0想象成小区物业的“服务总台”就很好理解了任何住户主机搬进来之前先要到总台报到登记基本信息读设备描述符、领取门牌号设置地址、了解小区规定读配置描述符然后才能正式入住。在入住之前所有沟通都只能通过总台进行这个总台就是端点0。4. 四种传输类型怎么选控制、批量、中断、等时4.1 各有脾气从可靠性、时延、带宽三个维度看USB协议定义了四种传输类型每种都是在可靠性、时延、带宽三个维度上做了取舍。理解这层关系你在选型时就能少走弯路。控制传输双向、可靠性最高专门用来传输配置命令和状态信息。它的特点是“请求-应答”式一次控制传输分为建立阶段、数据阶段可选和状态阶段。建立阶段主机发SETUP包数据阶段传数据状态阶段确认结束。控制传输占用带宽优先级最高但数据量通常很小基本就是枚举时和发命令用的。批量传输单向、可靠性高保证数据一定送达出错会重传但不保证时延。适合U盘、USB转串口、MSC类存储设备这类对数据完整性要求极高、对实时性不敏感的场景。批量传输在所有传输里优先级最低只能排在控制和等时后面“捡剩饭”。如果你的批量设备发现速率不稳定大概率就是总线上有其他实时传输在占用带宽。中断传输单向、可靠性高且保证轮询间隔。虽然名字叫中断但它其实是主机按固定周期轮询设备。适合鼠标、键盘、游戏手柄这类数据量小但希望延迟可控的设备。全速中断端点的轮询间隔可以设置为1~255ms高速可以按微帧来算最短125us轮询一次。注意低速设备只支持控制传输和中断传输而且低速中断端点最大包只有8字节。等时传输单向、不保证可靠性出错不重传但保证带宽和时延。适合USB摄像头、USB声卡、麦克风这类对实时性要求极高、丢一两帧无所谓的场景。等时传输没有ACK/NAK握手数据包发了就完了丢了也不补。所以你在设计和调试这类设备时一定要在应用层做容错处理。4.2 对照真实设备选型我整理了一个平时做项目时常用的对照表基本覆盖了常见设备类型设备类型传输类型为什么这么选U盘、移动硬盘批量数据量大、完整性要求极高延迟不是关键USB鼠标、键盘中断数据量小、希望快速响应轮询机制够用USB串口/CDC批量数据流可靠性优先延迟可接受USB摄像头等时/批量高带宽实时性优先掉帧可以通过上层补偿USB声卡等时音频流实时性要求极高偶尔丢一帧影响不大自定义HID设备中断双向小数据交换延迟可控有一个比较典型的误区是很多人以为“中断”传输比“批量”传输更快。实际恰恰相反在USB 2.0高速模式下批量传输的单次突发数据量远大于中断端点能达到每秒几十MB而中断传输靠的是“实时性”不是“吞吐量”。所以如果你的需求是传输大批量数据不要选中断传输它一帧微帧能传的数据有限整体带宽远不如批量。反过来说如果你需要低延迟的周期性数据上报批量传输就不合适因为它在带宽调度时可能被其他传输挤到后面延迟不可控。4.3 带宽分配帧与微帧里的时间片游戏USB 2.0在时间上把总线划分成“帧”Frame和“微帧”Microframe。全速模式是1毫秒一个帧高速模式是125微秒一个微帧一个帧等于8个微帧。每个帧/微帧开始主机都会发一个SOFStart of Frame令牌包相当于“新的一轮调度开始”。然后主机根据当前各个端点的需求在这段时间片里安排各种事务。调度策略大致是控制和等时传输优先级最高先安排中断传输其次批量传输最后只能占用剩余时间USB规范还给批量传输设定了一个限制在高速模式下批量传输不能占用超过80%的总带宽。这个限制其实是为了保证控制传输永远不会被饿死。因为如果批量设备持续满负荷传输把总线占满了控制传输就没机会执行了设备连“急刹车”的指令都收不到整个总线就失控了。理解这个调度逻辑对排查性能问题特别有帮助。比如你在同一个USB Hub上挂了一个U盘和一个USB摄像头如果U盘拷贝速度明显变慢不要急着怀疑U盘坏了先看看是不是摄像头的等时传输把微帧里的时间片占了留给批量的只剩一小块。5. 设备“报到”全过程枚举怎么把设备家底问清楚5.1 五个标准请求主机和设备靠这些“对话”USB枚举Enumeration就是设备插入后、驱动加载前主机与设备之间的一系列控制传输对话。这个对话主要是通过一组标准请求来完成的比较核心的有GET_DESCRIPTOR读取设备的各类描述符SET_ADDRESS给设备分配一个正式地址GET_CONFIGURATION / SET_CONFIGURATION查询/设置设备的配置GET_STATUS / CLEAR_FEATURE / SET_FEATURE读写设备的某些状态和特性SET_INTERFACE / GET_INTERFACE切换接口的Alternate Setting如果你在抓包工具里看到这些请求的交互过程那就是典型的枚举阶段。前几步几乎是固定的设备插入Hub检测到D/D-电平变化向主机报告“有设备插入”主机让Hub给设备复位设备进入默认状态地址为0主机向地址0端点0发GET_DESCRIPTOR(Device)但只读前8字节目的就是拿到端点0的最大包大小主机再次复位设备发SET_ADDRESS(新地址)设备从此用新地址通信主机发GET_DESCRIPTOR(Device)读完整18字节设备描述符主机发GET_DESCRIPTOR(Config)读配置描述符包括接口描述符和端点描述符主机根据识别到的类代码加载对应驱动驱动发SET_CONFIGURATION(1)设备进入配置状态开始正常工作整个过程通常在毫秒级完成肉眼是感觉不到的但抓包看每一步都非常清晰。5.2 描述符链设备的“简历”是怎么组织的描述符是USB设备“自我介绍”的数据结构主机的驱动识别设备全靠它。它的组织方式是嵌套的树形结构设备描述符Device Descriptor └── 配置描述符Configuration Descriptor └── 接口描述符Interface Descriptor └── 端点描述符Endpoint Descriptor设备描述符是整个设备的“封面页”包含USB版本号、设备类代码、厂商IDVID、产品IDPID、端点0的最大包大小、设备版本等。其中VID和PID是驱动匹配的重要依据你在设备管理器里看到的“VID_1234PID_5678”就是从这里来的。配置描述符描述一个配置的总体信息包括接口数量、供电方式自供电还是总线供电、最大电流消耗。一个设备可以有多个配置但同一时刻只能启用一个。比如一个USB设备可以有一个“高功耗高性能”配置和一个“低功耗低性能”配置根据供电情况切换。接口描述符描述一个“功能”。一个配置可以包含多个接口每个接口对应一个独立的功能。这个概念在复合设备Composite Device上特别重要比如一个USB设备既做键盘又做鼠标又做串口它就可以有三个接口每个接口对应一种功能。Windows会对每个接口枚举成一个独立的设备节点所以你会在设备管理器里看到“USB输入设备”和“COM口”同时出现。端点描述符定义端点的各项参数端点号、方向、传输类型、最大包大小、轮询间隔。一个全速设备最多可以有16个端点端点0 15个IN 15个OUT但实际上大多数设备只用到几个。在Linux下用lsusb -v可以看到完整的描述符树格式非常直观Bus 001 Device 004: ID 046d:c534 Logitech, Inc. Unifying Receiver Device Descriptor: idVendor 0x046d Logitech, Inc. idProduct 0xc534 bcdDevice 32.00 iManufacturer 1 Logitech iProduct 2 USB Receiver ... bNumConfigurations 1 Configuration Descriptor: bNumInterfaces 3 Interface Descriptor: bInterfaceClass 3 Human Interface Device bInterfaceSubClass 1 Boot Interface Subclass bInterfaceProtocol 2 Mouse Endpoint Descriptor: bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt wMaxPacketSize 0x0008 1x 8 bytes bInterval 10这里能看到鼠标的中断端点属性端点2 IN、中断传输、最大包8字节、轮询间隔10ms。字段都写得明明白白。5.3 枚举失败怎么查我踩过的三个典型坑做USB设备调试最痛苦的就是枚举失败。这里分享三个我真正踩过、也帮别人排查过的问题。第一个D/D-接反了。听着像是低级错误但实际操作中特别容易发生因为有些板子的D/D-丝印不明显或者转接板标注不统一。症状就是主机完全检测不到设备dmesg里什么都没有。排查办法很简单用示波器看D/D-有没有波形或者干脆量一下设备端的D上拉电阻是否连接到了正确的那根线。USB 2.0全速/高速设备要求在D上接1.5kΩ上拉电阻到3.3V低速设备在D-上接。如果上拉接错了线主机就识别不了设备速度。第二个端点描述符的配置和固件实际行为不一致。有一次我调一个复合设备描述了三个接口但固件里只初始化了两个。Windows在枚举时按描述符去找对应的端点和接口结果找不到直接报“设备描述符请求失败”Code 43。这个坑很隐蔽因为设备本身能响应控制请求枚举流程前一半都正常但到了配置阶段就崩溃。排查方法是把描述符导出来一字节一字节对确认bNumInterfaces和实际固件里初始化的一致。第三个供电不足导致设备反复复位。设备插入后如果瞬间电流需求超过了端口能供给的上限Hub会切断供电设备掉电重启然后又被检测到又掉电形成一个死循环。现象是设备在设备管理器里闪烁不停dmesg里能看到反复的“USB disconnect”和“USB connect”消息。解决方法是改用带外部供电的Hub或者独立供电的开发板。5.4 Linux上的USB调试三板斧既然我们写技术文章Linux下的USB调试工具还是得熟悉一下这比Windows下用Bus Hound要轻量得多。第一板斧是lsusb -v。它能列出所有USB设备以及完整的描述符。当你想确认设备到底上报了哪些描述符或者想查看设备是不是进入了异常状态用这个命令非常方便。第二板斧是dmesg。内核的USB子系统会打印非常详细的枚举过程信息包括设备速度、地址分配、配置选择、驱动绑定等。设备插入后跑一下dmesg | tail -20基本能看到问题出在哪个环节。第三板斧是usbmon Wireshark。加载usbmon内核模块后Wireshark可以直接抓USB总线上的所有包。抓包能让你看到每一个控制请求、每一次批量传输的握手情况。之前调一个USB转串口不定时丢数据的问题就是靠这个抓包发现设备在数据压力大时频繁回NAK从而定位到是固件里FIFO太小导致缓冲区溢出而不是串口芯片的硬件问题。用usbmon抓包的简要步骤sudo modprobe usbmon sudo wireshark在Wireshark的接口列表里选择usbmon1对应USB bus 1就能开始抓了。抓包时如果想缩小范围可以在过滤器里写usb.addr 3.2只看地址3设备端点2相关的流量非常精准。这里要给一个实际建议如果产品在Windows和Linux下表现不一致优先抓包对比。很多时候是Windows驱动和Linux驱动对描述符某些字段的解释不同导致的抓包一看就能看出来。6. 写在最后的调试心得这篇讲了架构、物理层编码、包结构、端点模型、四种传输类型和枚举流程其实核心逻辑串起来就一句话USB是一个严格的主从式共享总线所有通信都由主机发起通信的基本单元是事务事务由包组成端点是通信的窗口管道是通信的通道四种传输类型的取舍决定了设备的实时性和可靠性。把这个思维框架建好后面不管是看协议文档还是调Bug你都有了一个定位坐标系——先分清当前是枚举阶段还是数据传输阶段再从“主机/设备/线缆/Hub”四个环节去排查很多问题瞬间就清晰了。我个人的经验是调USB问题最忌讳的就是“瞎试”。这个板子换一下、那个参数改一下不如老老实实抓一次包。因为USB协议虽然复杂但它是完全确定的——总线上每一个包都有明确的目的地和含义问题一定能从抓包里找到蛛丝马迹。所以下次遇到USB设备不工作别慌先抓包再说别的。下一篇我们聊USB的class类是怎么定义的尤其是HID和CDC这两个最常用的类它们的协议细节和实际项目里的配置方法。到时候见。
返回列表