本讲目标
学完这一讲,你不需要记住任何寄存器的名字,但你应该获得一个足够坚固的直觉:在USB的世界里,Host是唯一会“主动开口”的那一方。设备从插入到被识别的全过程,本质上是一场由Host发起的、严格遵循问答规则的对话。设备可以“举手”(通过上拉电阻告诉Hub“我来了”),但它永远不能自己开口说话;它只能等待Host来问,然后回答。后续所有关于传输、驱动、协议的讨论,都建立在这个不对称关系之上。
一、USB长什么样:先建立物理层的“身体感”
在进入“电脑怎么知道你插了什么”之前,花几分钟看清USB接口本身,是有回报的——因为后面所有的“问答”,物理上就发生在其中两根细小的差分线上。
1.1 你认得的那几个形状
日常生活中最常碰到的USB接口,按代际和用途大致可以这样看:
Type-A是那个“永远插不准”的经典长方形,存在于电脑主机、充电器、Hub上。它在USB 2.0时代的核心引脚只有四根:VBUS(+5V电源)、D−、D+(差分数据线)、GND(地)。你在电脑上插U盘的那一端,就是Type-A。
Type-B是那个方形接口,常见于打印机、扫描仪和一些音频设备。它的存在本身就有防呆意图:形状决定了它不会被误插到Host端,因为它是设计给设备端用的。
Mini-USB 和 Micro-USB在Type-A/B的四根核心引脚之外增加了第五根 ID 引脚,用来在OTG场景中区分线缆哪一端是Host、哪一端是设备。Micro-USB一度是安卓手机的统一接口,如今正在被Type-C取代,但在大量嵌入式设备和低成本硬件中仍然常见。
Type-C是完全不同的设计:24针,可正反插,引入了CC引脚用于方向检测和功率协商,SuperSpeed差分对用于高速数据,SBU引脚留给Alternate Mode(DisplayPort、Thunderbolt等)。但请注意:Type-C首先是一个物理连接器标准,它承载的“USB通信”在底层仍然遵循同一套Host主导的逻辑。
1.2 四根线就够了——数据的物理载体
抛开Type-C的复杂性,USB 2.0的通信骨架极其精简:
- VBUS:+5V,给设备供电,也让设备知道“总线活着”;
- GND:电源回路的地;
- D+ 和 D−:一对差分线,所有“问答”的比特流都跑在这两根线上。
差分信号的意思是:信息不是靠某根线对地的绝对电压来表示的,而是靠D+和D−之间的电压差。这让USB线在几米长度上对抗电气噪声的能力远超单线方案。你不需要懂电气细节,只需要记住:后面所有关于枚举、描述符、控制传输的“对话”,物理上就是Host和Device在这对差分线上的推挽。
二、插进去之后发生了什么:枚举的“故事版”
现在进入本讲的核心。当你把U盘插进电脑,Windows右下角可能几秒内就弹出“设备已就绪”。这几秒里发生的事情,叫做枚举。把它讲成一个故事,比记状态机要直观得多。
2.1 故事的起点:设备“举手”,Host“看见”
设备插入的瞬间,它做的第一件事不是发送任何数据,而是在自己的D+或D−线上接上一个上拉电阻。Hub(主机控制器上的根Hub,或者外接Hub的端口)检测到这个上拉带来的电平变化,就知道“这个端口上有东西接进来了”。Hub随即通过中断管道向Host报告端口状态变化,Host开始介入。
这里已经出现了第一个不对称:设备只能“举手”,不能“说话”。它不能主动发一个包说“我是U盘”。它只能改变一根线的电平,然后等Host来问。
Host在确认端口状态后,会等待至少100ms让电源稳定,然后命令Hub对该端口执行复位。复位完成后,设备进入一个特殊状态:它开始监听地址0,并且只能从VBUS吸取不超过100mA的电流。此时它还没有名字(地址),Host还不知道它是什么,但它已经准备好回答问题了。
2.2 第一轮问答:只取8个字节
Host通过地址0向设备的端点0发送第一个请求:Get_Descriptor,目标是设备描述符。但Host并不贪婪——它只请求设备描述符的前8个字节。
为什么只取8个?因为设备描述符的第8个字节(偏移量7)是bMaxPacketSize0,它告诉Host“我的默认控制端点一次最多能收/发多大的数据包”。在不知道这个值之前,Host如果一次性要太多,设备可能装不下;要太少又浪费时间。所以先取8字节拿到“你的容量是多少”,再决定后续怎么谈。
这是一个非常典型的USB式节奏:Host先问一个小问题,拿到关键参数,再调整后续问题的“尺寸”。
2.3 命名:从“那个设备”到“地址7”
拿到bMaxPacketSize0之后,Host再次复位设备,然后发送Set_Address,给设备分配一个唯一的地址(比如7)。设备保存这个地址并返回确认,从此它不再响应地址0(除非再次复位),而是只回应地址7。
这一步在故事里相当于:主人问“你叫什么”,然后给了它一个编号。在枚举过程中,同一时刻只能有一个设备占用默认地址0,所以Host必须逐个完成“命名”,才能继续和多个设备分别对话。
2.4 正式“查户口”:把描述符一层层要出来
有了新地址后,Host重新发送Get_Descriptor,这次要的是完整的18字节设备描述符。从里面,Host读到VID(厂商ID)、PID(产品ID)、设备类代码等信息。
接着,Host要配置描述符。但配置描述符的长度不是固定的(它后面可能跟着接口描述符和端点描述符),所以Host先要9个字节,从里面读出wTotalLength,知道整个配置集合有多长,然后再把全部内容取回来。如果设备有字符串描述符(厂商名、产品名、序列号),Host也会逐一索取,用于在系统里展示可读信息。
这些描述符不是平铺的,而是一棵层次树:设备描述符在最高层,下面挂着配置描述符,配置下面挂着接口描述符,接口下面挂着端点描述符。接口描述符里的类/子类/协议字段,将直接决定Host给它加载什么驱动(下一节展开)。
2.5 最后一步:选择配置,设备“上岗”
Host根据收集到的信息,决定给这个设备加载哪个驱动,然后发送Set_Configuration,指定一个非零的配置值。设备接受后进入Configured 状态,从这一刻起,它才可以正式在接口和端点上进行数据传输。
如果设备是复合设备(比如一个带音频的摄像头),Host还会在配置内为各个接口分别绑定驱动。整个枚举结束,U盘出现在“此电脑”里,键盘开始响应按键——而这些“功能”的起点,仅仅是一连串Host发起的Get_Descriptor、Set_Address、Set_Configuration。
三、为什么电脑不需要装驱动就能认出U盘
这是初学者最容易困惑的地方:同样是USB设备,为什么U盘插上就能用,而某些开发板或专用仪器却要装驱动?
答案不在“U盘更简单”,而在USB设备类的设计。
3.1 标准类:设备自报“我是哪一类”,操作系统自带“通用答法”
USB规范定义了一组标准设备类。当一个设备的接口描述符中声明的类代码落在这组标准类里,操作系统就不需要厂商提供专用驱动,因为它已经内置了针对这个类的通用驱动。
最常见的几个类包括:大容量存储类(MSC,类代码08h),U盘、移动硬盘、存储卡读卡器都属于它;人机接口设备类(HID,03h),键盘、鼠标、游戏手柄;通信设备类(CDC,02h/0Ah),虚拟串口、网络适配器;音频类(01h)、**视频类(0Eh)**等。
这些类之所以能“免驱”,是因为规范不仅定义了类代码,还定义了该类设备必须如何响应哪些请求、使用哪些端点、数据格式是什么。操作系统看到“接口类 = 08h”,就知道“这是一个存储设备,我应该用大容量存储驱动去和它说话,用Bulk端点传数据,用SCSI命令集去读写扇区”。厂商不需要自己发明一套协议,只需要在描述符里诚实地声明自己属于哪个标准类,然后按规范行事。
3.2 自定义类:声明为“厂商特定”,操作系统就不认识了
如果一个设备的接口类代码是FFh(厂商特定),操作系统在枚举时读到这个值,就知道:“这个设备不遵循任何我认识的标准协议,我没有内置的通用驱动可以和它对话。”于是它要么提示用户安装厂商提供的驱动,要么在设备管理器里显示一个未知设备。
还有一些设备虽然属于某个标准类,但使用了厂商扩展。它们可能在标准类的基础上增加自定义请求或数据格式,仍然需要额外的驱动或INF文件来完成“标准驱动 + 厂商扩展”的配合。
所以,“免驱”的本质不是USB接口的神奇,而是设备在枚举阶段主动说出了操作系统能听懂的语言。U盘说“我是MSC”,Windows回答“好,我用MSC驱动来管你”。开发板说“我是FFh”,Windows只能说“我不认识你,请给我说明书(驱动)”。
四、把直觉焊死:Host主导的问答式通信
回到本讲真正要你带走的东西。枚举的每一步——复位、Get_Descriptor、Set_Address、Set_Configuration——在总线上都有一个共同的发起者:Host。
USB的底层事务结构把这一点体现得最赤裸。一次传输由一个Token包开头,而Token包只能由Host发出。Token有三种类型:
- SETUP:Host说“我要给你一个请求”(用于控制传输的起始);
- IN:Host说“把数据给我”;
- OUT:Host说“我要把数据给你”。
设备永远在等Token。它不能发一个Token说“我要报告一个事件”。即使是“设备有数据要上传”这种听起来像是设备主动的场景(比如键盘按下了一个键),物理上的顺序也是:Host周期性地发一个IN Token来问“你有事吗?”,设备如果有数据就在数据包中回答,如果没事就回一个NAK说“暂时没有”。设备的所有“主动”,都发生在Host允许它回答的那个窗口里。
控制传输把这个问答结构推到了最严格的形式:SETUP阶段Host发出请求,DATA阶段按请求的方向传输数据(方向仍然由SETUP阶段决定),STATUS阶段由硬件自动完成握手。整个过程中,Host决定“问什么”“什么时候问”“问多深”,设备只负责按规则应答。
你现在不需要记住SETUP/DATA/STATUS的字节细节。你只需要在脑子里留一句话:USB总线上的沉默权属于Host。设备没有被问,就不能答。下一讲无论讲什么传输类型、什么描述符、什么驱动匹配,你都会发现它们只是这套问答规则在不同场景下的展开。
本讲小结
- USB 2.0的核心物理通道只有四根线:VBUS、GND、D+、D−;Type-C改变了连接器的形态,但没有改变Host主导的通信逻辑。
- 枚举是一套Host发起、设备应答的识别流程:Hub检测连接 → Host复位 → 在地址0取8字节设备描述符 →
Set_Address分配唯一地址 → 取完整描述符树 →Set_Configuration使设备进入可用状态。 - “免驱”的U盘之所以免驱,是因为它在接口描述符中声明了标准类(如MSC),操作系统内置了该类的通用驱动;自定义类(FFh)则需要厂商驱动。
- 底层事务由Token包开路,而Token包只能由Host发出:SETUP是“我要问你”,IN是“给我数据”,OUT是“收下数据”。设备只能应答,不能发起。
留给下一讲的问题:Host用来“问”设备的那些请求(Get_Descriptor、Set_Address、Set_Configuration),走的是哪种传输类型?这种传输为什么被设计成“必须由Host发起”?当你理解了“控制传输”,你就理解了USB枚举的交通规则本身。