- 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,无需硬件)
4.7 KiB
4.7 KiB
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) |
关键寄存器:
| 寄存器 | 含义 |
|---|---|
0x1312 |
波特率分频高位(含预分频标志位) |
0x0F2C |
波特率分频低位 |
0x2518 |
LCR:0xC3 = 8 数据位 + 1 停止位 + 无校验 + bit7 允许写分频 |
0x0706 |
状态寄存器(初始化时读一下做同步) |
0xFFFF / 0xFF9F(作为 0xA4 的 value) |
modem 控制字,见上 |
波特率算法
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 会失败——驱动里把这种失败翻译成了中文提示。