CH340在Linux下的驱动加载实战:从识别到通信的完整路径
你有没有遇到过这样的场景?手头一块STM32开发板、ESP32模块,或是自己画的PCB小板子,通过一个小小的CH340转串芯片连上电脑,结果/dev/ttyUSB0死活不出现?插拔无数次,dmesg翻来覆去只看到“new full-speed USB device”,却不见驱动绑定——别急,这正是我们今天要彻底解决的问题。
CH340作为嵌入式世界里最“平民化”的USB转串芯片,几乎无处不在。它便宜、稳定、外围电路简单,但偏偏在某些定制Linux系统中“水土不服”。本文将带你从硬件识别到内核模块加载,再到串口通信验证,走完一趟完整的CH340驱动调试之旅。不只是告诉你“怎么做”,更要讲清楚“为什么”。
为什么CH340有时“用不了”?
现代主流Linux发行版(如Ubuntu 20.04+、Debian 11+)早已把CH340的驱动——准确说是ch341.ko模块——编译进内核或作为默认可加载模块。但一旦进入以下场景,问题就来了:
- 使用Buildroot、Yocto等工具构建的轻量级嵌入式系统;
- 内核配置时未启用
CONFIG_USB_CH341选项; - 老旧系统(如Ubuntu 14.04)或裁剪过度的工业镜像;
- 交叉编译环境缺少对应内核头文件。
这些情况下,即使CH340被USB子系统正确识别,也因缺乏匹配驱动而无法生成/dev/ttyUSB0,用户程序自然无从下手。
那我们该怎么办?不是去网上随便下个.ko文件强行insmod——那是危险操作。我们要做的是:理解机制、按需编译、安全加载。
CH340到底是什么?别再把它当成“普通串口”
先澄清一个常见误解:CH340并不是一个“串口设备”,而是一个USB设备,它的功能是把USB协议“翻译”成TTL电平的UART信号。
当你把CH340模块插入Linux主机时,发生了一系列自动化流程:
USB枚举开始
主机控制器(xHCI/EHCI)检测到新设备接入,读取其描述符,获取VID(Vendor ID)和PID(Product ID)。对CH340G来说,典型值是:
-idVendor = 0x1a86
-idProduct = 0x7523内核尝试匹配驱动
Linux内核遍历所有注册的USB驱动,查找其.id_table是否包含该VID/PID组合。驱动绑定与TTY创建
一旦匹配成功(比如找到了ch341驱动),内核就会调用.probe()函数初始化设备,并通过TTY子系统创建/dev/ttyUSB0。
整个过程依赖的核心框架,就是usb serial驱动架构。
Linux的usb serial架构:通用模型如何支撑万千芯片
Linux并没有为每种USB转串芯片写一套独立的通信逻辑。相反,它设计了一个高度抽象的usb-serial核心层(位于drivers/usb/serial/usb-serial-core.c),让不同厂商的驱动只需实现特定回调函数即可。
这个架构的关键在于两个结构体:
struct usb_serial_driver { const struct usb_device_id *id_table; // 支持哪些设备 int (*open)(struct tty_struct *tty, struct usb_serial_port *port); void (*close)(struct usb_serial_port *port); int (*set_termios)(struct tty_struct *tty, struct usb_serial_port *port, struct ktermios *old); // ... 其他回调 };以CH340为例,它的驱动文件是ch341.c,虽然名字叫ch341,但实际上同时支持CH340、CH341A等多种型号。
关键代码解析:CH340是怎么被“认出来”的?
static const struct usb_device_id ch341_id_table[] = { { USB_DEVICE(0x1a86, 0x7523) }, /* CH340 */ { USB_DEVICE(0x1a86, 0x5523) }, /* CH341A */ { } /* 结束标记 */ }; MODULE_DEVICE_TABLE(usb, ch341_id_table); static struct usb_serial_driver ch341_device = { .driver = { .owner = THIS_MODULE, .name = "ch341-uart", }, .id_table = ch341_id_table, .num_ports = 1, .open = ch341_open, .close = ch341_close, .set_termios = ch341_set_termios, .attach = ch341_startup, .dtr_rts = ch341_dtr_rts, }; static struct usb_serial_driver *const serial_drivers[] = { &ch341_device, NULL }; module_usb_serial_driver(serial_drivers, ch341_id_table);📌 注意:
module_usb_serial_driver是一个宏,它会自动完成模块的init/exit注册,并将其挂载到usb serial核心框架中。
也就是说,只要你的内核里有这段代码并被编译进去,插入CH340就能自动识别。
实战流程:当系统没有内置驱动时,我们怎么办?
假设你现在面对一台刚刷好的嵌入式主板,插入CH340后发现:
$ dmesg | tail [ +2.123] usb 1-1: new full-speed USB device number 4 using xhci_hcd [ +0.123] usb 1-1: New USB device found, idVendor=1a86, idProduct=7523看到了VID/PID,说明USB层面没问题。接下来检查驱动是否存在:
$ modinfo ch341如果返回“Module ch341 not found”,那就得手动补上了。
第一步:准备编译环境
你需要三样东西:
当前运行内核的版本
bash uname -r # 输出例如:5.15.0-103-generic对应的内核头文件(headers)
bash sudo apt install linux-headers-$(uname -r)内核源码(可选)
如果你是自己编译的内核,需要确保源码路径一致;使用发行版则通常不需要单独下载。
第二步:找到驱动源码位置
大多数情况下,ch341.c已经存在于内核源码树中。你可以直接查看:
find /usr/src -name "ch341.c" 2>/dev/null常见路径为:
/usr/src/linux-headers-$(uname -r)/drivers/usb/serial/ch341.c✅ 提示:即使你没安装完整源码包,只要装了
linux-headers-*,就有足够的信息来编译外部模块。
第三步:编写Makefile进行模块编译
新建一个目录,例如ch341-driver,放入以下Makefile:
obj-m += ch341.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean然后执行:
make你会得到几个文件,其中最关键的是:
ch341.ko—— 可加载的内核模块ch341.mod.c,ch341.mod.o—— 自动生成的元数据
第四步:加载模块并验证
sudo insmod ch341.ko此时再看dmesg:
$ dmesg | tail [ +1.123] ch341: CH341 USB Serial Driver loaded [ +0.001] usbcore: registered new interface driver ch341 [ +2.345] ch341 1-1:1.0: ch341 converter detected [ +0.001] usb 1-1: ch341 converter now attached to ttyUSB0看到了吗?ttyUSB0出现了!
第五步:权限设置与通信测试
默认情况下,只有root才能访问/dev/ttyUSB0。建议将用户加入dialout组:
sudo usermod -aG dialout $USER重启终端后即可免sudo使用串口工具。
测试通信(波特率根据设备调整):
screen /dev/ttyUSB0 115200,cs8,-ixon或使用Python脚本:
import serial ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1) print(ser.readline())如果能收到目标板的启动日志,恭喜你,链路通了!
常见坑点与调试秘籍
❌ 问题1:insmod: ERROR: could not insert module ch341.ko: Invalid module format
这是最常见的错误,原因只有一个:模块与当前内核不兼容。
可能情况包括:
- 编译时使用的内核头文件版本 ≠ 当前运行内核;
- 跨架构编译(x86_64 编译给 ARM 使用)未使用交叉工具链;
- 内核配置差异过大(如开启了CONFIG_MODVERSIONS但模块未签名)。
✅ 解决方法:
- 确保uname -r和linux-headers-*版本完全一致;
- 若为目标平台编译,必须使用对应架构的KERNEL_DIR和交叉编译器。
❌ 问题2:驱动加载成功,但每次插拔设备号递增(ttyUSB0 → ttyUSB1 → ttyUSB2…)
这是因为udev没有为设备建立固定命名规则。
✅ 解决方案:创建udev规则
# /etc/udev/rules.d/99-ch340.rules SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="ttyCH340"重新插拔后,始终可通过/dev/ttyCH340访问设备。
❌ 问题3:能识别,但串口乱码或无法通信
检查以下几点:
- 波特率是否匹配(有些MCU默认是9600,有些是115200);
- 是否启用了流控(RTS/CTS)?可在setserial中关闭;
- 目标板供电不足导致CH340工作异常(尤其是使用USB延长线时);
- 接线错误(TX-RX反接、GND未共地)。
进阶建议:让CH340支持更智能
自动加载:开机即用
编辑/etc/modules文件(Debian系)或创建 systemd module load service,添加一行:
ch341下次启动时自动加载模块。
构建系统集成:Buildroot/Yocto中启用CH340
在Buildroot中:
Device Drivers ---> USB support ---> USB Serial Converter support ---> <*> Winchiphead CH341 Single Port Serial Driver在Yocto中,确保DISTRO_FEATURES包含usb,并在.bbappend中启用配置。
这样生成的固件就原生支持CH340,无需额外干预。
写在最后:掌握底层,才能应对变化
CH340只是一个切入点。真正有价值的是你在这个过程中建立起的认知:
- USB设备是如何被Linux识别的?
- 内核模块如何动态加载?
- TTY子系统与用户空间如何协作?
- 如何安全地编译和部署驱动?
这些能力不仅适用于CH340,也适用于FT232、CP210x、PL2303乃至自定义HID转串设备。
未来哪怕RISC-V开发板满天飞,AIoT设备层出不穷,调试的第一道门,往往还是那个熟悉的串口提示符:“login:”。
而你能做的,就是确保那条通往终端的通道,永远畅通。
如果你在实际项目中遇到CH340或其他USB串口芯片的疑难杂症,欢迎留言交流。我们可以一起拆解dmesg日志、分析lsusb -v输出,甚至反向追踪固件行为。毕竟,每一个“不识别”的背后,都藏着一段等待解读的协议对话。