上篇把USB的诞生背景、版本迭代讲了一遍,这次接着往下走。如果说上篇是“纵览历史”,这篇就是把USB扒开看内里:协议架构是怎么搭起来的、端点和管道到底是什么、四种传输类型分别适合干什么,以及最关键的——真到了调板子、写固件、抓包排障的时候,这些知识怎么落地。
我之所以一直强调要搞懂协议而不只是“会用”,是因为USB这玩意儿跟UART、SPI完全不一样。SPI从设备被动响应,UART两边对着发,都是在“已经建立好的连接”上干活。USB则复杂得多,设备插上去之后要枚举、要分配地址、要选配置、要搞清传输类型,任何一个环节断了,结果就是设备管理器里那行刺眼的“未知USB设备(设备描述符请求失败)”。这块内容涉及的知识点非常密集,但好消息是它是纯逻辑的,理解了架构之后,剩下的都能推导出来。
1. USB协议架构:这套“主从世界”到底是怎么运转的
1.1 为什么USB一定要有个“主机”
先问个问题:U盘插到电脑上,是U盘主动告诉电脑“我来了”吗?答案是否定的。USB是严格的主从架构,所有通信都由主机(Host)发起,设备(Device)永远只能被动响应。主机说“我要数据”,设备才把数据拿出来;主机不说,设备哪怕有再多话也只能憋着。
这个设计和I2C有点像,但比I2C更彻底。I2C至少还有多主机模式,USB连这个口子都不留。主机控制着一切:总线上的所有传输事务(Transaction)都由主机调度,设备连“主动发个中断”的资格都没有。那鼠标动了、键盘按了,系统怎么知道?答案是主机周期性地轮询设备,问“你有没有新数据”,设备回答“有”或者“没有”。
理解这个模型是理解后面所有内容的地基。实际调试中,很多人遇到“设备收不到数据”或“上位机读不到设备发来的数据”时,第一反应是查设备端代码,其实很多时候问题出在主机根本就没发起轮询,或者设备在枚举阶段就没通过,主机压根不知道这个设备该怎么通信。
USB总线上当然还有一类角色叫Hub(集线器),它的作用是把一个主机口扩展成多个。但Hub本质上也是个“特殊的设备”,它同样受主机控制,不具备任何主动调度能力。所以整个USB拓扑就是一棵以主机为根、向下一层层展开的树,这一点和以太网那种对等网络有本质区别。
1.2 物理层细节:D+/D-、上下拉电阻与速度识别
USB用一对差分信号线D+和D-传输数据,配合VBUS供电和GND地线,一共四根线(USB 2.0时代)。差分的好处是抗共模干扰强,这也是USB线能做到几米传输的底气所在。但有意思的是,USB的速度识别并不靠“看信号波形”,而是靠一根电阻。
设备接入主机后,主机通过D+或D-上的电平状态来判断设备是什么速度:
- 全速(12Mbps)和高速(480Mbps)设备:在D+上接一个1.5kΩ上拉电阻
- 低速(1.5Mbps)设备:在D-上接一个1.5kΩ上拉电阻
- 主机端则在D+和D-上分别有15kΩ的下拉电阻
设备没插入时,主机两条线都被拉低;设备一插入,对应线上的1.5kΩ上拉和主机15kΩ下拉分压,线被拉高,主机就知道“有设备来了,是全速/低速”。高速设备的识别更特殊:它先以全速身份被识别,然后主机和设备通过一套chirp握手流程,确认双方都支持高速,再切到480Mbps。
网上常看到有人问“D+ D-上要不要加电容、加多大”,这其实是个误区。D+和D-上通常不会靠并联电容来识别速度,关键是上拉电阻和走线阻抗。D+、D-需要做差分走线,阻抗控制在90Ω左右,走线要等长、远离干扰源。如果为了滤除EMI串联一个小电容(pF级),不是不行,但加不好会直接毁了信号完整性,造成枚举不稳定。我见过一块板子,D+线上加了个100nF电容,结果高速设备完全识别不了,低速全速勉强能跑,问题就出在这。
1.3 传输的最小单元:包、事务与传输的三级结构
USB协议有三层递进的概念:包(Packet)、事务(Transaction)、传输(Transfer)。很多人被搞晕就是没捋清这三层。
包是总线上可见的最小数据单元,类似网络里的帧。一个包由SYNC同步字段、SOP包起始、PID包标识符、地址/帧号/数据等负载字段、CRC校验和EOP包结束组成。PID有好几大类:
- 令牌包:SETUP、IN、OUT、SOF,由主机发出,相当于“命令”
- 数据包:DATA0、DATA1,真正搬运数据
- 握手包:ACK、NAK、STALL,接收方给发送方的回应
事务是通信的基本动作,通常由“令牌包+数据包+握手包”三段组成。主机发一个IN令牌,设备回一个DATA包,主机再回一个ACK,这就完成了一个IN事务。控制传输、批量传输、中断传输、等时传输,本质上就是不同规则组合出来的事务序列。
传输则是更高层的概念,比如控制传输里的一次“读设备描述符”操作,可能包含多个事务。层与层之间的关系搞清楚了,抓包时就不会被一堆PID刷花眼——你先看令牌,判断主机在让谁干什么,再看数据和响应,整个通信过程就流畅了。
2. 端点通信拆解:管道模型与四种传输类型
2.1 端点的本质:设备内部的小信箱
端点(Endpoint)是设备内部的一个数据缓冲实体,也是主机与设备通信的唯一入口。你可以把端点想象成设备墙上开的一排小信箱,每个信箱有编号、有方向(IN还是OUT)、有传输类型。主机往OUT端点里塞数据,或者从IN端点里取数据。
这里的方向要特别强调:IN和OUT是站在主机的视角说的。IN端点是设备往主机方向发数据,OUT端点是主机往设备方向写数据。很多第一次写USB固件的人在这里栽跟头,觉得“我要把数据发出去,所以用OUT端点”,结果完全搞反。
USB 2.0协议里,一个设备最多可以有16个端点(编号0到15),每个端点都有方向属性。不过实际应用中,一个端点号通常成对出现:比如EP1 IN和EP1 OUT是两个不同的端点,但因为编号相同,常被大家合称“端点1”。
端点0非常特殊,它是每个USB设备必须有的“控制端点”,双向的,专门用来完成枚举和标准控制请求。设备一上电,主机默认跟端点0通信,其他端点要等枚举完成、驱动加载之后才会被使用。配置描述符、接口描述符、端点描述符这三级结构,就是用来告诉主机“这个设备有哪些端点、每个端点怎么用”。
2.2 控制传输:枚举的基本功
控制传输是USB协议的“元老”,专门传输小量、但对可靠性要求极高的数据。它有三个阶段:
- 建立阶段:主机发一个SETUP令牌,跟一个DATA0数据包(8字节的标准请求),然后设备回ACK
- 数据阶段:可选的,根据请求类型决定是否传输数据,方向由请求决定
- 状态阶段:一个零长度的数据包,用来确认整个操作完成
枚举过程中,主机就是用标准控制请求来“盘问”设备的。最重要的请求是GET_DESCRIPTOR(读描述符)和SET_ADDRESS(分配地址)。设备刚插入时地址是0,主机先往地址0发GET_DESCRIPTOR,设备把设备描述符前8字节(包含端点0最大包长)返回,主机再发SET_ADDRESS给设备分配一个唯一地址(通常是1到127),之后所有通信都走新地址。
设备描述符里最关键的几个字段,每个写固件的人都应该烂熟于心:
| 字段 | 含义 | 典型值 |
|---|---|---|
| bLength | 描述符长度 | 0x12(18字节) |
| bDescriptorType | 描述符类型 | 0x01 |
| bcdUSB | USB版本号 | 0x0200(USB 2.0) |
| bDeviceClass | 设备类 | 0x00(在接口描述符中定义) |
| bMaxPacketSize0 | 端点0最大包长 | 0x40(64字节) |
| idVendor | 厂商ID | 如0x0403(FTDI) |
| idProduct | 产品ID | 如0x6001(FT232R) |
配置描述符、接口描述符、端点描述符逐级展开,最终形成一个完整的能力清单,告诉主机这个设备有多少个功能、每个功能有哪些端点可以走。
2.3 批量传输:大块数据搬运工
批量传输(Bulk Transfer)是干粗活的。它不保证实时性,但保证数据一定送达,错了就重传,典型应用是U盘读写、USB转串口、打印机等。批量传输每次事务能搬运的数据量最大,全速(12Mbps)下一个包最多64字节,高速(480Mbps)下最多512字节,超速(USB 3.0)下最高达到1024字节。
批量传输的精髓在NAK机制。设备如果暂时缓冲满了、没准备好接收下一个包,就会回一个NAK,主机收到NAK之后过段时间再重试。这个机制让设备可以“慢一点”,但代价是总线效率下降。调试时如果发现批量传输速度上不去,很多时候不是因为协议瓶颈,而是设备端来不及处理数据、频繁回NAK,把总线拖垮了。
高速批量传输还有个PING扩展协议:主机先发PING令牌试探设备是否准备好,设备回ACK表示可以发,主机再发OUT和数据。这个机制避免了大数据包频繁被NAK的浪费。
2.4 中断传输:小数据、周期性的正解
中断传输(Interrupt Transfer)名字很唬人,实际上并不是设备“中断”主机,而是主机周期性地来轮询。设备如果有数据,就放进端点的缓冲里,等主机来取;没有数据就回NAK。
中断传输适合传输量小但又要求低延迟的数据,比如鼠标、键盘、游戏手柄。bInterval字段规定了轮询间隔,全速设备是1到255毫秒,高速设备是125微秒的倍数。别小看这个参数,它决定了你USB鼠标的响应速度。把全速设备的bInterval设为1,意味着主机每一毫秒来问一次,这个延迟人眼根本感觉不到。
中断传输的可靠性保证和批量传输一样:出错就重传。它和批量传输的区别主要在于总线调度的优先级:在每一帧(或微帧)里,主机会先保证等时和中断传输的带宽,剩下的带宽才分给批量传输。所以中断传输适合“数据量不大但必须及时”的场景。
2.5 等时传输:实时但允许丢包
等时传输(Isochronous Transfer)是四种传输类型里最特别的。它没有握手阶段,不支持错误重传,发送方发完就走,接收方接多接少看运气。但换来的是确定的带宽和低延迟,因为主机在每一帧里都会给它预留固定时隙。
典型应用是USB音频(USB Audio)和USB摄像头(UVC)。你拿USB麦克风连线开会,偶尔一个音频包丢了,听感上就是“啪”一个小杂音,问题不大;但如果用批量传输去传音频,宁可等重传也要保证数据完整,延迟就会大到没法用。等时传输就是“大风天快递,扔进院子就走,晚点没关系,但一定给你扔到”和“宁可多跑几趟,但每件都要交到你手上”这两种物流方式里选了前者。
等时传输的端点描述符需要额外指定每帧传输的字节数,主机据此预留带宽,所以等时端点在配置时要明确告诉主机“我每帧要多少带宽”,不然配置直接失败。
3. 传输流程全景:从设备插入到第一次通信
3.1 枚举全过程:设备从“无名之辈”到“有身份证”
把枚举流程完整走一遍,前面那些抽象概念就全部串起来了:
- 设备插入,D+/D-上的上拉电阻让主机检测到设备连接
- 主机对总线复位(SE0状态),设备进入默认状态
- 主机向地址0的设备发GET_DESCRIPTOR,读取设备描述符前8字节,获得端点0最大包长
- 主机再次复位总线,发送SET_ADDRESS,给设备分配唯一地址
- 主机用新地址重新GET_DESCRIPTOR,拿到完整的18字节设备描述符
- 主机继续GET_DESCRIPTOR,读取配置描述符。注意这里配置描述符请求一次可能读不完,因为配置描述符是一整块数据(包含接口和端点描述符),主机通常会先读前9字节拿到wTotalLength,再按这个长度把整个配置描述符读回来
- 主机解析配置描述符,识别出接口类型,加载对应驱动
- 主机发SET_CONFIGURATION,设备进入配置状态,其他端点开始可用
完成这八步,设备才真正“活”了。很多USB固件问题集中在这几步之间:设备描述符bMaxPacketSize0填错,主机直接设备描述符请求失败;配置描述符wTotalLength算错,设备枚举后反复重置。
3.2 枚举不成功?优先级最高的排查思路
设备管理器里“未知USB设备”是无数工程师和电子爱好者的噩梦。我的排查顺序永远是固定的:
第一,看硬件。D+或D-的上拉电阻有没有贴错、虚焊?用万用表量一下设备端D+/D-的对地电压,全速设备D+应该被拉到3V左右。这个是最高频的问题,别一上来就翻固件。
第二,看供电。VBUS有没有到位?很多USB设备是总线供电设备,如果设备功耗超过500mA(USB 2.0规范),主机可能直接拒绝供电或出现反复断开重连。
第三,看枚举响应。用逻辑分析仪或者USB分析仪抓包,看主机发的GET_DESCRIPTOR有没有响应。如果设备根本没回包,多半是固件根本没跑起来或者时钟有问题;如果回了包但CRC错误、长度不对,那就要仔细查描述符。
第四,看驱动。设备枚举成功但驱动装不上,那是另一回事。设备管理器里显示“未知设备”和显示“设备有问题,代码10”是完全不同的两码事。
这套流程我用了很多年,基本上覆盖了九成以上的枚举失败场景。
4. 实战:USB调试的一线方案与踩坑记录
4.1 软件抓包三板斧:usbmon、USBPcap、USBlyzer
调试USB协议,抓包是最有力的手段。硬件分析仪(比如Beagle USB、Ellisys)当然最专业,能抓到底层电平信号和时序,但价格动辄上万。好在软件方案对付绝大多数问题已经够了。
Linux下最简单的是usbmon。内核默认编译了usbmon模块时,mount -t debugfs none /sys/kernel/debug之后,在/sys/kernel/debug/usb/usbmon/目录下就能看到0u、1u、2u等接口,分别对应不同的USB控制器。cat /sys/kernel/debug/usb/usbmon/0u就能实时看到URB级别的通信记录,在Wireshark里选择usbmon接口,就能图形化分析每个URB的内容、方向、状态、数据长度,排查枚举和端点通信问题非常直观。
Windows下推荐USBPcap加Wireshark的组合。USBPcap装好后,Wireshark里会出现USBPcap1之类的接口,选择后就能抓取USB流量。它能看到的也是URB层的通信,虽然没有Linux usbmon那么底层,但对绝大多数问题是够用的。如果再往上走一层,用USBlyzer这种商业工具,能直接看到USB请求块(URB)和设备驱动的交互,对驱动兼容性问题有奇效。
说句实在话,软件抓包的最大价值不是“看报文内容”,而是“看主机有没有发请求、设备有没有响应、响应内容对不对”。
4.2 FT232系列老芯片在新系统上的驱动问题真相
FT232R、FT231X、FT232H这些FTDI的USB转UART芯片,在调试嵌入式设备时几乎人手一块。但它们在Windows 10/11上经常出幺蛾子,这是我收到私信问得最多的问题之一。
经典症状有三种:第一,设备管理器识别出设备但显示黄色感叹号,错误代码10或43;第二,端口能识别但打不开,或者一打开就蓝屏;第三,芯片本身是“假货”——网上低价买的FT232R,本质上是盗版芯片,FTDI官方新驱动检测到后会故意把PID置为0000,设备直接“死了”。
处理方案按顺序来:
- 确认芯片是不是正品。用官方FT_PROG工具读芯片信息,或者直接查芯片丝印
- 卸载系统自动安装的驱动,去FTDI官网下载最新CDM驱动(FTDI VCP Driver),手动指定inf安装
- 如果还是不行,检查设备管理器的“通用串行总线控制器”里是不是有其他冲突设备
- 老芯片在新Windows上一直不稳定的话,干脆换CH340或CP2102方案,兼容性还更好
FT232R之所以经典,是因为它内部有EEPROM,可以自定义PID/VID和序列号,这对产品量产管理非常有用。但正因如此,它也成了盗版重灾区。
4.3 硬件设计容易忽略的细节:从D+/D-走线到上拉电阻
自己做USB板子的人,硬件设计这部分一定要收藏。软件层的问题再难都能通过调试解决,PCB打样回来发现信号走线有问题,只能改版重来。
重点检查这几项:
- D+/D-要做差分走线,阻抗90Ω,两条线等长,误差越小越好
- 全速设备D+上1.5kΩ上拉电阻不要省,而且最好由稳定的3.3V或VBUS供电,不是所有的MCU GPIO都能提供合适的上拉
- 设备端D+/D-要加ESD保护器件,热插拔时的静电是芯片杀手
- 高速设备还要考虑匹配电阻和过孔stub问题,不是随便拉两根线就能跑480Mbps
- VBUS和GND的滤波电容要靠近设备端放置,去耦不到位会出现枚举不稳定
我曾经设计过一块STM32的全速USB板子,D+/D-两根线走了将近5厘米还没做等长处理,结果设备插上之后有时能枚举、有时不能,频率感人。后来把走线改成差分对、等长,问题就消失了。低速和全速设备对走线要求不算苛刻,但高速设备一点含糊不得。
4.4 USB在嵌入式领域的延展玩法:转串口、转网口与自制设备
USB转串口(UART)是嵌入式调试最常用的手段,本质上是USB设备枚举成CDC-ACM类设备,主机把它识别成COM口,然后通过批量端点传输数据。市面上常见的CH340、CP2102、FT232都是这个原理。再往下游延伸,USB转TTL、USB转RS485(在UART后加RS485收发器,如SP485)、USB转网口(如AX88179这类支持CDC-ECM/RNDIS的芯片),本质上都是“USB虚拟成另一种接口”。
如果想自己做一个USB设备,现代MCU基本都内置了USB外设。开发时有一点特别值得注意:USB协议栈里端点描述符的配置、回调函数的处理,跟串口那种“发就发收就收”完全是两套思路。串口是字节流,USB是包加端点,必须在主机和设备之间先建立好“管道”。
再往深了走,Linux下还有一种玩法叫FunctionFS,属于USB Gadget子系统的功能之一,允许用户态程序直接实现USB从设备功能。你在嵌入式Linux板子上插USB线连电脑,想模拟成鼠标、键盘、网卡或自定义HID设备,都可以通过FunctionFS在用户态写代码实现,不需要改内核驱动。ESP32-S3这类带USB OTG的芯片也常常被用来实现USB摄像头、USB MIDI类设备,这些都是“设备端”开发很好的练手项目。
5. 常见问题速查表与调试心态
5.1 一张表看懂典型故障
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 设备插入完全无反应 | 上拉电阻没接/虚焊、供电异常 | 量D+/D-电压、量VBUS |
| 枚举失败,设备描述符请求失败 | 端点0最大包长错误、时钟不对、上拉电阻问题 | usbmon抓包看是否回包 |
| 枚举成功但驱动装不上 | 描述符里的VID/PID不对、设备类声明错误 | 检查设备管理器详细信息 |
| USB转串口能识别但通信乱码 | 波特率不匹配、USB批量传输丢包 | 检查串口助手波特率设置、抓包看数据 |
| 高速设备插上只跑全速 | 高速chirp握手失败、线缆质量差 | 检查设备端高速使能配置、更换线缆 |
| Linux下usbmon看不到数据 | usbmon模块未加载、权限不足 | modprobe usbmon、sudo运行Wireshark |
| FT232在Win11上报错 | 驱动被替换、假芯片被锁 | 重装官方CDM驱动、换芯片方案 |
| 设备反复断开重连 | 供电不足、ESD导致复位 | 检查总线电流、加强供电 |
5.2 调试USB这么多年,最想提醒新人的三件事
第一,先确认“主机有没有发请求”,再去查“设备为什么没回”。很多人在设备端代码里翻来覆去找问题,结果cat一下usbmon发现主机压根没发过SETUP包。这个顺序搞反了,会浪费大量时间。
第二,抓包数据一定要配套“看ACK/NAK比例”。如果设备频繁回NAK,说明设备端处理不过来,别急着怀疑协议栈有bug,优先看固件里的中断响应时间和缓冲区管理。
第三,也是最实用的建议:别在脱离总线的环境下做USB开发。我以前调试一个设备,怎么都枚举不过,后来发现是调试器占用了MCU的USB控制器引脚。把调试接口和USB引脚复用配好之后,一切恢复正常。
USB协议几十年来保持了惊人的向后兼容性,但“向下兼容”也意味着调试复杂度始终不低。把这个协议彻底吃透,往后不管是搞嵌入式、搞上位机通信,还是自己DIY硬件,都会顺手很多。这次的内容到这里就结束了,如果你们在实际项目中遇到什么奇葩USB问题,欢迎在后面留言交流。