3. USB协议基础
做工业相机驱动,USB协议是绕不开的坎。我刚开始接触时也被那堆描述符搞得头晕,后来摸清了门道,发现其实就那几类传输方式在干活。今天咱们就把USB控制传输、批量传输、中断传输,以及设备、配置、接口、端点这四层描述符,一个一个掰开揉碎了讲。
3.1 USB传输类型:相机驱动里的三把刀
USB协议定义了四种传输类型,但工业相机驱动里常用的就三种:控制、批量、中断。等时传输在高速视频采集里也有用,但咱们先聚焦前三者,这是定位问题的基础。
| 传输类型 | 特点 | 相机驱动中的典型用途 | 我踩过的坑 |
|---|---|---|---|
| 控制传输 | 可靠、双向、数据量小(≤64字节) | 枚举、获取描述符、设置参数 | 控制传输超时导致枚举失败 |
| 批量传输 | 可靠、单向、数据量大、无带宽保证 | 图像数据传输、固件下载 | 批量端点STALL导致帧丢失 |
| 中断传输 | 可靠、低延迟、轮询机制 | 触发事件、状态反馈、GPIO | 中断间隔设置不当导致CPU空转 |
3.2 控制传输:驱动的握手礼
控制传输是所有USB通信的基础。设备刚插上时,主机就是靠控制传输来跟设备打招呼的。它分三个阶段:建立阶段、数据阶段(可选)、状态阶段。
控制传输的四个类型:
- 0x00 标准请求:获取描述符、设置地址、设置配置等,所有设备必须支持
- 0x01 类请求:UVC相机特有的请求,比如设置曝光、增益
- 0x02 厂商请求:芯片厂商自定义,我见过用来做固件升级的
- 0x03 保留:基本用不到
我在项目中遇到过一个问题:相机枚举时总是卡在获取配置描述符这一步。后来抓USB总线日志才发现,是控制传输的wLength字段填错了,设备返回的数据比主机预期的短,导致状态阶段一直等不到ACK。说白了,控制传输的每个字段都得对齐,差一个字节都不行。
3.3 批量传输:图像数据的搬运工
批量传输是工业相机传输图像的主力。它可靠,但优先级低,USB总线空闲时才传。你想想看,如果总线上同时挂着鼠标和相机,鼠标的中断传输优先级更高,相机的批量传输就得等着。
批量传输的端点通常成对出现:一个IN端点(设备→主机),一个OUT端点(主机→设备)。工业相机一般只用IN端点发图像,OUT端点偶尔用来发命令。
批量传输的STALL机制:
我曾经调试过一个相机,图像传着传着突然断了。查了半天,发现是批量端点返回了STALL。原因很蠢——设备端的FIFO满了,硬件自动发了STALL。主机收到STALL后,会停止该端点的所有传输,直到主机发送ClearFeature请求清除STALL状态。所以驱动里一定要处理STALL恢复逻辑。
3.4 中断传输:事件通知的快递员
中断传输名字带"中断",但别被它骗了。它不是硬件中断,而是主机以固定间隔轮询设备,看有没有数据要传。工业相机里,中断传输常用于传输触发事件、帧同步信号、GPIO状态变化等小数据量、低延迟的场景。
中断传输的关键参数是轮询间隔(bInterval)。全速设备最小1ms,高速设备最小125μs(微帧)。我建议不要设得太小,否则CPU占用率会飙升。我之前接手过一个项目,中断间隔设成了1ms,结果主机CPU占用率直接飙到30%,就为了轮询一个状态寄存器。后来改成8ms,完全够用。
3.5 描述符解析:设备的身份证
描述符是USB设备的身份证。主机通过控制传输获取这些描述符,才知道设备是什么、该怎么用。工业相机的描述符层级非常清晰:设备→配置→接口→端点。
3.6 设备描述符:第一张名片
设备描述符是主机获取的第一个描述符,长度固定18字节。关键字段就几个:
- idVendor / idProduct:厂商ID和产品ID,驱动匹配的依据
- bcdUSB:USB规范版本号,比如0x0200表示USB 2.0
- bMaxPacketSize0:端点0的最大包长,全速是8/16/32/64,高速固定64
- bNumConfigurations:配置描述符数量,工业相机通常只有1个
调试技巧:
我习惯在驱动加载时先打印设备描述符的idVendor和idProduct,确认设备枚举成功。如果这两个字段读不到,说明控制传输有问题,先查硬件连接和USB线缆质量。
3.7 配置描述符:设备的能力清单
配置描述符告诉主机设备需要多少电流、有多少接口。工业相机通常只需要一个配置,但里面可能包含多个接口。比如UVC相机就有两个接口:一个用于视频控制(VC),一个用于视频流(VS)。
配置描述符里有个字段叫bMaxPower,单位是2mA。我遇到过一台相机,标称功耗2.5W,但bMaxPower填了250(即500mA),插到USB 2.0端口上直接供电不足。后来改成150(300mA),配合外部供电才稳定。所以驱动里最好检查一下这个字段,如果超过总线供电能力,提示用户用外部电源。
3.8 接口描述符:功能的划分
接口描述符是描述符解析的重点。一个配置可以有多个接口,每个接口代表一个功能。工业相机里常见的接口组合:
| 接口编号 | 功能 | 端点数量 | 传输类型 |
|---|---|---|---|
| 接口0 | 视频控制(VC) | 1个中断端点 | 中断传输(事件通知) |
| 接口1 | 视频流(VS) | 1个批量端点 | 批量传输(图像数据) |
接口描述符的bInterfaceClass字段决定了驱动用什么协议。UVC相机的类代码是0x0E(视频设备类),子类代码0x01(视频控制)或0x02(视频流)。驱动根据这个字段加载对应的类驱动。
3.9 端点描述符:数据的出入口
端点描述符是最底层的描述符,定义了数据怎么进出设备。每个端点描述符7字节,关键字段:
- bEndpointAddress:端点号+方向。bit7=0表示OUT(主机→设备),bit7=1表示IN(设备→主机)。比如0x81表示端点1 IN
- bmAttributes:传输类型。0x02表示批量,0x03表示中断
- wMaxPacketSize:最大包长。高速批量端点通常是512字节
- bInterval:轮询间隔。中断传输用,单位取决于速度
端点描述符的常见陷阱:
我曾经调试过一个相机,图像传输总是丢帧。抓USB包发现,批量端点的wMaxPacketSize填的是1024,但USB 2.0高速批量端点最大只能512。设备端硬件实际只支持512,但描述符写错了。主机按1024准备缓冲区,结果设备每次只发512,导致数据错位。所以描述符里的值一定要和硬件实际能力一致,否则驱动层怎么调都没用。
3.10 描述符解析实战:从原始字节到驱动结构体
驱动里解析描述符,说白了就是把USB总线返回的原始字节流,按照协议规定的格式,填到结构体里。下面是一个简化的解析流程:
// 设备描述符解析(18字节)
struct usb_device_descriptor {
uint8_t bLength; // 固定18
uint8_t bDescriptorType; // 固定0x01
uint16_t bcdUSB; // USB版本号
uint8_t bDeviceClass; // 设备类
uint8_t bDeviceSubClass; // 设备子类
uint8_t bDeviceProtocol; // 设备协议
uint8_t bMaxPacketSize0; // 端点0最大包长
uint16_t idVendor; // 厂商ID
uint16_t idProduct; // 产品ID
uint16_t bcdDevice; // 设备版本号
uint8_t iManufacturer; // 厂商字符串索引
uint8_t iProduct; // 产品字符串索引
uint8_t iSerialNumber; // 序列号字符串索引
uint8_t bNumConfigurations; // 配置数量
};
// 解析时注意字节序:USB协议是小端
// idVendor和idProduct是uint16_t,低字节在前
// 比如收到 0x84 0x1A,实际值是 0x1A84
我建议驱动里不要直接memcpy原始数据到结构体,因为不同编译器的结构体对齐方式不一样。最好逐字段手动解析,虽然代码啰嗦点,但不会踩对齐的坑。
3.11 描述符解析的调试方法
描述符解析出问题,最常见的现象就是设备枚举失败。我的调试三板斧:
- 抓USB总线日志:用USB分析仪或软件抓包工具(比如Wireshark配合USBPcap),看主机发了什么请求,设备回了什么数据
- 打印原始字节:驱动里收到描述符后,先把原始字节打印出来,对照协议文档逐字节核对
- 检查描述符长度:每个描述符的第一个字节是长度,第二个字节是类型。长度不对,后面的解析全错
一个真实案例:
有次客户反馈相机在Linux下枚举失败,Windows下正常。我查了三天,最后发现是配置描述符里的总长度字段(wTotalLength)填错了。Windows驱动比较宽容,会忽略这个字段自己算长度;Linux驱动严格按照协议,发现总长度和实际收到的字节数不匹配,直接拒绝设备。所以写描述符时一定要严格遵守协议,别指望所有主机都宽容。
3.12 传输类型与描述符的联动关系
最后总结一下,传输类型和描述符是怎么配合的:
- 控制传输:走端点0,不需要在描述符里额外声明。所有设备默认支持
- 批量传输:必须在接口描述符后面跟着端点描述符,bmAttributes=0x02
- 中断传输:同样需要端点描述符,bmAttributes=0x03,bInterval不能乱填
驱动加载时,先通过控制传输获取设备描述符,再获取配置描述符(包含所有接口和端点描述符),然后根据接口描述符的类代码加载对应的驱动,最后通过端点描述符找到数据传输的通道。这套流程走通了,相机驱动的基础就算打牢了。