1. 课程导入
做 USB 虚拟串口,说白了就是让 USB 看起来像一根串口线。但在这之前,你得先搞懂 USB 到底是怎么工作的。我刚开始接触 USB 时,也被那一堆描述符搞得头晕。后来调了几个项目,才慢慢摸清门道。
这一节,咱们把 USB 最核心的几个概念过一遍。不扯太深,够用就行。
1.1 USB 总线拓扑结构
USB 的拓扑结构,其实就是一个树形结构。一个 Host(主机)下面挂一个 Root Hub,Root Hub 下面可以接设备,也可以接 Hub 再扩展。最多支持 127 个设备,包括 Hub 本身。
关键点:USB 通信永远是 Host 发起的。设备不能主动给 Host 发数据,只能等 Host 来问。这一点和串口完全不同,很多新手在这里栽跟头。
我画了一张拓扑图,帮你直观理解:
我的经验:调试 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 通过枚举过程,一层层读取描述符,才知道你是什么设备、怎么用。
描述符的层级关系是这样的:
- 设备描述符:描述整个设备的基本信息,比如 VID、PID、设备版本。
- 配置描述符:描述一种配置模式,包括供电方式、最大电流等。
- 接口描述符:描述一个功能接口。CDC 设备通常有两个接口:一个用于控制管理,一个用于数据收发。
- 端点描述符:描述具体的通信端点,包括端点号、方向、传输类型、最大包长。
我画了一张描述符的层级图,帮你理清关系:
嗯,这里要注意: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 设备的过程。大致步骤如下:
- Host 检测到设备插入,复位设备。
- Host 发送 Get_Descriptor(Device) 请求,读取设备描述符。
- Host 再次复位,并设置地址(Set_Address)。
- Host 读取配置描述符(包括接口和端点描述符)。
- Host 选择配置(Set_Configuration)。
- 设备进入配置状态,可以开始通信。
对于 CDC 设备,枚举完成后,Host 还会加载 CDC 驱动,然后虚拟出一个 COM 口。这个过程在 Windows 上需要 CDC 驱动(usbser.sys),在 Linux 上通常自带 cdc_acm 驱动。
我曾经踩过的坑:有一次枚举总是失败,抓包发现设备描述符的 bMaxPacketSize0 字段填错了。USB 2.0 全速设备这个值必须是 8、16、32 或 64,我填了个 128,Host 直接不认。所以描述符里的每个字节都不能马虎。