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设备的身份证。主机通过控制传输获取这些描述符,才知道设备是什么、该怎么用。工业相机的描述符层级非常清晰:设备→配置→接口→端点。

USB描述符层级结构(工业相机视角) 设备描述符 1个设备只有1个 配置描述符 通常1个,可多个 接口描述符 UVC相机至少1个 端点描述符 批量IN(图像数据) 端点描述符 中断IN(事件通知)

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 描述符解析的调试方法

描述符解析出问题,最常见的现象就是设备枚举失败。我的调试三板斧:

  1. 抓USB总线日志:用USB分析仪或软件抓包工具(比如Wireshark配合USBPcap),看主机发了什么请求,设备回了什么数据
  2. 打印原始字节:驱动里收到描述符后,先把原始字节打印出来,对照协议文档逐字节核对
  3. 检查描述符长度:每个描述符的第一个字节是长度,第二个字节是类型。长度不对,后面的解析全错

一个真实案例:

有次客户反馈相机在Linux下枚举失败,Windows下正常。我查了三天,最后发现是配置描述符里的总长度字段(wTotalLength)填错了。Windows驱动比较宽容,会忽略这个字段自己算长度;Linux驱动严格按照协议,发现总长度和实际收到的字节数不匹配,直接拒绝设备。所以写描述符时一定要严格遵守协议,别指望所有主机都宽容。

3.12 传输类型与描述符的联动关系

最后总结一下,传输类型和描述符是怎么配合的:

  • 控制传输:走端点0,不需要在描述符里额外声明。所有设备默认支持
  • 批量传输:必须在接口描述符后面跟着端点描述符,bmAttributes=0x02
  • 中断传输:同样需要端点描述符,bmAttributes=0x03,bInterval不能乱填

驱动加载时,先通过控制传输获取设备描述符,再获取配置描述符(包含所有接口和端点描述符),然后根据接口描述符的类代码加载对应的驱动,最后通过端点描述符找到数据传输的通道。这套流程走通了,相机驱动的基础就算打牢了。