3. CDC类协议详解
CDC-ACM 协议,说白了就是 USB 虚拟串口背后的那套“规矩”。它定义了主机和设备之间怎么握手、怎么配置、怎么传数据。今天咱们就挑三个最核心的请求来聊:SEND_ENCAPSULATED_COMMAND、SET_LINE_CODING 和 SET_CONTROL_LINE_STATE。
我刚开始做 USB 虚拟串口时,总觉得这些请求是“黑盒”,照着例程抄就完事了。直到有一次设备在 Linux 下死活枚举失败,我才老老实实把协议规范翻出来啃了一遍。嗯,从那以后我再也不敢小看这些“小请求”了。
3.1 SEND_ENCAPSULATED_COMMAND:封装命令的“快递员”
这个请求的名字听起来挺唬人,其实它的作用很简单——主机往设备发一条“封装好的命令”。它主要用于那些需要额外控制通道的 CDC 设备,比如调制解调器(Modem)或者一些工业现场总线适配器。
核心要点:
- 请求类型:Class-specific,方向是 Host to Device
- 主要用途:传输设备特定的控制命令,比如 AT 指令、固件升级命令等
- 数据长度:由 wLength 字段指定,最大不超过控制端点能承载的大小(通常是 64 字节或 256 字节)
我记得有一次调试一个 4G 模块的虚拟串口,主机通过 SEND_ENCAPSULATED_COMMAND 发送 AT 指令来配置模块。结果发现指令总是超时,抓包一看——主机发的命令长度是 80 字节,而我的设备控制端点最大包长只设了 64 字节。数据被截断了,模块自然不响应。
实战建议:如果你的设备不需要这个功能,可以在接口描述符中将 bmCapabilities 的 D0 位设为 0,这样主机就不会发这个请求了。省心省力。
3.2 SET_LINE_CODING:串口参数的“配置中心”
这个请求是 CDC-ACM 里最常用的,没有之一。它用来设置虚拟串口的波特率、数据位、停止位、校验位。说白了,就是主机告诉设备:“嘿,我要用 115200 8N1 了,你准备好。”
它的数据结构长这样:
struct usb_cdc_line_coding {
__le32 dwDTERate; // 波特率,单位 bps
__u8 bCharFormat; // 停止位:0=1位,1=1.5位,2=2位
__u8 bParityType; // 校验:0=无,1=奇,2=偶,3=标记,4=空格
__u8 bDataBits; // 数据位:5, 6, 7, 8
} __attribute__((packed));
这个结构体只有 7 个字节,但每个字段都藏着坑。我一个个说:
| 字段 | 常见值 | 踩坑记录 |
|---|---|---|
| dwDTERate | 9600, 115200, 921600 | 有些主机(比如 Windows)会发 0 作为波特率,设备要能处理 |
| bCharFormat | 0 (1位停止) | 1.5 位停止位很少用,很多设备直接忽略 |
| bParityType | 0 (无校验) | 奇偶校验在高速下容易丢数据,我一般建议客户用无校验 |
| bDataBits | 8 | 5/6/7 位数据位在虚拟串口里几乎绝迹了 |
注意:设备收到 SET_LINE_CODING 后,必须立即更新内部串口参数,并返回 ACK。如果参数不支持(比如波特率 300),建议返回 STALL,而不是默默接受。我曾经见过一个设备,收到不支持波特率后直接死机了……
3.3 SET_CONTROL_LINE_STATE:DTR/RTS 的“开关”
这个请求控制的是虚拟串口的硬件流控信号——DTR(数据终端就绪)和 RTS(请求发送)。主机通过它告诉设备:“我准备好收数据了”或者“我要暂停发送”。
请求的数据只有 2 个字节,但真正有用的只有低 2 位:
- Bit 0 (DTR): 1 表示 DTR 有效,0 表示无效
- Bit 1 (RTS): 1 表示 RTS 有效,0 表示无效
你可能会问:“就这么简单?” 嗯,确实简单。但它的时序意义很大。
举个例子:主机打开串口时,会先发 SET_CONTROL_LINE_STATE(DTR=1, RTS=1),然后才发 SET_LINE_CODING。关闭串口时,会先发 SET_CONTROL_LINE_STATE(DTR=0, RTS=0),再发 CLEAR_FEATURE 来停掉数据端点。
我在做一款工业数据采集器时,就利用了这个时序。设备检测到 DTR 从 1 变 0 时,自动进入低功耗模式。结果发现 Windows 下每次枚举都会先拉高再拉低 DTR,导致设备反复进出低功耗模式。后来加了个 500ms 的消抖才解决。
小技巧:如果你的设备不需要硬件流控,可以在响应 SET_CONTROL_LINE_STATE 时直接忽略 RTS 位,只处理 DTR。很多 USB 转串口芯片(比如 FT232)就是这么干的。
3.4 三个请求的“协作关系”
这三个请求不是孤立的。它们共同构成了虚拟串口的初始化流程。我画了一张图,帮你理清它们的关系:
从图上你能看到,SET_CONTROL_LINE_STATE 和 SET_LINE_CODING 是必选的,而 SEND_ENCAPSULATED_COMMAND 是可选的。主机通常先拉高 DTR/RTS,再配置串口参数,然后才开始收发数据。
3.5 实战中的“潜规则”
最后分享几个我这些年积累的经验:
- 不要假设主机一定会发 SET_LINE_CODING。有些 Linux 驱动在打开串口时只发 SET_CONTROL_LINE_STATE,不发 SET_LINE_CODING。设备应该使用默认参数(通常是 9600 8N1)继续工作。
- SET_LINE_CODING 的响应要快。主机对这个请求的响应时间很敏感,如果设备处理超过 50ms,Windows 可能会报“设备超时”错误。
- SEND_ENCAPSULATED_COMMAND 的数据长度要校验。我见过一些主机发 0 长度的封装命令,设备如果没做长度检查,直接 memcpy 就会出问题。
- DTR/RTS 的状态要持久化。设备掉电重启后,应该恢复 DTR/RTS 为默认状态(通常是低电平),而不是保持上次的值。
总结一下:
- SET_LINE_CODING 是串口参数的“心脏”,必须正确处理
- SET_CONTROL_LINE_STATE 是流控的“开关”,注意时序
- SEND_ENCAPSULATED_COMMAND 是扩展功能的“通道”,按需实现
这三个请求搞明白了,CDC-ACM 协议你就掌握了 80%。剩下的就是数据端点的收发逻辑了,那个咱们后面再聊。