4. USB类协议详解:HID类(键盘鼠标)、Mass Storage类(U盘)、CDC类(虚拟串口)、Video类(摄像头)

好,咱们进入USB协议栈里最接地气的部分——类协议。说白了,USB协议只定义了底层怎么传数据,但传上去的数据是什么含义?键盘按下了哪个键?U盘里存了什么文件?这些事USB主机根本不知道。所以就有了「类协议」这个东西,它给设备分了类,定好了数据格式。

我个人习惯把这四个类称为「四大金刚」:HID、Mass Storage、CDC、Video。你平时用的键盘鼠标、U盘、串口转USB线、摄像头,全在这四个类里。今天咱们一个一个拆开讲。

4.1 HID类——键盘鼠标的「隐形语言」

HID,全称Human Interface Device,人机交互设备。名字挺高大上,其实就是键盘、鼠标、游戏手柄这些东西。我刚开始做USB时,觉得HID协议很简单,不就是发几个按键码吗?后来踩了坑才发现,细节多得很。

4.1.1 HID的「报告描述符」

HID设备最核心的东西,不是数据本身,而是「报告描述符」(Report Descriptor)。它告诉主机:我要发什么数据,每个字节是什么含义。

举个例子,一个标准键盘的报告描述符,大概长这样:

0x05, 0x01,        // Usage Page (Generic Desktop)
0x09, 0x06,        // Usage (Keyboard)
0xA1, 0x01,        // Collection (Application)
0x05, 0x07,        //   Usage Page (Key Codes)
0x19, 0xE0,        //   Usage Minimum (224)
0x29, 0xE7,        //   Usage Maximum (231)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x08,        //   Report Count (8)
0x81, 0x02,        //   Input (Data,Var,Abs)
0x19, 0x00,        //   Usage Minimum (0)
0x29, 0x65,        //   Usage Maximum (101)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x65,        //   Logical Maximum (101)
0x75, 0x08,        //   Report Size (8)
0x95, 0x06,        //   Report Count (6)
0x81, 0x00,        //   Input (Data,Array)
0xC0               // End Collection

这段描述符定义了键盘的输入报告:8个字节的修饰键(Ctrl、Shift等),6个字节的按键码。嗯,这里要注意,键盘最多同时按6个键,不是随便按的。

我的经验: 写HID报告描述符时,最容易犯的错误是Logical Minimum和Logical Maximum设置不对。我曾经有个项目,鼠标的X轴移动范围设成了0-255,结果鼠标在屏幕上只能往右走,往左走不了。查了两天才发现,应该设成-127到127。

4.1.2 键盘与鼠标的数据格式

键盘的报告格式很简单:

字节 含义
0 修饰键(Ctrl=0x01, Shift=0x02, Alt=0x04, Win=0x08)
1 保留(必须为0)
2-7 按键码(最多6个同时按下)

鼠标的报告格式稍微复杂点:

字节 含义
0 按键状态(bit0=左键, bit1=右键, bit2=中键)
1 X轴位移(有符号数)
2 Y轴位移(有符号数)
3 滚轮(有符号数)

你想想看,鼠标移动的数据是有符号的,正数往右/往下,负数往左/往上。这个设计很巧妙,因为鼠标是相对定位设备,不是绝对定位。

4.2 Mass Storage类——U盘的「文件系统」

Mass Storage类,说白了就是U盘、移动硬盘这些东西。它用的协议叫Bulk-Only Transport(BOT),就是通过批量端点传数据。

我记得第一次做U盘项目时,以为只要把数据写到Flash里就行了。结果发现,USB主机根本不关心你用什么存储介质,它只认SCSI命令。

4.2.1 SCSI命令集

Mass Storage设备本质上是一个SCSI命令的翻译器。主机发SCSI命令过来,设备执行后返回结果。常用的SCSI命令有:

  • INQUIRY:查询设备信息(厂商、产品名、版本)
  • READ CAPACITY:读取总容量
  • READ (10):读取指定扇区
  • WRITE (10):写入指定扇区
  • TEST UNIT READY:检查设备是否就绪

一个典型的READ命令格式:

CBW(命令块包裹):
  dCBWSignature: 0x43425355
  dCBWTag: 0x00000001
  dCBWDataTransferLength: 0x00000200
  bmCBWFlags: 0x80(设备到主机)
  bCBWLUN: 0x00
  bCBWCBLength: 10
  CBWCB: READ(10)命令内容
    OP Code: 0x28
    LBA: 起始逻辑块地址
    Transfer Length: 要读的扇区数
避坑指南: 我曾经遇到一个U盘,在Windows上能正常读写,但在Mac上就识别不了。查了半天,发现是INQUIRY命令返回的厂商字符串里有个非法字符。Mac对字符串格式要求很严格,Windows反而很宽容。从那以后,我所有字符串都只用ASCII可打印字符。

4.2.2 文件系统与扇区

Mass Storage协议只负责扇区级的读写,不关心文件系统。FAT32、NTFS、exFAT这些,都是主机端的事。设备只需要做到:主机说读扇区100,我就把扇区100的数据返回去。

