2. USB主机与设备架构:主机控制器、设备端点、管道与事务
这一章,我们来聊聊USB架构里的几个核心概念。说白了,就是搞明白主机那头是怎么工作的,设备那头又是怎么回应的。我刚开始接触USB驱动栈的时候,也被这些术语绕得晕头转向。后来踩了几个坑,才算真正理顺了。你想想看,所有USB通信,其实都围绕着这几个东西转:主机控制器、端点、管道和事务。
2.1 主机控制器——USB总线的大脑
主机控制器,顾名思义,就是USB总线的大脑。它负责发起所有的通信。设备只会被动响应,不会主动找主机说话。这是USB架构的一条铁律。我在项目中遇到过好几次,因为没搞清楚这个主从关系,导致调试了半天才发现是设备主动发送了数据,直接就被主机忽略了。
主机控制器其实是一个硬件模块,挂在系统总线上(比如PCIe或者AHB)。它内部实现了USB协议栈的物理层和链路层。驱动通过操作它的寄存器,来发起传输。
- 硬件角色:管理总线上的帧周期、生成SOF(帧起始)包、处理错误检测。
- 传输发起者:所有事务都由主机控制器开启,设备只能等待被“点名”。
- 策略制定者:它决定某一帧里先跟哪个设备通信,后跟哪个设备通信。
不同的USB版本,对应不同规格的主机控制器。我个人习惯把这个清单记在脑子里,遇到对应硬件就直接定位问题区域:
| 规范版本 | 控制器类型 | 特点 |
|---|---|---|
| USB 1.x | OHCI(Open HCI) | 软实现为主,中断处理较频繁 |
| USB 1.x | UHCI(Universal HCI) | Intel主导,硬件更简单,驱动活多 |
| USB 2.0 | EHCI(Enhanced HCI) | 支持高速480Mbps,兼容OHCI/UHCI |
| USB 3.x | xHCI(eXtensible HCI) | 一个驱动搞定所有速度等级的设备 |
调试小技巧:OTG设备常使用xHCI控制器。因为它能动态切换角色——一会儿当主机,一会儿当设备。如果你遇到角色切换失败,第一步就去检查控制器的工作模式寄存器。我曾经有一个项目,反复插拔都切不过去,最后发现是D+和D-线上的下拉电阻值配错了,控制器检测不到电平变化。
2.2 设备端点——通信的数据口
端点这个概念,说白了就是设备端的一个数据口。每个USB设备可以有多个端点,每个端点都有自己的编号和方向。我把它想象成一台服务器的不同端口:端口80跑HTTP,端口22跑SSH。端点和用途一一对应。
- 端点0:这是每个设备的标配端点,用于控制传输。枚举设备、读描述符、配置设备都靠它。没有端点0,设备连枚举都过不去。
- 批量端点:大块数据传输,比如U盘读文件。不保证带宽,但保证数据正确。
- 中断端点:小包数据,周期性传输,比如鼠标键盘。我没见过鼠标用批量端点的,延迟受不了。
- 等时端点:实时流数据,比如音频。允许偶尔丢一包,但不能等重传。
端点有方向,这一点必须记牢:
- IN端点:数据从设备流向主机(设备发,主机收)。
- OUT端点:数据从主机流向设备(主机发,设备收)。
端点描述符里有个bEndpointAddress字段,最高位表示方向(1=IN,0=OUT),低4位是端点号。嗯,这里要注意:端点号不是随便填的,必须和设备固件里注册的一致。我见过有人把端点号填错,结果枚举过了,但一传输就超时。
2.3 管道——逻辑通信通道
管道把主机和设备的端点连接起来。你可以把它想象成一条逻辑通道。主机控制器通过管道往端点里写数据或读数据。
为什么要有管道?其实是为了抽象化。管道屏蔽了底层的传输细节。驱动里操作的是一个管道对象,而不是直接去控制总线上的时序。我在写驱动时,每次申请一个管道,都要指定目标设备的地址、端点号、传输类型、最大包长。
管道和端点是一一对应的:一个管道只连接一个端点。但注意,端点0对应默认控制管道,这个管道在设备上电后自动建立,不用你手动申请。
- 默认控制管道:端点0专用,双向通信。
- 批量管道:一对一批量端点。
- 中断管道:一对一中止端点。
- 等时管道:一对一等时端点。
踩坑经验:我曾经调试一个USB摄像头,发现图像总是有花块。跟踪了好几天,最后发现是等时管道的最大包长设置错了。摄像头要求1023字节,我设成了512。等时传输不会重传,所以包一旦被分割,就拼不回去了。那个图像真是惨不忍睹。
2.4 事务——总线上的最小动作单元
事务是USB通信的最小单元。每个事务由三个阶段组成:
- 令牌阶段:主机控制器发送一个令牌包,告诉总线“谁、往哪、做什么”。
- 数据阶段:按令牌方向,发送或接收数据包。
- 握手阶段:收方返回ACK/NAK/STALL,告诉发方这包我收好了、还是忙、或者不支持。
不同的传输类型,事务的结构也不太一样。
| 传输类型 | 典型事务结构 | 容错机制 |
|---|---|---|
| 控制传输 | 建立事务 → 数据事务(可多次) → 状态事务 | 必须重传 |
| 批量传输 | 单个事务带PING/NAK | 重传直到成功 |
| 中断传输 | 周期性单个事务 | 重传直到下一个周期 |
| 等时传输 | 只做令牌+数据,没有握手 | 不保证正确 |
性能优化:你可能会觉得等时传输不握手很浪费,没有ACK怎么知道发没发出去?其实音频流不关心那一两毫秒的丢包,重传反而导致抖动。我调过一个音频延时敏感的项目,就是故意禁用了等时装的重传机制,用户才满意。
整个USB主机与设备架构的关系,可以用下面这张图来总结。它展示了主机控制器、设备、端点、管道和事务之间是怎么串起来的。
核心理解:主机控制器是驱动者,设备是被驱动的。端点定义服务点,管道建立逻辑连接,事务才是真正在总线上跑的比特。你把这四个概念串起来,USB驱动栈的拼图就完整了。
特别注意:Android下的USB驱动调试,经常卡在枚举阶段出问题。百分之八十的原因是控制传输不成功。你先检查端点0的描述符对不对,再检查最大包长是不是与设备固件一致。我曾经有一次整了半天,结果发现设备固件里端点0的maxPacketSize写的是64,但我的主机驱动默认最大包长设成了8,通信直接就断开了。