讲USB协议的文章不少,但真正能让人看明白的确实不多。这一篇是USB硬核系列的中篇,上篇我们捋了USB从1.0到4.0的演进节奏,也把物理层的电平、编解码逻辑翻了个底朝天。这篇我们从整个协议栈的骨架讲起,重点落在设备侧最核心的“架构”和“端点通信”上,顺着数据是怎么从一个外设的一端,跑到主机驱动那头的,一路拆到底。
如果你是做嵌入式USB设备开发的,或者正在被设备枚举失败折腾,这篇文章基本能帮你建立一个完整的排查坐标系。看完你会知道:端点0为什么是所有设备的“生命线”,四类传输到底什么时候选哪个,枚举过程中主机到底对设备做了什么,以及为什么有时候明明接线正确,设备就是不能被识别。
我尽量不堆术语,但该出现的专业名词一个都不会少。毕竟USB协议这东西,躲不开,那就把它讲清楚。
1. USB整体架构:从一根线到一个“联邦制王国”
1.1 拓扑结构:主机永远是“国王”
USB和以太网、串口那种点对点或者网状连接都不一样,它是一个非常严格的主从式星型拓扑。整条总线上只能有一个主机(Host),所有设备都挂在以主机为中心延展出去的树上。这个“树”不是单纯地把几个设备并联在两根线上,而是靠**集线器(Hub)**一级一级把端口扩展开来。
整个USB拓扑里有三层概念:根集线器(Root Hub)、集线器(Hub)、功能设备(Function Device,也就是手机、键盘、U盘、摄像头这些实际干活的)。哪怕你插在主板上那个USB口,也得先经过CPU和芯片组内部的USB主机控制器,再走到根集线器,然后从那个物理口出去。主机控制器负责协议层的所有调度,根集线器负责物理连接和端口状态的监控。
一个USB系统最多能挂多少个设备呢?地址空间是7位,理论上可以寻址128个设备,但地址0是默认未配置地址,不能分配给普通设备,所以实际上最多127个。而且层级也有限制:包括根集线器在内,最多只能7层,意思是中间串Hub不能无限制地串下去。设计这个限制的原因很直接:每一层Hub都会带来转发延迟,层数太多,时序预算就不够了,尤其是低速设备对时序极其敏感。
1.2 分层协议栈:从信号到功能的分工
和网络协议一样,USB也把整个系统分成了好几层。上篇我们看的D+、D-电平信号,那是物理层的事儿;往上走,每一层都只管自己那部分活。我把USB2.0的核心分层画成一张表,你可以先存着,后面的内容全都是在给这张表填细节:
| 层级 | 负责内容 | 对应概念 |
|---|---|---|
| 功能层 | 设备类的具体行为,比如HID键盘上报键值、U盘执行SCSI命令 | 接口(Interface)、设备类(Class) |
| 设备层 | 设备的逻辑组成、地址管理、配置状态 | 设备(Device)、配置(Configuration)、端点(Endpoint) |
| 协议层 | 包、事务、传输的组装和解析 | 令牌包、数据包、握手包 |
| 传输层 | 在帧/微帧内做带宽调度,把数据按时序搬移 | SOF、帧、微帧 |
| 物理层 | 电压、差分信号、编解码 | NRZI编码、位填充、上拉电阻 |
这个分层最大的好处是:上层不需要关心电信号怎么编解码,下层也不需要知道数据到底代表什么含义。你要写一个USB HID键盘的固件,只需要在设备层把描述符写好,把端点配置对,至于主机端的驱动程序怎么跟你的设备对话,那是USB协议早就定好的标准流程。
1.3 主机控制器:整个总线的“总调度员”
设备侧大家平时接触得多,U盘、键盘都是现成的,但主机侧很多人反而陌生。主机控制器(Host Controller)才是USB系统里最忙的角色,它不仅负责产生所有事务,还掌握着时间分片。你插入一个设备时,主机控制器要负责复位、枚举、分配地址;平时传输数据时,它负责周期性向各种端点发起访问。
USB1.x时代常见的是OHCI和UHCI,USB2.0时代主流的叫EHCI,到了USB3.0之后主流是xHCI。xHCI厉害的地方在于它能同时兼容USB2.0和USB3.0的设备,而且把以往复杂的端口调度逻辑用软件队列(TRB Ring)来管理,这对开发者其实是降了一大门槛。你写设备侧固件,不需要关心主机控制器具体是哪种,但调试时如果遇到莫名其妙的问题,看一眼主控型号还是能提供很多线索的。
提示:做USB产品测试时,建议至少在两三种不同主控的电脑上交叉验证(Intel、AMD、第三方晶片),因为不同主机控制器对某些不规范描述符的容忍度差别很大。
2. 端点和管道:USB通信的最小逻辑单元
2.1 端点到底是什么
“端点”这个词在USB里有个很形象的物理隐喻:总线上的数据最终要到达设备内部某个“终点”,这个终点就是Endpoint。它不是一个抽象的软件对象,而是设备里一个实实在在的逻辑单元,有自己的编号、方向、传输类型和最大包长度。
每个USB设备拥有的端点资源是有限的。USB2.0规定,除端点0外,一个设备最多可以有15个IN端点和15个OUT端点(有些控制器会限制得更小)。端点号用4位表示,从1到15;方向只有IN和OUT两种,而且这方向是拿主机做参考的:IN表示设备向主机方向传数据,OUT表示主机向设备方向发数据。这里经常有人搞反,记住一点就行:站在主机的角度,“IN进来”、“OUT出去”。
端点还有一个关键特性:除了默认的控制端点0是双向的,其他所有端点在配置好之后,方向都是固定的。你不能一会儿把端点1当IN用,一会儿又当OUT用。这在设计固件的时候要想清楚:比如一个串口转USB芯片,它的OUT端点和IN端点往往是成对出现的,接收主机数据用一个OUT端点,向主机发数据用一个IN端点。
2.2 管道:端点和主机之间的“虚拟连接”
光有端点还不行,端点和主机软件之间的连接通道,USB里叫管道(Pipe)。一个管道对应一个端点,方向和类型都跟端点绑定。管道这个概念有点抽象,但你可以把它想象成一根水管:设备端是水龙头(端点),主机端是用水的地方(软件),管子(管道)里的水就是数据。
管道在主机侧实际体现为URB(USB Request Block),这是Linux内核里USB子系统的关键数据结构,Windows里对应的是USB Request。你写USB驱动的时候,本质上就是在构造URB、提交URB、处理URB完成回调。每次URB提交,主机控制器就会按照URB里描述的端点、方向、数据长度,去安排一次或多次总线事务。
USB规范把管道分成两类:消息管道(Message Pipe)和流管道(Stream Pipe)。消息管道只用于控制传输,数据总带有USB协议定义的“含义”,比如GET_DESCRIPTOR、SET_CONFIGURATION这些请求;流管道用于中断、批量、等时传输,数据本身就是一个字节流,主机和设备端各自知道自己怎么解释。这个区分很重要,因为控制传输不能简单当成普通数据收发来对待,它有严格的请求-响应结构。
2.3 端点0:一台设备的“客服电话”
在所有端点里,端点0的地位是独一无二的。它不需要你额外配置,设备一上电复位之后,端点0就天然存在。它同时支持IN和OUT两个方向,而且只能用于控制传输。在设备没有被分配地址、没有被配置之前,主机唯一能访问的端点就是端点0。所以端点0就是设备的“客服电话”,任何正规使用USB的设备,这套电话必须在设备上电后就能接通。
主机在枚举期间跟设备的所有对话,比如问“你是谁”“你支持什么配置”,全部通过端点0进行。端点0有一个关键参数叫bMaxPacketSize0,即端点0的最大包长度。低速设备固定是8字节,全速设备可以是8、16、32或64字节,高速设备固定是64字节。你在设备描述符里写的这个值不能乱填,必须和硬件实际支持的一致,否则枚举会直接失败或者后续控制传输出错。
我调试过不少设备,很多奇怪的问题就出在这个bMaxPacketSize0上。有的芯片默认配置是64字节,但固件描述符里写成32,结果主机发64字节的数据包时设备根本收不下,控制传输直接挂掉。最后用协议分析仪一看,主机一直在重发,设备一直不回复,画面相当尴尬。
注意:修改端点0最大包长度后,必须重新检查所有控制传输的数据阶段拆分逻辑。主机发送的SETUP包固定是8字节,但数据阶段的数据包长度取决于端点0的bMaxPacketSize0,不是简单按8字节拆的。
3. 四种传输方式:USB的“四种性格”
3.1 控制传输:规矩最多的那个
控制传输是USB里的“总管”,它的核心特点是有两个阶段严格遵循请求-响应结构。它主要用于枚举、设备配置、发送类请求这些对可靠性要求极高的场景。控制传输的最大特点是:既经常又复杂,但带宽占用必须让你觉得它很“克制”。规范规定,全速总线上一帧内控制传输最多只能占用10%的带宽(约1.2MB/s),剩下的要留给其他传输。
控制传输分为三个阶段:设置阶段(Setup)、数据阶段(Data)、状态阶段(Status)。设置阶段固定发送一个8字节的Setup包,告诉设备“我这个请求是要干什么”;数据阶段按需存在,可能没有数据(比如SET_ADDRESS),也可能有IN方向或OUT方向的数据(比如GET_DESCRIPTOR就是IN方向);状态阶段用来报告整个请求的执行结果,设备返回ACK表示成功,返回STALL表示不支持。
这个结构使得控制传输相比另外三种类型容量不高,但“确定性”很强。主机获取设备描述符、配置描述符,都靠它。后面我会专门拿一个SET_ADDRESS请求把整个过程拆开看,这里先有个概念。
3.2 中断传输:坚持“准时”,但不一定“大量”
“中断”这个名字很容易让人误会,以为像硬件中断一样,设备随时能主动通知主机。其实USB总线权力完全在主机手里,设备根本不能主动发起通信。中断传输的本质是主机按照固定周期去轮询端点,所以更准确的理解是“周期性查询传输”。
中断传输适合传输数据量不大、但对延迟有明确要求的场景,典型就是鼠标键盘(HID)、游戏手柄。全速设备的中断IN端点,主机可能每隔1ms到255ms来轮询一次;低速设备则只能支持10ms到255ms的间隔。每次轮询最多能拿多少数据呢?低速设备一个事务最多8字节,全速设备最多64字节,高速设备最多1024字节。
中断传输有可靠性保证,如果数据传输出错,链路层会重试。这跟等一下要讲的等时传输很不一样。我之前遇到过一个问题:某个HID设备把轮询间隔配置成0,这是不合法的(规范里bInterval=0是保留值),结果在某个主机上收不到数据。所以量产产品设计时,bInterval这个参数一定要仔细斟酌,太短浪费总线带宽,太长影响用户体验。
3.3 批量传输:能“迟到”,但绝不能“出错”
批量传输是数据搬运工,专为大块数据传输设计,从几十字节到几百千字节都行。它不保证延迟,但保证可靠性——出错了会重传,直到数据正确到达。代价就是:它的优先级在等时和中断后面,总线空闲的时候它才能抢到时间片。
批量传输的典型应用是U盘、读卡器、USB转串口。一个全速批量端点一次事务最多传输64字节,高速批量端点一次最多512字节。你拷贝一个大文件时,总线上的画面就是海量的批量事务在排队,每个事务包含IN或OUT令牌、数据包、握手包。批量传输没有固定的带宽预留,所以总线上如果有等时或者中断传输占着时间段,批量传输的吞吐量就会明显下降。
批量端点的缓冲区设计也非常有意思。在设备侧,通常用FIFO来解耦总线速率和应用速率。如果FIFO太小,主机来读数据时发现空,端点会返回NAK,主机收到NAK不会报错,下次继续读。很多刚做USB设备的同学会发现总线上一堆NAK,那其实不一定是错误,而是设备还没准备好。真正需要关心的是NAK比例太高导致吞吐量上不去。
3.4 等时传输:最“原始”也最“直接”的实时通道
等时传输是四种传输类型里唯一“不保证交付”的。它专门为音视频、传感器采集这一类对时间敏感、对数据完整性要求相对宽松的场景设计。错过了就错过了,主机和设备都不重试。这换来的是时间维度上的确定性:每个帧/微帧里,主机都保证为等时端点预留带宽。
全速等时端点一次最多1024字节,高速等时端点一次最多1024字节,但由于每微帧可以做3个事务,实际每个微帧最多3072字节。等时传输的包结构里没有握手包——因为不需要设备确认收到没收到。这也意味着,如果数据在传输中出错,接收方只能直接丢弃那个包。视频摄像头用等时传输传图像数据,偶尔丢一帧用户基本无感;但如果用批量传输,重传会导致卡顿和延迟累积,反而体验更差。
选择等时传输前,一定要评估:你的数据能不能容忍偶尔丢失?如果答案是“绝对不能丢”,那就用批量,别贪图等时的低延迟。设备侧实现等时端点时,也一定要考虑FIFO溢出/下溢处理,因为主机的调度节奏稍微一变,音频就可能出现爆音或者暂停。
3.5 一图看懂四种传输的选型逻辑
四类搞定,放个总结对照,开发时拿不准就翻这个表:
| 传输类型 | 周期性 | 可靠性 | 典型应用 | 全速最大包 | 高速最大包 |
|---|---|---|---|---|---|
| 控制 | 不定期 | 高(重试) | 枚举、配置、命令 | 64字节 | 64字节 |
| 中断 | 固定周期 | 高(重试) | HID、游戏手柄 | 64字节 | 1024字节 |
| 批量 | 无保证 | 高(重试) | U盘、串口、大数据 | 64字节 | 512字节 |
| 等时 | 固定周期 | 低(无重试) | 音频、摄像头 | 1023字节 | 1024字节/微帧 |
从这个表能看出来,USB的四种传输类型不是随便瞎设计的,它们是用来匹配不同应用场景里“延迟、带宽、可靠性”三个维度上的不同偏好的。你设计一个USB设备,第一步工作就是想清楚每个端点用哪种传输,这个选错,后面做多少都是白费。
4. 枚举过程:USB设备如何从“陌生人”变成“正式员工”
4.1 设备上电后的状态机
主机和设备的每一次交互,本质上都围绕设备的状态机在进行。USB2.0规范里设备有六大状态:Attached、Powered、Default、Address、Configured、Suspended。你插上U盘那一刻,设备先进入Attached,然后上电进入Powered,接着主机开始下面的流程。
理解这套状态机特别重要,因为很多调试就是看设备“卡在哪个状态了”。要是设备在Default状态迟迟进不了Address,那说明枚举请求根本没走通;如果进入了Configured但功能不正常,那问题可能出在描述符内容本身或者端点配置上。排查这类问题时,我习惯先把固件里状态机的日志加全,每次状态切换都打一条,这样能快速缩小范围。
4.2 枚举流程的完整拆解
规范的枚举流程大致是下面这几步,每步都有具体的总线交互:
- 设备连接检测:设备插入,设备端的1.5kΩ上拉电阻把D+(全速/高速)或D-(低速)拉高,根集线器/集线器检测到电平变化,向主机报告端口状态事件。
- 复位(Bus Reset):主机向端口下发复位信号,D+和D-都被拉低至少10ms。设备检测到复位后就清楚了:我需要在默认地址0等待命令。
- 获取设备描述符前8字节:主机向地址0发送GET_DESCRIPTOR(Device)请求,wLength设为8,目标是从设备描述符里读出第8个字节bMaxPacketSize0,知道端点0能收多大包。这一步完成后,主机通常会再发一次复位。
- 设置地址(SET_ADDRESS):主机发送SET_ADDRESS请求,携带一个1~127之间的地址(比如0x0B)。设备收到后必须在这个请求的状态阶段完成后,才真正切换到新地址。
- 获取完整设备描述符:主机用新地址再次发送GET_DESCRIPTOR(Device),这次wLength=18,拿到全部设备描述符,解析出VID、PID、bcdUSB版本等关键信息。
- 获取配置描述符:主机发送GET_DESCRIPTOR(Configuration)请求。这个有点绕:第一次可能只拿9字节配置头,从配置头里读出总长度,然后主机再按总长度拉取完整的配置描述符集合(配置描述符+接口描述符+端点描述符)。
- 选择配置(SET_CONFIGURATION):主机根据描述符信息、驱动匹配情况,向设备发送SET_CONFIGURATION请求,设备进入Configured状态,之前配置好的非0端点开始正式工作。
里面有几个容易忽略的细节:第一步设备连接检测,全速和高速设备的区分是靠上拉电阻在D+,低速设备上拉在D-,而USB3.0的SuperSpeed连接检测逻辑完全不同;第二步复位期间设备不能响应任何事务;SET_ADDRESS这个请求在状态阶段使用的是端点0,但状态阶段完成之后才切换地址,主机和设备的时序如果把握不好,就会出现第一次用新地址访问设备时设备还没切过去的情况。这也是很多自定义USB控制器固件翻车的地方。
4.3 描述符体系:设备的“身份证”和“简历”
整个枚举过程里,主机反复在向设备要描述符。描述符本质是一组定义好的结构化数据,USB协议用它们来描述设备的身份和能力。这套体系从上到下是设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符,一层套一层,像简历结构一样。
设备描述符是根节点,包含USB版本号、设备类、VID、PID以及端点0最大包长度等关键信息;一个设备可以有多个配置描述符,每个配置描述符描述了整套电源消耗和端点组合;每个配置下面可以有一个或多个接口描述符,接口在逻辑上代表一个功能(比如一个复合设备有HID接口和CDC接口);接口下面有端点描述符,描述端点的方向、类型、最大包大小、轮询间隔。
主机加载驱动时,主要就是靠VID、PID和接口描述符里的bInterfaceClass、bInterfaceSubClass、bInterfaceProtocol来匹配。另外说一句,字符串描述符是可选的,但强烈建议做,尤其是给产品做个序列号什么的,对生产、维修、软件授权都很有帮助。
经验:写描述符时,把所有字节都列出来人工核对一遍,或者用现成的描述符生成工具,比盲写靠谱得多。我见过有人把配置描述符的总长度字段算错,导致主机拉取描述符时数据不完整,最终枚举失败。
4.4 设备地址和配置的“记忆陷阱”
一个常见的坑是:某些设备在收到SET_ADDRESS后,配置数据还在闪存里慢慢加载,还没来得及准备好应答新的描述符请求,结果主机已经在新地址上发出GET_DESCRIPTOR了,设备因为“太慢”直接不响应。USB规范建议设备在复位后尽早准备好描述符数据,而不是等到收到请求才去读闪存。这里有一个设计上的取舍:如果描述符数据很小,尽量放到RAM的静态区,甚至硬编码,避免从外部存储读取的延迟。
还有一个记忆点:设备收到SET_CONFIGURATION后才算“正式启用”非0端点。在配置之前,即使你已经在描述符里描述了端点1、端点2这些,设备收到对端点1的非控制请求也应该直接STALL或者忽略。有些固件在未配置状态下错误地响应了端点数据,会导致主机协议状态错乱。
5. 端点通信实战:把一条控制传输拆到字节级
5.1 控制传输的三阶段结构
理论讲了这么多,是时候走一遍真实的事务流了。控制传输是所有传输类型的“模板”,也最适合用来理解USB如何在物理线上交换数据。以主机要给设备设置新地址为例,一次完整的控制传输长这样:
- 设置阶段:主机发OUT令牌(地址0端点0),再发DATA0包,里面是8字节的Setup包;设备收到后,如果接收正常,回复ACK握手。
- 数据阶段:SET_ADDRESS这个请求的数据长度是0,所以数据阶段直接跳过,没有数据包。
- 状态阶段:这次数据方向变成IN,也就是设备向主机方向返回状态。因为没有任何数据要返回,所以主机发IN令牌,设备收到后回复一个空的数据包(ZLP,零长度包)作为状态阶段的DATA1包;主机收到后回复ACK握手,表示“我看到了,你完成得不错”。
设备收到主机的ACK之后,才能把内部地址寄存器切到新地址。地址切换的时机如果过早,设备会在状态阶段用新地址应答,那就全乱了。
5.2 Setup包:8字节里的大学问
那8字节的Setup包是整个控制传输的“指令核心”,5个字段各司其职:
| 字节偏移 | 字段名 | 作用 |
|---|---|---|
| 0 | bmRequestType | 位7表示方向(0=OUT,1=IN);位6~5表示类型(0标准,1类,2厂商,3保留);位4~0表示接收者(0设备,1接口,2端点,3其他) |
| 1 | bRequest | 请求码,比如GET_DESCRIPTOR=0x06,SET_ADDRESS=0x05,SET_CONFIGURATION=0x09 |
| 2~3 | wValue | 请求相关的主参数,比如GET_DESCRIPTOR时高字节表示描述符类型,低字节表示描述符索引 |
| 4~5 | wIndex | 请求相关的索引参数,比如接口号或端点号 |
| 6~7 | wLength | 数据阶段期望传输的字节数,0表示没有数据阶段 |
以GET_DESCRIPTOR(Device)为例,标准请求发送的Setup包是:bmRequestType=0x80(IN方向,标准请求,接收者是设备),bRequest=0x06,wValue=0x0100(描述符类型1,索引0),wIndex=0x0000,wLength=0x0012(18字节,也可能是0x0008)。
这里有个细节:第一次获取设备描述符时,主机是故意只取8字节的,因为此时它还不知道bMaxPacketSize0是多大。如果主机自大一点,一上来就要求18字节,而设备端点0最大包只有8字节,主机根本不知道后面怎么拆分数据包。所以USB规范规定,首次GET_DESCRIPTOR请求设备描述符时wLength必须为8,这不是约定俗成,而是必须如此,否则主机可能无法解析。
5.3 数据Toggle:防止丢包和重复包的最后防线
在USB链路层,数据包有一个序号位叫数据切换(Data Toggle),取值只有DATA0和DATA1两种。它的核心作用是保证接收方能识别出当前数据包是新的一包还是重传的一包。控制、批量、中断传输都会用到它,等时传输因为不重传所以不用。
规则很简单:每个端点(除了等时)都有一个独立的Toggle位,初始化时通常是DATA0。一个事务发送数据包时带上当前Toggle值,握手成功后,发送方和接收方一起翻转Toggle;如果事务失败重传,则Toggle保持不动,继续用原来的值。这样接收方发现“咦,还是上一个Toggle值”,就知道这是重传,即使前面那个包其实已经收到了,也能避免重复处理。
在控制传输里,Toggle的使用还略微特殊:设置阶段固定用DATA0,数据阶段第一个包用DATA1,状态阶段用DATA1。这套固定的序列保证了控制传输各阶段能被准确区分。很多自制USB设备枚举失败,就是因为固件里Toggle翻转逻辑写错了,主机发来一个DATA0,它回了ACK但没翻转Toggle,下一包主机发DATA1,设备还以为是在重传,直接不认了。
注意:不要自己实现USB协议的时候想着“我简化一下,不搞Toggle”。省略Toggle会导致重传场景下数据错乱,尤其是在USB2.0高速模式下,NAK、NYET、重传的频率远超你的想象。这个机制是整个链路可靠性的基石。
5.4 握手包:ACK、NAK、STALL、NYET到底在说什么
USB2.0的握手包一共有四种,每一种都是通信双方在“讨价还价”:
- ACK:接收方明确告诉对方“这个事务的数据我收好了,处理正常”。
- NAK:端点说“我现在还没准备好”,通常表示设备忙,但功能本身是正常的。主机收到NAK后会择机重试,不会报错。
- STALL:端点说“这个请求我没法处理/端点被halted了”,这是明确的错误状态。主机收到STALL后通常会中止当前URB,并向驱动程序汇报错误。
- NYET:只在高速模式下的OUT传输里出现,设备表示“这一包我收下了,但下一个事务最好等一下”。
反应到调试上:总线分析仪里如果全是NAK,多半是设备端FIFO没及时填充数据,或者软件处理速度跟不上;如果出现STALL,就要去查端点是不是被固件halt了,或者主机发了一个设备不支持的请求。有时候HID设备对标准请求实现了错误的值,返回STALL,那问题就出在描述符或者请求处理器上。
我遇到过最典型的一次:某设备的厂商自定义请求在固件里忘了实现,主机驱动一提交请求就收到STALL,驱动直接报错。这类问题排查起来很折磨人,因为它不是每次必现,而是跟驱动初始化顺序有关。后来把固件里所有不支持的请求都默认返回STALL而不是什么都不回,问题立刻消失了——因为什么都不回会让主机一直等到超时,而明确STALL至少能告诉主机“这条路走不通”。
6. 常见问题与调试心得:USB开发踩过的坑
6.1 枚举失败:先分清是“没电”还是“没信号”
枚举失败是USB开发里最常见的问题,没有之一。设备插上去一点反应都没有,我建议按下面这个顺序排查:
- 量一下设备端VBUS是否有5V,用电流计看看设备有没有过流或者短路。
- 用示波器/逻辑分析仪抓D+或D-,看设备连接后有没有被上拉到3.3V以上。如果一直是0,说明上拉电阻没接对,或者芯片的上拉没使能。
- 确认复位信号:主机发复位时D+和D-都会被拉低至少10ms,如果主机检测不到设备,根本不会进入复位阶段。
- 如果总线电平均正常但枚举还是失败,那大概率是描述符或者端点0逻辑有问题。用协议分析仪抓一遍,就能看到主机和设备的交互停在哪个请求上。
还有一个容易被忽略的问题:有些设备上电瞬间电流很大,如果你的供电电路压降严重,设备刚上电又掉电,主机看到的就是一个反复插拔的现象。这种情况优先解决供电,而不是协议。
6.2 高速设备为何总是“跌倒”在全速模式
USB2.0高速设备在上电后并不是直接以480Mbps跑的,它要先经过一个高速检测握手过程(Chirp握手)。设备上拉D+后,主机如果支持高速,会在复位期间发出一连串的Chirp K信号,设备检测到后用Chirp K/J序列回应,双方确认后才切换到高速模式;如果这握手失败,设备就会以全速模式工作,虽然功能可能看起来正常,但性能大打折扣。
常见的握手失败原因:电路板走线太差导致信号质量不过关、设备端高速终端电阻(18Ω)没接对、复位移位时序不对、甚至某些劣质USB线缆的高频特性太差。调试这种问题,用示波器抓Chirp波形最直接,高分辨率下看两个Chirp K脉冲的时间间隔,基本能定位是哪一侧出了问题。
经验:如果你的产品设计目标就是USB2.0高速,原理图阶段就得把D+/D-的差分阻抗做对(90Ω±10%),而且尽量在芯片引脚旁边放ESD保护器件,但要注意寄生电容别太大。很多高速握手失败不是逻辑错误,而是PCB布局和物料问题。
6.3 带宽不够用:别让等时吃干抹净
当多个设备同时工作,你会发现总线的实际吞吐量和理论值差得远。这时候要查两件事:一是有没有低速/全速设备拖慢了整个Hub的调度时间片,二是有没有等时端点占用了预留带宽。
USB规范规定,每个帧/微帧里,中断和等时传输最多占用90%的带宽,剩下的10%必须保留给控制传输,批量传输只能用这之外的“缝缝”时间。如果你的总线上一堆中断端点都在1ms轮询,即使每个包只有8字节,大量设备累积起来的开销也会明显挤占批量传输的空间。解决思路就是尽量用高速Hub隔离低速设备,让低速设备的流量只影响它所在的下游端口。
在设备固件侧,也可以优化端点的FIFO深度和中断触发阈值,减少每个事务的因“没准备好”产生的NAK次数,提升有效吞吐。我曾经把一个CDC串口设备的批量传输吞吐从不到2MB/s优化到接近10MB/s,靠的就是把FIFO阈值调大、减少软件拷贝次数、以及把端点事务做成双缓冲。
6.4 调试工具:USB分析仪和协议日志的取舍
做USB开发,工具很重要,但不一定越贵越好。业余调试完全可以先用软件方案:在Linux下打开usbmon内核模块,用Wireshark抓USB协议包,看枚举、控制传输、URB完成情况,成本为零,能解决80%的问题。缺点是软件抓包拿不到物理层信息,比如信号质量、复位时序。
如果物理层有问题,就需要USB协议分析仪了。市面上的分析仪从入门到专业跨度很大,个人开发者买到手的通常是逻辑分析仪加协议解码,或者Total Phase Beagle系列这种专用分析仪。用分析仪的好处是能看到从包级到事务级的完整时序,包括重传、NAK、带宽占用时间戳,这对定位Toggle错误、等时带宽计算这类问题几乎是唯一的手段。
我个人调试习惯是这样:遇到“看起来是逻辑问题”的先用Wireshark抓包,快速确认请求和响应是否正常;遇到“设备完全无响应”的就先用示波器看电平,排除物理层;只有当问题逻辑和物理都有可能的时候,才把协议分析仪接上。分析仪最大的价值不是日常调试,而是产品投产前的合规性验证,比如跑一遍USB-IF的USBCV测试,把描述符、命令处理的合规问题都扫一遍。
6.5 硬件和固件协同设计的四条建议
回到执行层面,USB开发如果能在硬件和固件设计阶段就未雨绸缪,能省掉后面大量调试时间。这几条都是我踩坑踩出来的经验,写在这里:
- 给每个端点把FIFO配足。特别是批量IN端点,FIFO太小会导致吞吐量上不去;等时OUT/IN端点,FIFO太小会导致音频或视频流抖动。
- 固件里把所有标准请求都实现完整。不要觉得自己只用厂商请求,就把标准请求的响应写得草率。USBCV测试会把标准请求挨个打一遍,少一个字段都过不了。
- 尽量让描述符可配置化。在Flash里保存一套描述符参数,调试时用串口打印出来核对,比每次改代码烧录快得多。
- 尽早测试高速握手。如果芯片支持高速,不要等到产品快做完了才测试高速信号。第一次打样就该把高速测试做了,因为PCB布线问题越早发现越省钱。
这些建议没有一条是“高大上”的黑科技,但它们都是实打实能从调试时间里省出成本的细节。USB协议本身不复杂,复杂的是它牵扯到的时序、信号完整性、驱动匹配、带宽调度这些跨领域问题。你处理得越多,越会觉得它像一门手艺活,得一点一点磨。
如果让我说一个最值得投资的习惯,那就是把USB协议分析仪和串口日志协同起来用。分析仪记录的总线行为是客观真相,串口日志反映的是固件内部状态,两个一对照,大部分疑难杂症都能快速定位。你还真别嫌这工作麻烦,USB开发调通了之后,那前后的差距感,跟看着一个死透的设备突然被系统识别出来、驱动装好、功能跑起来,是完全不一样的体验。