1. USB 基础概念回顾

做 USB 虚拟串口,说白了就是让 USB 看起来像一根串口线。但在这之前,你得先搞懂 USB 到底是怎么工作的。我刚开始接触 USB 时,也被那一堆描述符搞得头晕。后来调了几个项目,才慢慢摸清门道。

这一节,咱们把 USB 最核心的几个概念过一遍。不扯太深,够用就行。

1.1 USB 总线拓扑结构

USB 的拓扑结构,其实就是一个树形结构。一个 Host(主机)下面挂一个 Root Hub,Root Hub 下面可以接设备,也可以接 Hub 再扩展。最多支持 127 个设备,包括 Hub 本身。

关键点:USB 通信永远是 Host 发起的。设备不能主动给 Host 发数据,只能等 Host 来问。这一点和串口完全不同,很多新手在这里栽跟头。

我画了一张拓扑图,帮你直观理解:

USB 总线拓扑结构 USB Host(主机) Root Hub Hub 1 Hub 2 设备 1(鼠标) 设备 2(CDC) 设备 3(CDC) 设备 4(U盘) 说明 Host 发起所有通信 设备不能主动发数据 最多 127 个设备 树形层级结构

我的经验:调试 CDC 虚拟串口时,如果 Host 端识别不到设备,先检查 Hub 链路是否正常。我曾经遇到一个案子,设备在 Root Hub 上能工作,但经过两级 Hub 后就掉枚举了——最后发现是 Hub 的电源管理问题。

1.2 USB 传输类型

USB 定义了四种传输类型。每种类型适合不同的场景。CDC 虚拟串口主要用 批量传输,但控制传输也绕不开。

传输类型 特点 典型应用 CDC 中是否使用
控制传输 双向、可靠、带宽小 枚举、配置、命令 是(枚举和 CDC 管理请求)
批量传输 双向、可靠、带宽大 U盘、虚拟串口数据 是(数据收发)
中断传输 单向、可靠、低延迟 鼠标、键盘 否(CDC 不用)
同步传输 单向、不可靠、固定带宽 音频、视频 否(CDC 不用)

你想想看,虚拟串口传数据,最关心什么?可靠性和吞吐量。批量传输正好满足这两点。控制传输虽然可靠,但带宽太小,只适合传配置和命令。

注意:批量传输没有固定的带宽保证。如果总线上有其他设备在抢带宽,CDC 的数据传输可能会变慢。我在一个项目里就遇到过,USB 同时接了 CDC 和 U盘,大文件拷贝时虚拟串口的数据就卡住了。

1.3 USB 描述符体系

描述符,说白了就是设备的「身份证」。Host 通过枚举过程,一层层读取描述符,才知道你是什么设备、怎么用。

描述符的层级关系是这样的:

  1. 设备描述符:描述整个设备的基本信息,比如 VID、PID、设备版本。
  2. 配置描述符:描述一种配置模式,包括供电方式、最大电流等。
  3. 接口描述符:描述一个功能接口。CDC 设备通常有两个接口:一个用于控制管理,一个用于数据收发。
  4. 端点描述符:描述具体的通信端点,包括端点号、方向、传输类型、最大包长。

我画了一张描述符的层级图,帮你理清关系:

USB 描述符层级结构 设备描述符(Device Descriptor) VID: 0x1234, PID: 0x5678, bcdUSB: 0x0200 配置描述符(Configuration Descriptor) bMaxPower: 100mA, bNumInterfaces: 2 接口描述符 0(控制) bInterfaceClass: 0x02 (CDC) 接口描述符 1(数据) bInterfaceClass: 0x0A (CDC Data) 端点描述符(EP0 控制) 端点描述符(批量 IN/OUT) EP1 IN, EP2 OUT, wMaxPacketSize: 64

嗯,这里要注意:CDC 设备至少需要两个接口描述符。第一个接口是「通信接口」,用来发控制命令(比如设置波特率、DTR/RTS 信号)。第二个接口是「数据接口」,用来收发实际数据。

核心要点:描述符的顺序不能乱。Host 枚举时,会按照 设备 → 配置 → 接口 → 端点 的顺序逐层读取。如果某个描述符返回错误,枚举就会失败。我调试时,经常用 USB 分析仪抓包,看 Host 读到哪一步卡住了。

1.4 端点与管道

端点(Endpoint)是 USB 设备中最基本的通信单元。每个端点都有一个唯一的地址,由端点号和方向组成。比如 EP1 IN 表示端点 1 的输入方向(设备到 Host)。

管道(Pipe)是 Host 和端点之间的逻辑连接。说白了,管道就是一条数据通道。控制传输走默认管道(EP0),批量传输走批量管道。

CDC 虚拟串口典型的端点配置:

  • EP0:控制端点,双向,用于枚举和 CDC 管理请求。
  • EP1 IN:批量端点,设备→Host,用于发送数据。
  • EP2 OUT:批量端点,Host→设备,用于接收数据。

我的习惯:端点号从 1 开始分配,EP0 保留给控制传输。IN 和 OUT 端点号可以相同,但方向不同。比如 EP1 IN 和 EP1 OUT 是两个不同的端点。不过我个人喜欢用不同的端点号,调试时一眼就能分清方向。

1.5 枚举过程简述

枚举,就是 Host 发现并配置 USB 设备的过程。大致步骤如下:

  1. Host 检测到设备插入,复位设备。
  2. Host 发送 Get_Descriptor(Device) 请求,读取设备描述符。
  3. Host 再次复位,并设置地址(Set_Address)。
  4. Host 读取配置描述符(包括接口和端点描述符)。
  5. Host 选择配置(Set_Configuration)。
  6. 设备进入配置状态,可以开始通信。

对于 CDC 设备,枚举完成后,Host 还会加载 CDC 驱动,然后虚拟出一个 COM 口。这个过程在 Windows 上需要 CDC 驱动(usbser.sys),在 Linux 上通常自带 cdc_acm 驱动。

我曾经踩过的坑:有一次枚举总是失败,抓包发现设备描述符的 bMaxPacketSize0 字段填错了。USB 2.0 全速设备这个值必须是 8、16、32 或 64,我填了个 128,Host 直接不认。所以描述符里的每个字节都不能马虎。