|
| 1 | +--- |
| 2 | +id: xrusb-dev-stack-cdc |
| 3 | +title: CDC |
| 4 | +sidebar_position: 1 |
| 5 | +--- |
| 6 | + |
| 7 | +# CDC 设备协议栈 |
| 8 | + |
| 9 | +本节介绍 XRUSB 的 **USB CDC ACM(虚拟串口)** 设备类实现,重点覆盖: |
| 10 | + |
| 11 | +- 描述符组织方式(IAD + Communication Interface + Data Interface) |
| 12 | +- 端点资源申请、配置与回调分发 |
| 13 | +- CDC ACM 标准类请求处理(Line Coding / Control Line State) |
| 14 | +- Serial State 通知的格式与发送策略 |
| 15 | +- 上层适配(`CDCUart`)与吞吐测试类(`CDCWriteTest` / `CDCReadTest`) |
| 16 | + |
| 17 | +当前 CDC 协议栈由以下头文件构成(源码随仓库提供): |
| 18 | + |
| 19 | +- `cdc_base.hpp`:CDC ACM 通用基类(描述符 / 类请求 / 端点管理 / 回调分发) |
| 20 | +- `cdc_uart.hpp`:CDC ↔ UART 语义适配(对上提供 `LibXR::UART` 的 Read/Write) |
| 21 | +- `cdc_test.hpp`:吞吐测试用类(持续写出 / 持续读入) |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## 组件概览 |
| 26 | + |
| 27 | +### `LibXR::USB::CDCBase` |
| 28 | + |
| 29 | +`CDCBase` 继承自 `DeviceClass`,实现 CDC ACM 的通用部分: |
| 30 | + |
| 31 | +- 端点资源申请与配置(Data IN / Data OUT / Comm IN) |
| 32 | +- IAD + 通信接口 + 数据接口的配置描述符块填充 |
| 33 | +- CDC ACM 标准类请求处理(`GET_LINE_CODING` / `SET_LINE_CODING` / `SET_CONTROL_LINE_STATE`) |
| 34 | +- 通过回调将 **控制线变化(DTR/RTS)**、**线路参数变化(Line Coding)** 通知给上层 |
| 35 | +- 提供收发完成钩子(`OnDataOutComplete` / `OnDataInComplete`)供派生类实现具体数据通路 |
| 36 | + |
| 37 | +`CDCBase` 不直接实现数据通路;派生类需要实现: |
| 38 | + |
| 39 | +```cpp |
| 40 | +virtual void OnDataOutComplete(bool in_isr, ConstRawData& data) = 0; |
| 41 | +virtual void OnDataInComplete(bool in_isr, ConstRawData& data) = 0; |
| 42 | +``` |
| 43 | +
|
| 44 | +回调由端点传输完成触发,并由 `CDCBase` 的静态 trampoline 做 `inited_` 防护后分发。 |
| 45 | +
|
| 46 | +### `LibXR::USB::CDCUart` |
| 47 | +
|
| 48 | +`CDCUart` 在 `CDCBase` 的基础上再继承 `LibXR::UART`,对上层暴露典型串口语义: |
| 49 | +
|
| 50 | +- `Read()`:从主机发来的 OUT 数据中读取 |
| 51 | +- `Write()`:向主机发送 IN 数据 |
| 52 | +- `SetConfig()`:把 UART 配置映射到 CDC Line Coding,并发送一次 Serial State 通知 |
| 53 | +
|
| 54 | +它内部使用 `LibXR::ReadPort` / `LibXR::WritePort` 做软件缓冲与写队列管理,并在端点回调中完成数据入队/出队。 |
| 55 | +
|
| 56 | +### `LibXR::USB::CDCWriteTest` / `LibXR::USB::CDCReadTest` |
| 57 | +
|
| 58 | +两者均派生自 `CDCBase`,用于验证链路吞吐与驱动稳定性: |
| 59 | +
|
| 60 | +- `CDCWriteTest`:当主机发送任意数据时,持续通过 Data IN 回传数据(测试设备 → 主机通路) |
| 61 | +- `CDCReadTest`:持续预装 OUT 端点接收并在完成后立即重启(测试主机 → 设备通路) |
| 62 | +
|
| 63 | +--- |
| 64 | +
|
| 65 | +## 接口与端点布局 |
| 66 | +
|
| 67 | +### 接口(Interface) |
| 68 | +
|
| 69 | +CDC ACM 设备以 **两接口(Communication + Data)** 的方式呈现,并带 IAD(Interface Association Descriptor),便于主机将其识别为一个 CDC 复合功能。 |
| 70 | +
|
| 71 | +- 通信接口(Communication Interface):包含 1 个 Interrupt IN 端点(Notification Endpoint) |
| 72 | +- 数据接口(Data Interface):包含 1 个 Bulk OUT + 1 个 Bulk IN(数据收发) |
| 73 | +
|
| 74 | +`CDCBase::GetInterfaceNum()` 固定返回 `2`,`HasIAD()` 固定返回 `true`。 |
| 75 | +
|
| 76 | +说明: |
| 77 | +
|
| 78 | +- IAD 的 `bFirstInterface` 由 `start_itf_num` 偏移得到 |
| 79 | +- Communication Interface 通常是 class request 的目标接口(`wIndex` 指向该接口号) |
| 80 | +
|
| 81 | +### 端点(Endpoint) |
| 82 | +
|
| 83 | +`CDCBase::Init()` 从 `EndpointPool` 申请并配置以下端点: |
| 84 | +
|
| 85 | +| 端点 | 方向 | 类型 | 典型用途 | |
| 86 | +| -------- | ---- | --------- | --------------------------- | |
| 87 | +| Data OUT | OUT | BULK | 主机 → 设备 数据接收 | |
| 88 | +| Data IN | IN | BULK | 设备 → 主机 数据发送 | |
| 89 | +| Comm IN | IN | INTERRUPT | CDC 通知(Serial State 等) | |
| 90 | +
|
| 91 | +Comm IN 端点最大包大小固定为 16 字节;Serial State 通知本身为 10 字节结构(见下文)。 |
| 92 | +
|
| 93 | +端点号可在构造 `CDCBase` / `CDCUart` / 测试类时指定;默认使用 `Endpoint::EPNumber::EP_AUTO` 由端点池自动分配。 |
| 94 | +
|
| 95 | +### 速度与最大包大小 |
| 96 | +
|
| 97 | +Data IN/OUT 的描述符 `wMaxPacketSize` 取自端点对象的 `MaxPacketSize()`。 |
| 98 | +
|
| 99 | +- Full-Speed Bulk 典型为 64 bytes/packet |
| 100 | +- High-Speed Bulk 典型为 512 bytes/packet(若平台支持 HS) |
| 101 | +
|
| 102 | +实际值以平台 USB 控制器与端点实现为准。协议栈在描述符中写入端点报告的值。 |
| 103 | +
|
| 104 | +--- |
| 105 | +
|
| 106 | +## CDCBase 关键能力 |
| 107 | +
|
| 108 | +### DTR/RTS 控制线状态 |
| 109 | +
|
| 110 | +`CDCBase` 内部维护控制线状态 `control_line_state_`,并提供: |
| 111 | +
|
| 112 | +```cpp |
| 113 | +bool IsDtrSet() const; |
| 114 | +bool IsRtsSet() const; |
| 115 | +``` |
| 116 | + |
| 117 | +当收到类请求 `SET_CONTROL_LINE_STATE` 时,行为为: |
| 118 | + |
| 119 | +- 更新 `control_line_state_`(来自 `wValue`) |
| 120 | +- 返回 ZLP(Zero-Length Packet)确认 |
| 121 | +- 调用 `SendSerialState()` 尝试通过 Comm IN 上报当前串行状态 |
| 122 | +- 触发用户回调 `SetOnSetControlLineStateCallback(cb)`,参数为 `(DTR, RTS)` |
| 123 | + |
| 124 | +工程建议: |
| 125 | + |
| 126 | +- 将 **DTR** 视作“主机串口已打开/准备通信”的关键信号 |
| 127 | +- DTR 断开时避免继续发送,避免上层阻塞或无意义的队列堆积 |
| 128 | + |
| 129 | +### Line Coding(波特率/校验/停止位/数据位) |
| 130 | + |
| 131 | +CDC ACM 的 Line Coding 通过类请求 `SET_LINE_CODING` / `GET_LINE_CODING` 进行读写。 |
| 132 | + |
| 133 | +- `GET_LINE_CODING`:设备返回当前 `line_coding_`(7 字节) |
| 134 | +- `SET_LINE_CODING`:控制传输的数据阶段写入 7 字节 `line_coding_`,随后在 `OnClassData()` 中转换为 `LibXR::UART::Configuration` 并回调上层 |
| 135 | + |
| 136 | +`SET_LINE_CODING` 的数据长度必须为 7 字节,不符合则返回 `ErrorCode::ARG_ERR`。 |
| 137 | + |
| 138 | +#### Line Coding 映射规则 |
| 139 | + |
| 140 | +当前 `CDCBase` 将 CDC Line Coding 映射到 `LibXR::UART::Configuration` 的规则如下: |
| 141 | + |
| 142 | +| CDC 字段 | 取值 | 映射到 UART 配置 | |
| 143 | +| ------------- | ---------- | ---------------------------------------------------- | |
| 144 | +| `dwDTERate` | 任意 | `cfg.baudrate = dwDTERate` | |
| 145 | +| `bCharFormat` | 0 | `stop_bits = 1` | |
| 146 | +| `bCharFormat` | 2 | `stop_bits = 2` | |
| 147 | +| `bCharFormat` | 其他 | 降级为 `stop_bits = 1`(`1.5 stop bits` 目前未实现) | |
| 148 | +| `bParityType` | 1 | `parity = ODD` | |
| 149 | +| `bParityType` | 2 | `parity = EVEN` | |
| 150 | +| `bParityType` | 其他 | 降级为 `NO_PARITY`(Mark/Space 将降级) | |
| 151 | +| `bDataBits` | 5/6/7/8/16 | `data_bits = bDataBits`(透传) | |
| 152 | + |
| 153 | +提示: |
| 154 | + |
| 155 | +- USB CDC 的 Line Coding 在多数桌面 OS 上更多是“协商/提示”,是否真正影响主机侧串口参数取决于驱动策略 |
| 156 | +- 如果用于桥接真实 UART 外设,请以回调参数为准并在外设侧做合法性校验 |
| 157 | + |
| 158 | +### Serial State 通知 |
| 159 | + |
| 160 | +`SendSerialState()` 通过 Comm IN(Interrupt IN)端点向主机发送 Serial State 通知。 |
| 161 | + |
| 162 | +通知结构为 10 字节: |
| 163 | + |
| 164 | +- 8 字节 CDC Notification Header |
| 165 | +- 2 字节 UART state 位图 `serialState` |
| 166 | + |
| 167 | +代码中结构体定义为: |
| 168 | + |
| 169 | +```cpp |
| 170 | +#pragma pack(push, 1) |
| 171 | +struct SerialStateNotification |
| 172 | +{ |
| 173 | + uint8_t bmRequestType; // 固定 0xA1 |
| 174 | + uint8_t bNotification; // 固定 SERIAL_STATE (0x20) |
| 175 | + uint16_t wValue; // 固定 0 |
| 176 | + uint16_t wIndex; // Interface number(Communication Interface) |
| 177 | + uint16_t wLength; // 固定 2 |
| 178 | + uint16_t serialState; // UART state bitmap |
| 179 | +}; |
| 180 | +#pragma pack(pop) |
| 181 | +``` |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +## 回调与执行上下文 |
| 186 | + |
| 187 | +`CDCBase` 对外提供两类上层回调: |
| 188 | + |
| 189 | +- `SetOnSetControlLineStateCallback(LibXR::Callback<bool, bool> cb)` |
| 190 | +- `SetOnSetLineCodingCallback(LibXR::Callback<LibXR::UART::Configuration> cb)` |
| 191 | + |
| 192 | +这些回调的 `Run(in_isr, ...)` 由控制传输处理路径触发;`in_isr` 用于提示调用上下文。 |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## 初始化与资源释放行为 |
| 197 | + |
| 198 | +### Init 行为 |
| 199 | + |
| 200 | +`CDCBase::Init(endpoint_pool, start_itf_num)` 的关键行为: |
| 201 | + |
| 202 | +- 清零 `control_line_state_` |
| 203 | +- 通过 `EndpointPool` 申请三个端点并完成 `Configure` |
| 204 | +- 填充 IAD、Communication Interface、Data Interface 与端点描述符块 |
| 205 | +- 将描述符块通过 `SetData(RawData{...})` 交给设备框架拼入配置描述符 |
| 206 | +- 注册 Data OUT / Data IN 端点传输完成回调 |
| 207 | +- 设置 `inited_ = true` |
| 208 | +- 启动 Data OUT 预接收:`ep_data_out_->Transfer(ep_data_out_->MaxTransferSize())` |
| 209 | + |
| 210 | +提示: |
| 211 | + |
| 212 | +- OUT 端点预接收的长度取决于端点实现的 `MaxTransferSize()`,用于持续接收主机数据 |
| 213 | +- `CDCBase` 不对收到的数据做缓存;派生类需在 `OnDataOutComplete` 中消费并重启 OUT 传输(或按自身策略重启) |
| 214 | + |
| 215 | +### Deinit 行为 |
| 216 | + |
| 217 | +`CDCBase::Deinit(endpoint_pool)` 的关键行为: |
| 218 | + |
| 219 | +- `inited_ = false` |
| 220 | +- 清零 `control_line_state_` |
| 221 | +- 关闭三端点、清零 active length |
| 222 | +- 将端点归还给 `EndpointPool` |
| 223 | +- 置端点指针为空 |
| 224 | + |
| 225 | +派生类或上层适配类在 `Deinit()` 时应确保: |
| 226 | + |
| 227 | +- 终止所有依赖端点对象的异步操作 |
| 228 | +- 对外完成或失败掉未完成的读写请求,避免上层永久等待 |
| 229 | + |
| 230 | +--- |
| 231 | + |
| 232 | +## 使用示例 |
| 233 | + |
| 234 | +### 作为 CDC 虚拟串口使用(推荐:`CDCUart`) |
| 235 | + |
| 236 | +```cpp |
| 237 | +#include "cdc_uart.hpp" |
| 238 | + |
| 239 | +LibXR::USB::CDCUart cdc_uart(/*rx*/256, /*tx*/256, /*tx_queue*/8); |
| 240 | + |
| 241 | +// 设备构造时把 &cdc_uart 放入 class 列表:{{&cdc_uart}} |
| 242 | +// usb_dev.Init(); |
| 243 | +// usb_dev.Start(); |
| 244 | +``` |
| 245 | +
|
| 246 | +可选:监听主机对 Line Coding / DTR/RTS 的变化: |
| 247 | +
|
| 248 | +```cpp |
| 249 | +cdc_uart.SetOnSetLineCodingCallback( |
| 250 | + LibXR::Callback<LibXR::UART::Configuration>( |
| 251 | + [](bool in_isr, LibXR::UART::Configuration cfg) { |
| 252 | + (void)in_isr; |
| 253 | + // 可在此同步到真实 UART 外设(注意 ISR 场景下不要阻塞) |
| 254 | + } |
| 255 | + ) |
| 256 | +); |
| 257 | +
|
| 258 | +cdc_uart.SetOnSetControlLineStateCallback( |
| 259 | + LibXR::Callback<bool, bool>( |
| 260 | + [](bool in_isr, bool dtr, bool rts) { |
| 261 | + (void)in_isr; |
| 262 | + (void)rts; |
| 263 | + // dtr=true 表示主机已打开串口,可开始发送 |
| 264 | + } |
| 265 | + ) |
| 266 | +); |
| 267 | +``` |
| 268 | + |
| 269 | +### 吞吐测试 |
| 270 | + |
| 271 | +写测试: |
| 272 | + |
| 273 | +```cpp |
| 274 | +#include "cdc_test.hpp" |
| 275 | +LibXR::USB::CDCWriteTest cdc_write_test; |
| 276 | +``` |
| 277 | + |
| 278 | +读测试: |
| 279 | + |
| 280 | +```cpp |
| 281 | +#include "cdc_test.hpp" |
| 282 | +LibXR::USB::CDCReadTest cdc_read_test; |
| 283 | +``` |
| 284 | + |
| 285 | +同样通过 USB Device 的 class 列表传入即可。 |
0 commit comments