Ubuntu 18.04下CH340/CH341串口驱动安装与编译踩坑指南

发布时间:2026/10/17 14:55:02
Ubuntu 18.04下CH340/CH341串口驱动安装与编译踩坑指南
1. 为什么在 Ubuntu 18.04 上折腾 CH340/CH341 驱动手里攥着一块 Arduino Nano、ESP8266 或者某款国产开发板插上 USB 线满心欢喜地敲下ls /dev/ttyUSB*结果终端冷冷地回你一句No such file or directory。这种场景我遇到过太多次了尤其是刚接触嵌入式开发的朋友硬件没问题、代码没问题卡就卡在这颗不起眼的 USB 转串口芯片上。CH340 和 CH341 是沁恒微电子推出的 USB 总线转接芯片前者主打 USB 转串口后者功能更丰富一些除了串口还能做并口、I2C 等转换。市面上大量低成本开发板、烧录器、USB 转 TTL 模块用的都是这两颗芯片。问题在于Ubuntu 18.04 自带的内核模块里虽然包含了ch341驱动但它的默认行为并不总是能正确识别所有 CH340/CH341 设备尤其是那些 PID 被厂商改过的定制版本或者需要特定波特率支持的场景。这篇文章面向的是在 Ubuntu 18.04 上使用串口设备的开发者、嵌入式爱好者、硬件调试人员。我会把驱动安装的完整流程拆开讲清楚包括内核模块的加载机制、为什么有时候系统自带的驱动不够用、如何编译安装官方驱动、以及那些文档里不会写的踩坑经验。读完你不仅能解决眼前的问题还能理解背后的原理下次遇到类似芯片心里有底。2. 先搞清楚你的设备到底被识别成什么2.1 用 lsusb 确认芯片型号和 VID/PID动手装驱动之前第一步永远是确认系统到底看到了什么。把设备插上 USB 口终端执行lsusb你会看到类似这样的输出Bus 001 Device 004: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter Bus 001 Device 005: ID 1a86:5523 QinHeng Electronics CH341 in serial mode这里的关键信息是1a86:7523和1a86:5523。1a86是沁恒的厂商 IDVID后面的7523是 CH340 的产品 IDPID5523是 CH341 在串口模式下的 PID。如果你看到的 PID 不是这两个比如某些厂商改成了1a86:7522或者其他值那系统自带的驱动可能就认不出来。我见过不少模块用的是 CH340G 或者 CH340CPID 都是7523但有些廉价模块为了规避某些限制会把 PID 改成非标准值。这种情况下lsusb能看到设备但/dev/ttyUSB*就是不出来。2.2 检查内核是否已经加载了 ch341 模块Ubuntu 18.04 默认内核4.15 系列是自带ch341模块的。执行lsmod | grep ch341如果有输出说明模块已经加载了。再进一步看看它绑定了哪些设备ls /sys/bus/usb-serial/drivers/ch341-uart/正常情况下应该能看到类似1-1:1.0这样的目录对应你的设备。如果这里是空的或者根本没有ch341-uart这个目录那说明驱动没有正确绑定。还有一个更直接的检查方法dmesg | grep -i ch34看看内核日志里有没有ch341-uart相关的信息。如果出现ch341-uart: probe of 1-1:1.0 failed with error -110之类的错误那就是驱动尝试绑定但失败了通常和硬件握手或者供电有关。2.3 判断是否需要手动编译驱动什么时候需要自己编译驱动我总结了几种典型情况系统自带的ch341模块版本太老不支持你的设备 PID你需要使用 CH341 的并口模式或者 I2C 模式而默认只加载了串口模式你需要特定的波特率支持比如非标准的 921600 或者更高的速率设备被识别成了ch341但无法正常通信需要更新到最新版驱动如果只是普通的 CH340 串口模块PID 是标准的7523那 Ubuntu 18.04 自带驱动大概率能直接用。但如果你遇到了上面任何一种情况往下看。3. 方案选型内核自带驱动 vs 官方源码编译3.1 内核自带 ch341 模块的局限性Ubuntu 18.04 的 4.15 内核里ch341模块的代码来自主线内核版本相对固定。它的主要问题有几个第一设备 ID 表可能不包含某些厂商自定义的 PID。内核模块在编译时就把支持的 VID/PID 列表写死了遇到不在列表里的设备驱动不会自动绑定。第二默认只支持串口模式。CH341 芯片本身可以工作在并口、I2C 等多种模式但内核模块默认只注册了串口功能。如果你需要其他模式得自己改代码或者用官方驱动。第三波特率支持有限。标准内核驱动支持的波特率是固定的几个值如果你需要 250000、500000 这种非标准波特率某些航模、传感器会用可能会遇到问题。3.2 官方驱动源码的优势沁恒官方提供了 Linux 下的驱动源码虽然代码风格比较“原生态”但胜在更新及时、支持全面。官方驱动的主要优势包含最新的设备 ID 列表支持更多定制 PID可以手动配置工作模式串口/并口/I2C波特率支持更灵活有问题可以直接改代码重新编译当然官方驱动也有缺点代码质量参差不齐编译时可能有警告而且需要每次内核更新后重新编译。所以我的建议是先用系统自带驱动试不行再上官方源码。3.3 两种方案的对比对比项内核自带驱动官方源码驱动安装难度零配置即插即用需要编译安装设备兼容性仅标准 PID支持自定义 PID工作模式仅串口串口/并口/I2C波特率支持标准值可自定义内核更新后自动适配需重新编译稳定性经过主线测试取决于源码质量我的实操建议是先花两分钟检查系统自带驱动能不能用能用就别折腾。如果不行再走源码编译路线。4. 实操从零编译安装官方驱动4.1 准备工作安装编译依赖和内核头文件编译内核模块需要内核头文件版本必须和当前运行的内核完全一致。先确认内核版本uname -r然后安装对应的头文件和编译工具sudo apt update sudo apt install build-essential linux-headers-$(uname -r) git这里有个坑要注意如果你之前更新过内核但没重启uname -r显示的可能是旧版本而linux-headers-$(uname -r)安装的也是旧版本头文件。编译出来的模块加载到新内核上会报version magic错误。所以保险起见先重启一次再操作。提示如果apt找不到对应版本的头文件可能是你的内核版本太新或太旧。执行apt search linux-headers看看有哪些可用版本必要时手动指定。4.2 获取驱动源码并解压官方驱动源码可以从沁恒官网下载文件名通常是CH341SER_LINUX.ZIP之类的。下载后解压unzip CH341SER_LINUX.ZIP -d ch341-driver cd ch341-driver目录里应该能看到ch34x.c、Makefile等文件。先看一眼 Makefile 内容cat Makefile典型的 Makefile 长这样obj-m : ch34x.o KERNELDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean这个 Makefile 的逻辑是调用内核构建系统来编译外部模块。KERNELDIR指向当前内核的构建目录M$(PWD)告诉内核构建系统模块源码在当前目录。4.3 编译过程中的常见报错与修复直接执行make可能会遇到各种报错我把我踩过的几个典型问题列出来。报错一error: implicit declaration of function signal_pending这是内核 API 变更导致的。新版本内核把signal_pending换成了signal_pending(current)的用法或者头文件路径变了。解决办法是在ch34x.c开头加上#include linux/sched/signal.h报错二error: struct tty_struct has no member named termios这是内核 4.15 之后 tty 结构体变更导致的。需要把代码里所有tty-termios改成tty-termios对应的新访问方式通常是tty-termios变成了通过tty-driver_data获取。具体改法要看报错行号一般是把struct ktermios *termios tty-termios;改成struct ktermios *termios tty-termios;等等这里我需要说得更准确一些。在 4.15 内核里tty-termios还是存在的但类型从struct termios变成了struct ktermios。所以如果报错是类型不匹配把变量声明改成struct ktermios *即可。报错三error: unknown type name wait_queue_t内核 4.15 把wait_queue_t改成了wait_queue_entry_t。全局替换一下sed -i s/wait_queue_t/wait_queue_entry_t/g ch34x.c报错四error: passing argument 1 of init_timer from incompatible pointer type内核定时器 API 在 4.15 有变化init_timer被timer_setup取代。这个改动比较大需要把init_timer(dev-timer); dev-timer.function ch34x_timer; dev-timer.data (unsigned long)dev;改成timer_setup(dev-timer, ch34x_timer, 0);同时把定时器回调函数的签名从void (*)(unsigned long)改成void (*)(struct timer_list *)。这些修改听起来麻烦但实际操作就是照着报错一行行改。我建议改之前先备份原始文件cp ch34x.c ch34x.c.bak4.4 加载模块并验证设备节点编译成功后目录里会出现ch34x.ko文件。先卸载系统自带的模块sudo rmmod ch341然后加载新编译的模块sudo insmod ch34x.ko检查是否加载成功lsmod | grep ch34x dmesg | tail -20如果一切正常dmesg里应该能看到类似ch34x: version 1.5和usb 1-1: ch34x converter now attached to ttyUSB0的信息。这时候再执行ls /dev/ttyUSB*应该就能看到设备节点了。4.5 设置开机自动加载insmod加载的模块重启后就没了。要让它开机自动加载需要把模块复制到内核模块目录并更新依赖sudo cp ch34x.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/ sudo depmod -a然后创建配置文件echo ch34x | sudo tee /etc/modules-load.d/ch34x.conf如果希望系统自带的ch341模块不要自动加载避免冲突可以把它加入黑名单echo blacklist ch341 | sudo tee /etc/modprobe.d/blacklist-ch341.conf重启后验证lsmod | grep ch34x ls /dev/ttyUSB*5. 权限配置让普通用户也能访问串口5.1 把用户加入 dialout 组默认情况下/dev/ttyUSB0的属主是root:dialout权限是crw-rw----。普通用户不在dialout组里就没法读写。解决办法sudo usermod -aG dialout $USER执行完后需要注销重新登录或者执行newgrp dialout让当前会话生效。验证groups看看输出里有没有dialout。5.2 用 udev 规则固定设备名如果你同时插多个串口设备ttyUSB0、ttyUSB1的顺序可能会变导致脚本里的设备路径失效。用 udev 规则可以根据设备的序列号或者物理端口固定名称。先获取设备的序列号udevadm info -a -n /dev/ttyUSB0 | grep serial然后创建规则文件sudo nano /etc/udev/rules.d/99-usb-serial.rules写入SUBSYSTEMtty, ATTRS{idVendor}1a86, ATTRS{idProduct}7523, ATTRS{serial}你的序列号, SYMLINKttyMyDevice重新加载规则sudo udevadm control --reload-rules sudo udevadm trigger之后就可以用/dev/ttyMyDevice这个固定路径访问了。注意不是所有 CH340 模块都有唯一序列号有些廉价模块的序列号是空的或者重复的。这种情况下可以用KERNELS或者ATTRS{devpath}来区分物理端口。6. 常见问题排查速查表6.1 设备插上没反应lsusb 都看不到这种情况通常是硬件层面的问题。先换一根 USB 线试试有些线只能充电不能传数据。再换一个 USB 口前置面板的供电可能不足。如果lsusb能看到设备但dmesg里有device descriptor read/64, error -71之类的错误那可能是 USB 信号完整性问题换根短一点的优质线。6.2 lsusb 能看到但 /dev/ttyUSB* 不出现先检查dmesg | grep ch34看看驱动有没有尝试绑定。如果看到ch341-uart: probe failed可能是 PID 不在支持列表里。这时候需要手动在驱动源码的id_table里加上你的 PIDstatic struct usb_device_id id_table[] { { USB_DEVICE(0x1a86, 0x7523) }, { USB_DEVICE(0x1a86, 0x5523) }, { USB_DEVICE(0x1a86, 0x你的PID) }, // 手动添加 {} };改完重新编译加载。6.3 设备节点出现了但无法通信先确认波特率设置是否正确。用stty命令查看stty -F /dev/ttyUSB0 -a如果通信乱码可能是波特率不匹配。CH340 对某些非标准波特率支持不好比如 921600 可能会有较大误差。可以尝试降低到 115200 测试。还有一种情况是流控设置问题。有些设备需要硬件流控RTS/CTS有些不需要。用stty关闭流控stty -F /dev/ttyUSB0 -crtscts6.4 编译时报错找不到头文件确认linux-headers-$(uname -r)已经安装dpkg -l | grep linux-headers如果没有先安装。如果安装了但还是找不到检查/lib/modules/$(uname -r)/build这个软链接是否存在ls -la /lib/modules/$(uname -r)/build如果链接断了重新安装头文件包。6.5 内核更新后驱动失效这是源码编译方案的固有缺陷。每次内核更新后需要重新编译安装驱动。可以写个简单的脚本自动化这个过程#!/bin/bash cd /path/to/ch341-driver make clean make sudo rmmod ch34x 2/dev/null sudo cp ch34x.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/ sudo depmod -a sudo modprobe ch34x把脚本放在方便的位置内核更新后执行一次即可。7. 几个我踩过的坑和实操心得第一个坑是关于rmmod ch341的。如果你已经用系统自带驱动打开了串口比如正在用 minicom 或者 screen直接rmmod会报Module ch341 is in use。这时候需要先关掉所有占用串口的程序或者用fuser /dev/ttyUSB0找到占用进程再处理。第二个坑是编译时的内核版本匹配问题。我有一次在虚拟机里编译宿主机内核更新了但虚拟机没重启uname -r显示的还是旧版本编译出来的模块加载到新内核上直接报invalid module format。所以编译前一定确认uname -r和ls /lib/modules/里的目录名一致。第三个坑是关于 CH341 的并口模式。如果你需要用 CH341 的并口功能比如驱动某些老式打印机或者 LCD默认的串口驱动是不行的。需要在官方驱动里选择并口模式编译而且并口模式下的设备节点是/dev/ch34x_pis*而不是ttyUSB*。这个在官方文档里写得比较隐晦我当初找了好久。第四个心得是关于波特率的。CH340 在 115200 及以下波特率的误差很小但到了 921600 这种高速率误差可能超过 2%导致通信不稳定。如果必须用高速率建议换 CH343 或者 CP2102 这类支持高速率的芯片。CH341 在高速率下的表现比 CH340 稍好但也不是特别稳。第五个心得是关于 USB 供电的。有些 CH340 模块从 USB 取电给目标板供电如果目标板功耗较大会导致 USB 电压跌落进而影响 CH340 本身的工作稳定性。表现就是设备时认时不认或者通信中途断开。解决办法是给目标板单独供电CH340 只负责信号连接。8. 验证驱动是否真正工作正常装完驱动、看到设备节点不代表就万事大吉了。我习惯做几个验证步骤。先做个回环测试。把 CH340 模块的 TX 和 RX 短接然后用 Python 发数据收数据import serial import time ser serial.Serial(/dev/ttyUSB0, 115200, timeout1) test_data bHello CH340 ser.write(test_data) time.sleep(0.1) received ser.read(len(test_data)) print(fSent: {test_data}) print(fReceived: {received}) print(fMatch: {test_data received}) ser.close()如果Match: True说明驱动和硬件基本正常。如果收到乱码或者收不到检查波特率、线序和短接是否可靠。再做个长时间稳定性测试。连续发送数据几分钟看看会不会中途断开import serial import time ser serial.Serial(/dev/ttyUSB0, 115200, timeout1) start time.time() count 0 while time.time() - start 300: ser.write(bX * 1024) ser.read(1024) count 1 if count % 100 0: print(fIteration {count}, elapsed {time.time()-start:.1f}s) ser.close() print(Stability test passed)这个测试能暴露供电不足、驱动 bug 或者 USB 线材质量差的问题。我遇到过跑几分钟就断的情况最后发现是 USB 线太长导致信号衰减。最后检查一下dmesg里有没有异常日志dmesg | grep -i -E ch34|usb.*error|ttyUSB | tail -30如果看到usb 1-1: reset high-speed USB device之类的信息频繁出现说明 USB 连接不稳定需要排查线材和供电。这套流程走下来CH340/CH341 在 Ubuntu 18.04 上的驱动问题基本都能解决。核心思路就是先确认系统认不认认了但用不了就查权限和波特率不认就编译官方驱动编译报错就照着内核 API 变更逐条改。整个过程最耗时的往往是编译时的报错修复但只要耐心对着报错改没有解决不了的。