4. USB 2.0/3.0协议详解:USB拓扑与主机控制器、传输类型、描述符链、URB请求块、USB 3.0超高速差异
大家好,我是你们的嵌入式驱动开发讲师。今天我们来啃一块硬骨头——USB协议。说实话,USB协议栈的复杂度在PC外设接口里能排前三。很多工程师一听到「描述符链」、「URB」这些词就头大。别急,我们一步步拆开来看。
我个人习惯把USB协议理解成一套「外设即插即用的通信规则」。你插个U盘、鼠标、摄像头,系统怎么知道你是谁?怎么跟你说话?怎么保证数据不丢?这些问题的答案,都藏在我们今天要讲的内容里。
4.1 USB拓扑与主机控制器
4.1.1 拓扑结构:星型中的星型
USB的拓扑结构是典型的「树形」或「星型」结构。最顶端是主机控制器,下面挂接根集线器(Root Hub),再往下可以级联集线器,最多支持7层(包括根集线器)。
为什么这么设计?说白了,就是为了解决「一个主机带多个设备」的问题。你想想看,如果所有设备都直接连到主机上,那主机的接口得有多少?而且信号完整性也扛不住。
我在项目中遇到过一个问题:一个USB摄像头在级联到第4层集线器后,经常掉线。排查下来发现是集线器的供电不足,加上信号经过多次中继后衰减严重。嗯,这里要注意:USB 2.0的线缆长度限制是5米,但经过集线器级联后,总距离不能超过30米。
4.1.2 主机控制器:EHCI与xHCI
主机控制器是USB的大脑。在USB 2.0时代,主流的是EHCI(Enhanced Host Controller Interface)。它负责管理所有USB 2.0的高速(480Mbps)和全速/低速设备。
到了USB 3.0时代,事情变得复杂了。因为USB 3.0引入了全新的超高速(SuperSpeed)总线,和USB 2.0的电气特性完全不同。所以Intel搞了个xHCI(eXtensible Host Controller Interface)。
xHCI牛在哪里?它同时兼容USB 1.x/2.0/3.0。也就是说,一个xHCI控制器可以管理所有类型的USB设备。我曾经在调试一个USB 3.0的U盘时,发现它在xHCI下跑出了350MB/s的读速度,但换到EHCI下只能跑40MB/s——因为EHCI根本不支持超高速。
lspci -v 查看主机控制器类型。如果是 USB Controller: Intel Corporation 8 Series/C220 Series Chipset Family USB xHCI,那就是xHCI。
4.2 传输类型:四种武器
USB协议定义了四种传输类型。为什么需要四种?因为不同的外设有不同的需求。鼠标需要低延迟,U盘需要高吞吐,摄像头需要实时性,控制命令需要可靠性。一种传输类型搞不定所有场景。
4.2.1 控制传输(Control Transfer)
这是USB的「元传输」。每个USB设备都必须支持控制传输。它主要用于设备枚举、配置、状态查询等操作。
控制传输的特点是:可靠但慢。它由三个阶段组成:建立阶段(Setup)、数据阶段(Data,可选)、状态阶段(Status)。
我记得刚做驱动时,写了一个控制传输请求去读取设备的描述符,结果一直超时。后来发现是wLength字段填错了——你请求的数据长度不能超过设备实际能提供的长度,否则设备会直接STALL掉这个请求。
4.2.2 批量传输(Bulk Transfer)
批量传输是USB 2.0/3.0里最常用的传输类型。它保证数据可靠传输,但不保证延迟。说白了,就是「什么时候有空,什么时候传」。适合U盘、打印机这类对实时性要求不高的设备。
USB 2.0下,批量传输的最大包大小是512字节。USB 3.0下,这个数字提升到了1024字节。而且USB 3.0的批量传输支持突发传输(Burst),一次可以连续发多个包,吞吐量直接起飞。
我在项目中遇到过一个问题:一个U盘在批量写入时,速度只有10MB/s。用USB分析仪抓包发现,主机每次只发一个批量包,然后等设备ACK后再发下一个。后来在驱动里开启了URB的URB_BULK_CONTINUATION标志,允许主机连续发送多个批量包,速度直接飙到了30MB/s。
4.2.3 中断传输(Interrupt Transfer)
名字叫「中断」,但它和硬件中断完全是两码事。中断传输其实是主机定期轮询设备,看设备有没有数据要传。轮询间隔由设备在描述符里指定,最小可以到1毫秒(USB 2.0全速)或125微秒(USB 2.0高速)。
中断传输适合鼠标、键盘这类需要低延迟的设备。它的特点是:保证延迟,但数据量小。USB 2.0下,中断传输的最大包大小是64字节(全速)或1024字节(高速)。
嗯,这里要注意:中断传输的轮询间隔不能太短,否则会占用大量总线带宽。我曾经调试一个游戏鼠标,它把轮询间隔设成了1毫秒,结果导致同一个集线器上的U盘速度暴跌。最后在设备固件里把轮询间隔改成了4毫秒,问题解决。
4.2.4 同步传输(Isochronous Transfer)
同步传输是USB里最特殊的传输类型。它保证延迟,但不保证可靠性。也就是说,数据丢了就丢了,不会重传。为什么?因为音频、视频这类数据对实时性要求极高,重传会导致卡顿,还不如丢一帧。
同步传输没有ACK/NAK握手,主机只管发,设备只管收。USB 2.0下,同步传输的最大包大小是1023字节。USB 3.0下,这个限制被大幅放宽,可以达到1024字节×突发数。
我记得在调试一个USB摄像头时,发现画面偶尔出现撕裂。用USB分析仪抓包发现,同步传输的包在总线繁忙时被主机直接丢弃了。解决方案是在驱动里申请更大的缓冲区,允许设备缓存几帧数据,这样即使丢包也能从缓冲区里补回来。
| 传输类型 | 可靠性 | 延迟保证 | 数据量 | 典型应用 |
|---|---|---|---|---|
| 控制传输 | 高 | 无 | 小 | 枚举、配置 |
| 批量传输 | 高 | 无 | 大 | U盘、打印机 |
| 中断传输 | 高 | 有 | 小 | 鼠标、键盘 |
| 同步传输 | 低 | 有 | 中 | 摄像头、音频 |
4.3 描述符链:设备的身份证
每个USB设备都有一组描述符,就像你的身份证一样,告诉主机「我是谁、我能干什么」。描述符是层级结构的,从设备描述符开始,一层层往下。
4.3.1 设备描述符(Device Descriptor)
这是最顶层的描述符。它包含了USB版本号、设备类、厂商ID(VID)、产品ID(PID)等信息。主机通过控制传输读取设备描述符,就知道该加载哪个驱动了。
设备描述符是18字节固定长度。其中bcdUSB字段表示USB规范版本号,比如0x0200表示USB 2.0,0x0300表示USB 3.0。
4.3.2 配置描述符(Configuration Descriptor)
一个设备可以有多个配置,但同一时刻只能有一个配置生效。配置描述符告诉主机:这个配置下有多少个接口、需要多少功耗。
配置描述符是9字节固定长度,后面跟着接口描述符和端点描述符。我建议你在写设备固件时,尽量只提供一个配置,否则主机在枚举时可能会选错配置,导致驱动加载失败。
4.3.3 接口描述符(Interface Descriptor)
接口描述符描述了一个功能单元。比如一个USB摄像头可能有视频接口和音频接口。每个接口可以包含多个端点。
接口描述符是9字节固定长度。其中bInterfaceClass字段定义了接口的功能类型,比如0x01表示音频,0x07表示打印机,0x08表示大容量存储。
4.3.4 端点描述符(Endpoint Descriptor)
端点是USB通信的基本单元。每个端点都有一个地址(0-15)和一个方向(IN/OUT)。端点0是控制端点,所有设备都必须支持。
端点描述符是7字节固定长度。其中bmAttributes字段定义了端点的传输类型(0=控制,1=同步,2=批量,3=中断)。
lsusb -v 查看设备的完整描述符链。我曾经用这个命令发现一个U盘的端点描述符里wMaxPacketSize字段写错了,导致驱动无法正确分配缓冲区。
4.4 URB请求块:驱动与USB核心的桥梁
在Linux USB驱动中,URB(USB Request Block)是核心数据结构。它封装了一次USB传输的所有信息:目标设备、端点、传输类型、数据缓冲区、回调函数等。
URB的生命周期是这样的:驱动创建URB → 填充URB → 提交给USB核心 → USB核心调度传输 → 传输完成 → 回调函数被调用。
我写一个简单的批量传输URB示例给你看:
struct urb *urb;
unsigned char *buf;
// 1. 分配URB
urb = usb_alloc_urb(0, GFP_KERNEL);
if (!urb) {
dev_err(&interface->dev, "Failed to allocate URB\n");
return -ENOMEM;
}
// 2. 分配DMA缓冲区
buf = usb_alloc_coherent(dev, 1024, GFP_KERNEL, &urb->transfer_dma);
if (!buf) {
usb_free_urb(urb);
return -ENOMEM;
}
// 3. 填充URB
usb_fill_bulk_urb(urb, dev, usb_sndbulkpipe(dev, 1),
buf, 1024, bulk_write_callback, interface);
// 4. 设置DMA标志
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
// 5. 提交URB
ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
dev_err(&interface->dev, "Failed to submit URB: %d\n", ret);
usb_free_coherent(dev, 1024, buf, urb->transfer_dma);
usb_free_urb(urb);
return ret;
}
嗯,这里要注意:URB的回调函数是在中断上下文执行的,所以不能在里面调用可能睡眠的函数(比如kmalloc、copy_to_user)。我曾经在回调函数里调用了printk,结果导致系统卡死——因为printk在中断上下文里可能会触发调度。
4.5 USB 3.0超高速差异
USB 3.0的「超高速」和USB 2.0的「高速」相比,不仅仅是速度上的提升(5Gbps vs 480Mbps),而是架构上的根本性变革。
4.5.1 双总线架构
USB 3.0的物理层是双总线的:一条是传统的USB 2.0总线(D+/D-),另一条是全新的超高速总线(SSTX+/SSTX-、SSRX+/SSRX-)。这意味着USB 3.0设备可以同时工作在USB 2.0和USB 3.0模式下。
为什么这么设计?说白了,就是为了兼容性。你插一个USB 3.0 U盘到USB 2.0接口上,它还能用,只是速度降级到USB 2.0。
4.5.2 协议层的差异
USB 3.0的协议层和USB 2.0完全不同。USB 2.0是半双工的,所有设备共享一条总线。USB 3.0是全双工的,每个设备有独立的发送和接收通道。
USB 3.0还引入了流控(Flow Control)机制。设备可以告诉主机「我暂时收不了数据了」,主机就会暂停发送。这在USB 2.0里是靠NAK握手实现的,效率很低。
我记得在调试一个USB 3.0的SSD时,发现它在高负载下速度不稳定。用USB分析仪抓包发现,设备频繁发送NRDY(Not Ready)包,导致主机不断重试。后来在设备固件里优化了缓冲区管理,减少了NRDY的发送频率,速度就稳定了。
4.5.3 电源管理差异
USB 3.0的电源管理比USB 2.0复杂得多。它引入了U0/U1/U2/U3四种电源状态。U0是正常工作,U1/U2是低功耗待机,U3是休眠。
设备可以在空闲时主动进入U1/U2状态,主机也可以强制设备进入U3状态。但要注意:从U3唤醒需要时间,如果设备频繁进入U3,会导致响应延迟增加。
echo on > /sys/bus/usb/devices/1-1/power/control 强制USB设备不进入低功耗状态。我在调试USB 3.0摄像头时,发现画面偶尔卡顿,就是因为设备频繁进入U1状态导致的。
4.5.4 批量传输的改进
USB 3.0的批量传输引入了流(Stream)的概念。一个批量端点可以有多个流,每个流可以独立传输数据。这大大提高了多线程场景下的吞吐量。
举个例子:一个USB 3.0 SSD可以同时处理多个读写请求,每个请求使用不同的流。主机不需要等待一个请求完成再发下一个,而是可以同时发送多个请求。
我在项目中测试过,开启流支持后,一个USB 3.0 SSD的随机读写性能提升了约30%。但要注意:流支持需要设备和驱动双方都支持,否则会回退到传统的批量传输模式。
好了,USB协议的核心内容就讲到这里。下一章我们会深入USB设备枚举的完整流程,包括复位、获取描述符、配置设备等步骤。到时候我会带大家手写一个简单的USB驱动,把今天讲的理论用代码实现出来。