2. USB基础与CDC类概述

好,咱们正式开始啃 CDC 这块硬骨头。很多工程师一提到 USB CDC,第一反应就是“虚拟串口”。其实 CDC 家族远不止一个虚拟串口那么简单。我个人习惯把 CDC 理解成 USB 世界里的“通信设备万能工具箱”——它定义了设备怎么跟主机交换电话、网络、串口这类数据。

2.1 CDC 类模型:不止一个 ACM

CDC 全称是 Communication Device Class。它下面有好几个子类,每个子类对应一种通信场景。我列个表,你一看就明白:

子类 全称 典型用途
ACM Abstract Control Model 虚拟串口、Modem、AT 指令
ECM Ethernet Control Model USB 转以太网(RNDIS 的前身)
EEM Ethernet Emulation Model 更高效的 USB 网络设备
NCM Network Control Model 高速网络(4G/5G 上网卡)
OBEX Object Exchange 蓝牙文件传输的 USB 版本

咱们这门课主要聚焦 ACM。为什么?因为虚拟串口是嵌入式开发里最常用、也最容易踩坑的 CDC 子类。ECM 和 NCM 我后面会提,但不会深讲——你想想看,一个物联网设备,90% 的场景只需要一个串口跟 PC 通信就够了。

核心认知:CDC ACM 不是“把 UART 接到 USB 上”,而是在 USB 总线上模拟出一个串口设备的行为。主机端看到的 COM 口,其实是 USB 协议栈和驱动共同“演”出来的。

2.2 虚拟串口的通信模型

虚拟串口在 USB 总线上的样子,跟物理串口完全不同。物理串口有 TX、RX、RTS、CTS 这些独立引脚。USB 虚拟串口呢?它只有一对端点:一个 IN 端点(设备→主机),一个 OUT 端点(主机→设备)。

我画了一张图,帮你理清这个模型:

USB CDC ACM 虚拟串口通信模型 主机(Host) USB 主机控制器 CDC ACM 驱动 (虚拟 COM 口) 设备(Device) USB 设备控制器 CDC ACM 固件 (虚拟串口逻辑) USB 总线 IN / OUT 端点对 数据流:双向,通过 IN/OUT 端点传输 控制流 中断端点(通知) 控制端点(配置) 控制流 串口状态变化 波特率/格式设置 数据流走批量端点,控制流走中断端点 + 控制端点——两条路互不干扰

从图里你能看到,虚拟串口的数据流和控制流是分开走的。数据流走批量端点(Bulk),控制流走中断端点(Interrupt)和控制端点(Control)。为什么要分开?我接着往下讲。

2.3 数据流与控制流的分离机制

这是 CDC ACM 最巧妙的设计之一。说白了,就是把“串口里传的数据”和“串口的状态/配置”拆成两条独立的通道。

数据流:只管传字节

数据流走批量端点。批量端点的特点是:保证送达,但不保证实时。USB 主机和设备之间通过 IN/OUT 端点来回搬运串口数据。数据流里只有纯粹的字节,没有控制信息。

我的经验:批量端点一次最大包长通常是 64 字节(全速)或 512 字节(高速)。如果你一次要发超过这个长度的数据,USB 协议会自动分包。我曾经遇到一个坑——发送端没处理好 ZLP(零长度包),导致接收端一直等数据。后面我会专门讲 ZLP 的处理。

控制流:管状态和配置

控制流走两条路:

  • 中断端点:设备主动通知主机串口状态变化。比如 DTR 信号变了、Break 信号来了。中断端点的轮询间隔由描述符里的 bInterval 字段决定。
  • 控制端点(端点 0):主机主动查询或设置串口参数。比如 SetLineCoding(设置波特率、数据位、停止位、校验位)、GetLineCoding、SetControlLineState(设置 RTS/DTR)。

为什么要分离?你想想看:如果数据流和控制流混在一起,主机收到一个包,还得先解析它是数据还是控制命令。解析完了,数据还得重新排队。这既增加了延迟,又让协议栈变得复杂。分离之后,数据流可以全速跑,控制流随叫随到,互不阻塞。

避坑指南:我曾经在一个项目里,把中断端点的 bInterval 设成了 255(最大值)。结果主机收不到串口状态变化通知,导致上位机一直以为设备没连上。后来查了 USB 规范才发现,中断端点的轮询间隔不能太大,全速设备建议不超过 32ms。这个细节,很多 CDC 教程都不会提。

2.4 端点配置速查表

一个标准的 CDC ACM 设备,至少需要三个端点。我整理了一个速查表,你移植的时候可以直接对照:

端点 方向 传输类型 用途 典型最大包长
端点 0 双向 控制 枚举、SetLineCoding、GetLineCoding 64(全速)/ 64(高速)
中断端点 IN(设备→主机) 中断 串口状态通知(SerialState) 8~16 字节
批量 IN 端点 IN(设备→主机) 批量 串口发送数据 64(全速)/ 512(高速)
批量 OUT 端点 OUT(主机→设备) 批量 串口接收数据 64(全速)/ 512(高速)

注意看,中断端点只传 8~16 字节。为什么这么小?因为串口状态就那么几个 bit——DTR、RTS、Break、Ring 信号。用不着大包。批量端点才是真正搬砖的。

2.5 数据流与控制流的交互场景

光说理论太干。我举个实际场景,你感受一下这两条流是怎么配合的:

  1. 主机打开 COM 口:主机通过控制端点发 SetControlLineState,激活 DTR/RTS。设备收到后,知道主机“准备好”了。
  2. 主机设置波特率:主机通过控制端点发 SetLineCoding,告诉设备用 115200、8N1。设备收到后,配置内部 UART(如果有的话)。
  3. 设备发数据:设备把串口数据打包,通过批量 IN 端点发给主机。主机从 COM 口读到这些数据。
  4. 主机发数据:主机往 COM 口写数据,数据通过批量 OUT 端点到达设备。设备从端点 FIFO 里取出数据,转发给内部 UART 或应用层。
  5. 设备通知状态变化:如果设备检测到 Break 信号,通过中断端点发 SerialState 通知主机。主机收到后,应用层可以响应 Break 事件。

整个过程,数据流和控制流各走各的道,互不干扰。这就是 CDC ACM 的精髓。

一句话总结:CDC ACM 虚拟串口 = 批量端点搬数据 + 中断端点报状态 + 控制端点设参数。三条腿走路,稳得很。

嗯,这一章的内容就到这儿。CDC 的模型和分离机制是后面所有移植工作的基础。你把这个搞清楚了,后面写描述符、配端点、调驱动的时候,心里就有底了。