feat: 浏览器端 CH340/CH341 WebUSB 驱动 + navigator.serial 垫片

- ch340-webusb.js: WebUSB 上完整的 USB-Serial 栈
  · CH340/CH341 vendor 控制传输初始化(对齐 Linux ch341.c)
  · 波特率分频算法 calcCh34xBaud(1532620800 基准)
  · bulk IN 32 深读队列 + ReadableStream / 4096 分片写 + WritableStream
  · DTR/RTS(0xA4 取反写入)、拔插事件、诊断计数
  · 附带 CDC-ACM(CH343/CH9102/CH347)、CP210x、FTDI(含 modem 字节剥离)、PL2303(HXN/HX/01/T)
- ch340-serial-shim.js: 安卓上补出 navigator.serial,老代码零改动可用
- ch340-test.html: 连接/收发/HEX/DTR-RTS/回环自测 测试台
- docs/ch34x-registers.md: 寄存器与控制传输逆向说明
- test/driver.test.mjs: 15 项 mock 断言(node,无需硬件)
This commit is contained in:
2026-09-23 00:35:13 +08:00
commit 7af53efaa9
9 changed files with 1854 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
# CH34x 寄存器与初始化序列(逆向 + 内核对照)
本驱动没有用到任何私有 SDK:全部控制传输都对齐 Linux 内核 `drivers/usb/serial/ch341.c`
以及 bbamorachi 站点(tools.bbamorachi.us)前端 bundle 中的实现(两者一致)。
## 设备标识
| 芯片 | VID | PID | 接口类 | 浏览器端驱动 |
|---|---|---|---|---|
| CH340 / CH340G / CH340C / CH340B | `0x1A86` | `0x7523` | `0xFF` vendor-specific | `ch34x` |
| CH341 / CH341A(串口模式) | `0x1A86` | `0x5523` | `0xFF` | `ch34x` |
| 部分 CH341A 克隆 | `0x1A86` | `0x5512` / `0x7522` | `0xFF` | `ch34x`(默认接受) |
| CH343 / CH9102 / CH347(CDC 模式) | `0x1A86` | `0x55D2…0x55E7` | CDC-ACM | `cdcAcm` |
CH340 只有 **一个接口、两个 bulk 端点**(OUT `0x02`、IN `0x82`,wMaxPacketSize = 32)。
端点号不写死,由 `layout()` 从配置描述符里找:"第一个同时带 bulk IN 和 bulk OUT 的接口"。
## 控制传输(vendor / device)
| 请求 | 名称 | 作用 |
|---|---|---|
| `0x5F` | READ_VERSION | 读 2 字节版本,第 2 字节必须为 0,否则芯片型号/固件不对 |
| `0x95` | READ_REG | 读寄存器:`value` = 寄存器地址,长度 2 |
| `0x9A` | WRITE_REG | 写寄存器:`value` = 寄存器地址,`index` = 数据 |
| `0xA1` | SERIAL_INIT | 串口初始化 / modem 控制(`0x501F,0xD90A` = 关流控) |
| `0xA4` | MODEM_CTRL | DTR/RTS,**位取反写入**:`~((rts?0x40:0)|(dtr?0x20:0))` |
关键寄存器:
| 寄存器 | 含义 |
|---|---|
| `0x1312` | 波特率分频高位(含预分频标志位) |
| `0x0F2C` | 波特率分频低位 |
| `0x2518` | LCR:`0xC3` = 8 数据位 + 1 停止位 + 无校验 + bit7 允许写分频 |
| `0x0706` | 状态寄存器(初始化时读一下做同步) |
| `0xFFFF` / `0xFF9F`(作为 `0xA4` 的 value) | modem 控制字,见上 |
## 波特率算法
```js
factor = Math.floor(1532620800 / baud);
ps = 3;
while (factor > 65520 && ps > 0) { factor >>= 3; ps -= 1; }
if (factor > 65520) throw new Error('Unsupported CH34x baud rate');
factor = 65536 - factor;
ps |= 0x80; // 预分频标志
write(0x9A, 0x1312, (factor & 0xFF00) | ps); // 高位
write(0x9A, 0x0F2C, factor & 0xFF); // 低位
```
常用值的计算结果(已由 `test/driver.test.mjs` 断言):
| 波特率 | 0x1312 | 0x0F2C |
|---|---|---|
| 9600 | `0xB282` | `0x0C` |
| 115200 | `0xCC83` | `0x08` |
| 460800 | `0xF383` | `0x02` |
基准时钟 1532620800 Hz = 48 MHz × 32(`CH341_BAUDBASE_FACTOR`),与内核一致。
## 完整初始化序列(本驱动实现)
```
1 0x5F 读版本(2B) → [1] 必须为 0
2 0xA1 0000 0000 → 清 modem 控制
3 0x9A 1312 <9600 factor> → 先按 9600 配置(芯片上电默认收发格式)
4 0x9A 0F2C <9600 divisor>
5 0x95 2518 (读 2B) → 读回 LCR 做同步
6 0x9A 2518 00C3 → 写 LCR = 8N1
7 0x95 0706 (读 2B)
8 0xA1 501F D90A → 关硬件流控
9 0x9A 1312 <目标波特率 factor>
10 0x9A 0F2C <目标波特率 divisor>
11 0xA4 FFFF 0000 → DTR/RTS 复位
12 0x95 0706 (读 2B)
13 0x9A 1312 <目标波特率 factor> ← 与 9/10 重复一次,确保切换生效
14 0x9A 0F2C <目标波特率 divisor>
15 0x9A 2518 00C3
16 0xA4 FF9F 0000 → DTR+RTS 有效(`~0x0060`)
```
共 16 次控制传输,测试里逐条比对。
## CDC-ACM(CH343 / CH9102 / CH347 CDC 模式)
标准 USB CDC 请求,不用厂商私有寄存器:
| 请求 | 名称 | 数据 |
|---|---|---|
| `0x20` (class/interface) | SET_LINE_CODING | 7 字节:波特率 uint32 小端 + 停止位 + 校验 + 数据位(`00 00 08` = 8N1) |
| `0x22` (class/interface) | SET_CONTROL_LINE_STATE | `0x0003` = DTR+RTS,`0x0000` = 全部释放 |
## 数据通道
- **读**:`transferIn(epIn, packetSize)`,默认预排队 **32** 个请求(`readQueueDepth`),
通过 `ReadableStream` 暴露;每个 transfer 完成即补一个,安卓上不容易丢数据。
- **写**:`transferOut(epOut, chunk)`,按 **4096 字节**分片,检查 `bytesWritten` 保证有进展。
- open 时如果 `device.configuration` 为空,先 `selectConfiguration(configurations[0])`;
然后 `claimInterface`,非 0 的 alt setting 需要 `selectAlternateInterface`。
## 为什么安卓不用装驱动
CH340 的接口类标志是 `0xFF`(vendor-specific),安卓/Linux 内核**不会**给它绑定
`cdc_acm` / `usbserial`,所以设备是"空闲"的,浏览器(用户态)可以 `claimInterface`
并直接收发。CDC-ACM 类设备就没这么幸运:个别 ROM 上内核会先抢走,此时
`claimInterface` 会失败——驱动里把这种失败翻译成了中文提示。