但有个细节要注意:扇区大小。标准U盘用512字节扇区,但大容量设备(超过2TB)需要用4K扇区。这个在READ CAPACITY命令里要返回正确值。

4.3 CDC类——虚拟串口的「伪装术」

CDC类,全称Communication Device Class。最常见的应用就是USB转串口,比如CH340、CP2102这些芯片。它让USB看起来像是一个传统的RS232串口。

我个人觉得CDC类是最容易实现的USB类协议,因为它不需要处理复杂的文件系统或报告描述符。但坑也不少。

4.3.1 抽象控制模型(ACM)

CDC类分两个子类:

  • ACM(Abstract Control Model):虚拟串口,最常用
  • EEM(Ethernet Emulation Model):虚拟网卡

ACM设备需要两个接口:

  1. 通信接口(Control Interface):用于设置波特率、数据位等参数
  2. 数据接口(Data Interface):用于收发实际数据

通信接口使用控制端点(端点0),数据接口使用批量端点。数据格式很简单:发什么就收什么,没有额外协议头。

4.3.2 波特率设置与流控

设置波特率时,主机发送SET_LINE_CODING请求:

struct line_coding {
    uint32_t dwDTERate;   // 波特率,如115200
    uint8_t  bCharFormat; // 数据位:0=5, 1=6, 2=7, 3=8
    uint8_t  bParityType; // 校验:0=None, 1=Odd, 2=Even
    uint8_t  bDataBits;   // 停止位:0=1, 1=1.5, 2=2
};

嗯,这里要注意,很多初学者会把波特率设成固定值。但实际上,主机可能会随时改变波特率,设备必须能动态响应。

我的经验: 做CDC设备时,一定要实现SET_CONTROL_LINE_STATE请求。这个请求用来控制RTS和DTR信号。我有个项目没实现这个,结果在Linux下串口工具打不开,因为Linux依赖DTR信号来判断设备是否就绪。

4.4 Video类——摄像头的「数据洪流」

Video类,全称USB Video Class(UVC)。摄像头、视频采集卡都用这个协议。它最大的特点就是数据量大,对实时性要求高。

我记得第一次做UVC摄像头项目时,被数据量吓了一跳。一个1080p30帧的摄像头,每秒要传将近1.5Gbps的原始数据。USB 2.0的480Mbps根本扛不住,所以必须用压缩。

4.4.1 视频流格式

UVC支持多种视频格式:

格式 描述 典型带宽
MJPEG Motion JPEG,每帧独立压缩 中等
H.264 高效视频编码,帧间压缩
YUY2 未压缩的YUV 4:2:2格式 极高
NV12 YUV 4:2:0格式,常用于硬件编码

为什么会有这么多格式?说白了就是带宽和画质的权衡。YUY2画质最好,但USB 2.0下只能跑VGA分辨率。MJPEG画质稍差,但能跑1080p。

4.4.2 等时传输与帧同步

Video类使用等时传输(Isochronous Transfer),因为它对实时性要求高,可以容忍偶尔丢包,但不能容忍延迟。

等时传输的帧结构:

每个USB帧(1ms)内包含多个等时事务:
  - 事务1: 视频数据包(含帧头)
  - 事务2: 视频数据包
  - ...
  - 事务N: 视频数据包(含帧尾)

帧头标记:
  - FID(Frame ID):0/1交替,表示新帧开始
  - EOF(End of Frame):1表示当前帧结束
避坑指南: 我曾经做过一个UVC摄像头,在Windows下用AMCap软件能正常显示,但在Linux下用V4L2就花屏。查了三天,发现是帧头里的FID位没有正确翻转。Windows的UVC驱动比较宽容,会自动修正,但Linux的V4L2严格按照规范来,FID不翻转就认为丢帧了。

4.4.3 视频控制与参数调节

UVC除了传视频数据,还支持控制功能,比如:

  • 亮度(Brightness):范围0-255
  • 对比度(Contrast):范围0-10
  • 饱和度(Saturation):范围0-200
  • 自动曝光(Auto Exposure):开关控制

这些控制通过视频控制接口(VC Interface)实现,使用控制端点发送请求。每个控制项都有对应的Unit和Terminal,主机通过查询这些Unit来获取支持的控制范围。

你想想看,一个摄像头可能有几十个可调参数,但并不是所有参数都支持。所以UVC协议要求设备上报自己的控制能力,主机再根据能力来显示调节界面。

小结

这四个类协议,每个都有自己的特点:

  • HID:靠报告描述符定义数据格式,灵活但容易出错
  • Mass Storage:用SCSI命令操作扇区,简单但要注意兼容性
  • CDC:模拟串口,实现简单但流控细节多
  • Video:数据量大,等时传输,帧同步是关键

我个人建议,初学者先从CDC类入手,因为它最简单,能快速看到效果。然后做HID类,理解报告描述符。接着做Mass Storage,掌握SCSI命令。最后挑战Video类,处理大数据量和实时性问题。

嗯,今天就讲到这里。下一章咱们聊聊USB的电源管理和枚举过程,这两个是调试USB设备时最常遇到的问题。