CH340不是“插上就能用”的黑盒子:一次真实的USB串口通信解剖实验
你有没有过这样的经历?
把CH340转接板插进电脑,dmesg里确实打印了ttyUSB0,但一发AT指令,目标设备毫无反应;
或者用minicom连上后能收不能发,换个波特率就满屏乱码;
更常见的是——拔掉再重插,设备节点从/dev/ttyUSB0变成了/dev/ttyUSB1,脚本直接崩掉。
这时候,很多人第一反应是换根线、换台电脑、重装驱动……其实问题往往不在硬件,而在于我们对CH340的理解,还卡在“它是个USB转TTL的桥芯片”这个模糊印象里。它到底怎么和Linux对话?内核凭什么认出它是串口而不是U盘?tcsetattr()调用后,那串“115200”究竟变成什么信号写进了芯片?本文不讲API用法,不堆概念术语,而是带你亲手拆开CH340通信链路的每一层封装,从USB包结构到寄存器配置,从URB提交到TTY缓冲区调度,还原一个真实可验证、可调试、可复现的底层工作现场。
它不是“即插即用”,而是被内核一层层“认出来”的
CH340插入USB口的瞬间,Linux内核做的第一件事,根本不是分配ttyUSB0,而是像海关查护照一样,逐字比对它的USB描述符。
打开终端执行:
lsusb -v -d 1a86:7523 2>/dev/null | grep -A5 "Interface Descriptor"你会看到类似这样的关键字段:
bInterfaceClass 2 CDC Communications bInterfaceSubClass 2 Abstract Control Model bInterfaceProtocol 1 AT commands这三行就是CH340的“身份ID”。Linux内核的usbcore模块拿到这些数据后,会遍历所有已注册的USB驱动,查找谁声明了支持Class=0x02, SubClass=0x02, Protocol=0x01——答案是drivers/usb/serial/ch341.c中的ch341_device_ids[]表。注意:驱动文件名叫ch341.c,但它同时支持CH340/CH341/CH342,这是历史命名惯性,别被名字带偏。
真正决定驱动是否加载的,是内核编译时的配置项:
zcat /proc/config.gz | grep CONFIG_USB_CH341 # 输出 CONFIG_USB_CH341=m 表示以模块形式存在如果输出为空,说明你的内核压根没编译这个驱动——此时无论插多少块CH340,dmesg里都只会显示“New USB device found”,却不会出现“ch341-uart converter now attached”。
✅实战验证点:拔掉CH340,运行
sudo modprobe -r ch341 usbserial卸载驱动;再插回设备,观察dmesg | tail -10是否重新触发绑定流程。这是确认驱动行为是否受控的第一步。
驱动初始化干了什么?不是“注册设备”,而是“搭好四条通道”
ch341_probe()函数是整个通信链路的起点。它不负责处理数据,只做一件事:为后续数据流动准备好基础设施。这个过程可以拆解为四个不可跳过的动作:
1. 端点地址不是猜的,是解析出来的
CH340在USB描述符中明确声明了三个端点:
-INTERRUPT IN(端点0x81):用于上报DTR、RTS电平变化、线路状态(如DSR、DCD)
-BULK OUT(端点0x02):主机向CH340发送串口数据(TX方向)
-BULK IN(端点0x83):CH340向主机上传串口数据(RX方向)
驱动通过usb_find_common_endpoints()遍历接口的endpoint[]数组,把这三个地址提取出来并存入struct usb_serial_port的bulk_in_endpointAddress等字段。没有这一步,后面所有读写都会因端点地址错误而超时失败。
2. TTY端口不是“创建”的,而是“挂载”的
Linux的串口抽象统一走tty_port框架。ch341_probe()会为每个物理串口端口(CH340最多支持2路UART)分配一个struct usb_serial_port,然后调用:
tty_port_register_device(&port->port, serial->type, port->number, &interface->dev);这行代码才是真正让/dev/ttyUSB0出现在文件系统里的关键。它本质是把usb_serial_port和内核TTY子系统的设备模型绑定,后续所有open()/write()/read()系统调用,都会经由这个桥梁路由到驱动的回调函数。
3. 波特率不是“设置”的,而是“烧写”的
CH340不支持标准CDCSET_LINE_CODING请求中的全部字段(比如它忽略bCharFormat和bParityType)。驱动必须绕过协议,用厂商自定义请求CH341_REQ_WRITE_REG直接操作内部寄存器。
核心换算公式藏在ch341_set_baudrate()函数里:
divisor = (48000000 + baudrate * 8) / (baudrate * 16); // 例如:115200 → divisor ≈ 26然后将divisor & 0xff写入寄存器0x13,(divisor >> 8) & 0xff写入0x12。
这意味着:如果你用stty -F /dev/ttyUSB0 921600设置波特率,驱动会尝试计算 divisor≈3,但CH340实际支持的最低 divisor 是 2(对应 1.5Mbps),超出范围就会静默失败——此时dmesg里没有任何报错,但串口就是不通。
⚠️坑点与秘籍:CH340G官方手册标明支持最高2Mbps,但实测在Linux下稳定工作的上限普遍是921600。若需更高速率,务必检查
ch341.c中ch341_set_baudrate()的 divisor 边界判断逻辑,并确认你的CH340批次是否为G版(部分T版不支持高速模式)。
4. 数据通路不是“自动通”的,而是靠URB“推着走”的
URB(USB Request Block)是USB数据传输的载体。CH340驱动在初始化时会预分配一组read_urbs(默认4个),每个URB的transfer_buffer指向一块64字节的DMA内存,并提交给USB Core等待CH340上传数据。
关键在于:URB一旦提交,就进入“等待完成”状态;只有当CH340真的发来数据、USB控制器收到并填充buffer后,内核才会触发ch341_read_bulk_callback(),把数据从URB buffer拷贝进TTY的flip buffer,再唤醒等待read()的用户进程。
如果应用层read()太慢(比如用阻塞式fscanf()一行行读),flip buffer溢出,数据就丢了——这就是所谓“大数据量丢包”的真相,和USB带宽无关,纯粹是软件消费速度跟不上硬件生产速度。
用户空间看到的/dev/ttyUSB0,背后是三层缓冲区在接力
当你执行echo "HELLO" > /dev/ttyUSB0,数据并非直通CH340,而是流经以下三级缓冲:
| 缓冲层 | 位置 | 容量 | 控制方式 |
|---|---|---|---|
| 用户空间缓冲 | glibc的stdiobuffer | 默认8KB | setvbuf()可控,fflush()强制刷新 |
| TTY线路规程缓冲 | 内核tty_struct->ldisc->receive_buf | 可配置(stty -icanon关闭行缓存) | termios.c_iflag控制(如ICANON,IEXTEN) |
| CH340硬件FIFO | 芯片内部TX FIFO | 固定64字节 | 无法软件调节,依赖驱动及时取走数据 |
最典型的陷阱就发生在第二层:
默认stty配置开启icanon(规范模式),意味着内核会等你输入回车(\n)才把整行数据交给驱动。此时你执行echo -n "AT"(无换行),数据会卡在TTY线路规程层,永远发不出去。
✅ 正确做法是关闭规范模式并设置最小读取长度:
stty -F /dev/ttyUSB0 115200 -icanon min 1 time 0 # -icanon:禁用行缓存;min 1:至少收到1字节就返回;time 0:不等待超时此时read()调用会立即返回可用字节数,驱动才能及时把数据打包进URB发往CH340。
真正的调试武器:不用抓硬件,也能看见USB包在飞
遇到通信异常,别急着换线或重刷固件。Linux提供了两套原生工具,让你在不依赖示波器、不打开逻辑分析仪的情况下,看清USB总线上的每一个字节:
▶usbmon:内核级USB协议嗅探器
启用监控:
sudo modprobe usbmon sudo cat /sys/kernel/debug/usb/usbmon/1u > /tmp/usbmon.log & # 1u 表示监控USB总线1上的所有URB(u=USB,p=PHY)然后执行你的串口操作(如echo "AT" > /dev/ttyUSB0),停止捕获并分析:
sudo kill %1 # 查看OUT传输(主机→CH340) grep "C Bi" /tmp/usbmon.log | grep " 2:" | head -5 # 输出示例:ffff88810e2b7000 1015611526 C Bi:1:005:2 0 2 = 4154 # 4154 即 ASCII "AT" 的十六进制▶udevadm monitor:跟踪设备生命周期事件
当CH340反复插拔导致ttyUSB*编号漂移时,用它看内核如何识别设备:
sudo udevadm monitor --subsystem-match=tty --property # 插入设备,你会看到: # ID_VENDOR_ID=1a86 # ID_MODEL_ID=7523 # DEVNAME=/dev/ttyUSB0 # SYMLINK=ttyCH340_0这正是你写udev规则的依据——不要硬编码ttyUSB0,而要根据ID_VENDOR_ID和ID_MODEL_ID动态生成稳定符号链接。
最后一句大实话:CH340的稳定性,80%取决于你的软件节奏
很多工程师抱怨CH340“容易丢数据”,但翻遍ch341.c源码,你会发现它几乎没有中断处理逻辑,不依赖DMA,也不做复杂的状态机。它的设计哲学就是:简单、确定、可预测。
所以真正的瓶颈从来不在CH340本身,而在于:
- 用户空间程序是否以足够快的频率read(),避免内核flip buffer溢出;
- 是否正确配置termios,避免数据卡在线路规程层;
- URB提交策略是否合理(CH340默认4个read_urb,对高吞吐场景可能不够);
- 供电是否干净(CH340对VCC噪声敏感,劣质USB线易引发波特率失锁)。
下次再遇到CH340通信异常,别先怀疑芯片坏了。打开dmesg看驱动是否加载成功,用lsusb -v核对描述符,跑一遍usbmon抓包,最后检查你的stty配置——这四步做完,90%的问题都能定位到具体环节。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。