在NVIDIA Jetson上做边缘计算项目,几乎人手一个CH340 USB转串口模块。不管是连接STM32下位机、读取飞控数据,还是通过板载UART调试口输出日志,CH340驱动没装好,整条链路就断在最前面。两年前我第一次在Jetson Nano上调一个传感器项目,插上USB转串口模块后lsusb能看到芯片,但/dev/ttyUSB0就是不出来,折腾了大半个晚上才搞清楚是驱动模块没加载。后来在Orin NX、Xavier NX上又陆续遇到过权限、内核升级、接线错误等一堆问题,每次都得花时间重新排查。这篇就把我在Jetson系列平台上安装CH340驱动的完整经验整理出来,按从易到难的顺序讲清楚怎么检查、怎么装、怎么排错,适合刚拿到Jetson板子的新手,也能给被驱动折腾过的人参考。
1. 为什么 Jetson 上装 CH340 驱动比普通电脑费劲
1.1 CH340 芯片与 Linux 驱动现状
CH340 是沁恒(WCH)出的一款低成本USB转串口芯片,在开发板、单片机、飞控、3D打印机主板上用得非常多。它的VID:PID通常是1a86:7523,Linux 内核主线里早已包含对应驱动,模块名叫ch341。
这里有个容易搞混的点:芯片叫CH340,但Linux驱动模块叫ch341。因为沁恒后续出了CH341芯片,驱动源码同时兼容这两类芯片,模块名一直沿用了ch341。所以在Linux系统里,你要找的不是ch340模块,而是ch341模块。
Windows下的情况大家比较熟,装一个CH341SER.EXE就完事,但Linux下需要内核模块配合。普通Ubuntu的发行版内核通常默认编译了这个模块,插入设备就会自动加载。而Jetson用的可不是普通Ubuntu内核,是NVIDIA基于Ubuntu深度定制的tegra内核,模块配置和桌面版Ubuntu不完全一样,这是很多人卡住的根本原因。
1.2 Jetson 与普通 Ubuntu 在驱动上的差异
Jetson 的系统和驱动有两层关系要理清楚。
第一层是系统层面。Jetson用SDK Manager刷的官方镜像,底层是Ubuntu,但内核是NVIDIA维护的tegra内核。以Jetson Nano最常见的JetPack 4.6.1为例,它是Ubuntu 18.04,内核版本是4.9.253-tegra;到Jetson Orin系列,JetPack 6.x对应Ubuntu 22.04,内核版本到了5.15.136-tegra左右。这个带-tegra后缀的内核,模块配置和桌面Ubuntu的generic内核差别不小,主线默认开启的驱动不一定被带进来。
第二层是编译环境层面。普通Ubuntu上想给内核装模块,装一个linux-headers-$(uname -r)基本就够。但在Jetson上,apt install linux-headers-$(uname -r)经常找不到对应包,因为tegra内核的headers依赖NVIDIA自己维护的源码包,不一定在Ubuntu软件源里。这就导致很多人下载了CH340驱动源码,一make就报错,提示找不到内核构建目录。
所以,在Jetson上装CH340驱动,不能照搬普通电脑上的做法,得先确认系统里到底有没有现成模块,没有再去手动编译,而且手动编译之前要把内核头文件问题先解决掉。
2. 动手前的环境检查:镜像版本、内核状态与设备识别
2.1 确定 Jetson 型号与系统版本
同样叫Jetson,Nano、TX2、Xavier NX、Orin NX之间,系统镜像和内核配置差异不小。排查驱动问题前,先确认自己的板子是哪个型号、跑的是什么系统,这一步能避免后面走弯路。
板卡型号可以用:
cat /proc/device-tree/model输出类似NVIDIA Jetson Nano Developer Kit或NVIDIA Jetson Orin NX。
系统版本要看两部分,一是Ubuntu版本:
lsb_release -a二是NVIDIA L4T(Linux for Tegra)版本:
cat /etc/nv_tegra_release比如JetPack 4.6.1通常对应L4T R32.7.x,JetPack 6.0对应L4T R36.x。
再看内核:
uname -a这几条命令结合输出,就能清楚知道自己手里的板子适合哪条安装路线。L4T R32系列的内核比较老(4.9),WCH官方源码基本能直接编译;L4T R35/R36系列的内核较新(5.10/5.15),官方老源码编译很容易报错,后面我会讲怎么处理。
2.2 确认 CH340 设备是否被系统识别
不急着装驱动,先把模块插到Jetson的USB口上,然后确认系统到底有没有看到这个设备。
第一步看USB总线:
lsusb正常情况会出现一行:
Bus 001 Device 003: ID 1a86:7523 QinHeng Electronics CH340 serial converter如果这一行都没有,问题不在驱动,而在硬件层面。先换个USB口,尤其是Jetson Nano的Micro USB口和数据口别搞混了;也可以换个CH340模块,排除模块本身损坏。部分扩展坞上的USB口供电不稳,也会导致识别不到芯片,建议直连板子的USB Type-A口测试。
如果lsusb能看到1a86:7523,说明USB枚举成功了,接下来看内核有没有把它转成串口设备:
ls /dev/ttyUSB*或者用:
dmesg | tail -30插入设备的瞬间,dmesg里会出现一行类似:
usb 1-2: ch341-uart converter now attached to ttyUSB0说明驱动已经正常工作。如果没有这一行,而lsusb又能看到芯片,那基本就是驱动模块缺失或没加载,进入下一节处理。
3. 先别急着编驱动:主线 ch341 模块的加载技巧
3.1 系统自带模块的检查与手动加载
很多人的第一反应是下载WCH官方源码去编译,但我要先说一句:多数Jetson镜像里其实已经带了ch341内核模块,只是没有自动加载。先检查这一个,能省掉后面一整个流程的麻烦。
检查模块是否存在:
modinfo ch341能看到类似filename: /lib/modules/.../kernel/drivers/usb/serial/ch341.ko的输出,就说明系统里有这个模块。此时直接加载:
sudo modprobe ch341再插入CH340模块(如果之前已经插着,拔掉重插一次),然后看:
dmesg | tail -20 ls /dev/ttyUSB*如果/dev/ttyUSB0出现了,问题就解决了。
如果modinfo提示modinfo: ERROR: Module ch341 not found,说明镜像里没有这个模块,只能手动编译。还有一种情况是modprobe本身报错,比如Required key not available,这是Secure Boot相关机制导致的模块签名校验失败,Jetson上不常见,真遇到的话多半是系统被人改过引导配置,不在本文讨论范围。
3.2 让 ch341 模块开机自动加载
modprobe手动加载只是临时的,重启之后又会失效。最好设置开机自动加载:
echo "ch341" | sudo tee /etc/modules-load.d/ch341.conf这个文件会让系统在启动早期就加载ch341模块。如果你希望更彻底,也可以在/etc/rc.local里写modprobe ch341,但要确认系统支持rc.local,现代Ubuntu默认不一定会执行它。modules-load.d是更标准也更干净的做法。
我个人习惯是把这一步放在所有手动编译操作之前做。因为官方镜像的模块配置在不同版本间有变化,同是JetPack 4.6,Nano的镜像里可能带了ch341,Orin NX的镜像里可能没带,先查一遍总没坏处。
4. 手动编译官方驱动的完整流程与坑点
4.1 确认内核头文件环境
如果系统确实没有ch341模块,就得手动编译。编译内核模块必须要有和当前内核完全匹配的头文件,否则编出来的.ko文件加载不进去。
先看内核构建目录是否存在:
ls /lib/modules/$(uname -r)/build这个目录如果存在且能进入,说明头文件环境基本OK。如果提示不存在,先看系统里装了什么headers包:
dpkg -l | grep linux-headers在Jetson上,常见情况是这样的:有的镜像为了节省空间,没有预装headers包。这时候可以试一下:
sudo apt update sudo apt install linux-headers-$(uname -r)但如果软件源里没有对应的tegra内核headers包,apt会直接报找不到。遇到这种情况,比较实用的办法是从NVIDIA的L4T源码仓库下载对应版本的kernel source,解压后把kernel/kernel-4.9或类似目录直接软链到/lib/modules/$(uname -r)/build。这个步骤比较繁琐,但跑一次之后,以后再编译任何内核模块都用得上。
如果你用的板子已经刷了较新的JetPack版本,也可以直接看/usr/src目录下有没有现成的headers目录:
ls /usr/src如果出现类似linux-headers-5.15.136-tegra-ubuntu22.04这样的目录,把它软链过去:
sudo ln -s /usr/src/linux-headers-5.15.136-tegra-ubuntu22.04 /lib/modules/$(uname -r)/build软链创建之后,再用ls /lib/modules/$(uname -r)/build验证一次。
4.2 WCH 官方源码的获取与编译
确认头文件环境没问题后,去WCH官网下载CH341 Linux驱动源码。文件名一般是CH341SER_LINUX.ZIP,解压后目录结构很简单:
CH341SER_LINUX/ ├── Makefile ├── ch341.c └── ch341.h如果板子上不方便用浏览器下载,可以先在电脑上下载,再通过scp传到Jetson上,或者直接:
wget https://github.com/WCHSoftGroup/ch341ser_linux/archive/refs/heads/master.zipWCH在GitHub上有官方仓库,文件更新比官网及时一些,很多针对新内核的适配补丁已经合进去了,这是比官网下载更省心的选择。
进入源码目录后直接编译:
cd CH341SER_LINUX make clean make如果一切顺利,当前目录下会生成ch341.ko文件。但不要指望一次通过,我在不同版本的内核上编译这个驱动时,遇到过几类典型的报错。
第一类,make提示找不到内核目录,比如:
Makefile: 没有那个文件或目录 **** Unable to build, no kernel build directory这个基本都是/lib/modules/$(uname -r)/build软链没配好,检查4.1节的步骤。
第二类,源码版本太老,在新内核上接口对不上,比如:
error: implicit declaration of function ‘signal’ error: initialization of ‘int (*)(struct inode *, struct file *, unsigned int, long unsigned int)’ from incompatible pointer type这是老版本ch341.c里用了旧版内核的signal函数和ioctl接口,新内核把这些都改了。解决办法有两个:一是从WCH GitHub仓库拉取更新版本,里面已经适配了新内核;二是自己改源码,把.ioctl换成.unlocked_ioctl,把#include <linux/signal.h>加上。对不熟悉内核编程的朋友,我更推荐直接用GitHub版本。
第三类,编译成功但modprobe加载时提示版本不匹配:
modprobe: ERROR: could not insert 'ch341': Exec format errordmesg | tail会看到类似module version magic mismatch。原因是源码的Makefile里可能带了固定的内核版本标记,或者当前内核开启了Module Versioning(CONFIG_MODVERSIONS),此时需要重新make clean后再make,确保没有遗留的旧对象文件。
4.3 安装模块并完整验证
编译成功后,官方Makefile提供了安装命令:
sudo make install这个命令会把ch341.ko复制到当前内核的模块目录下:
sudo cp ch341.ko /lib/modules/$(uname -r)/kernel/drivers/usb/serial/ sudo depmod -a然后加载:
sudo modprobe ch341重新拔插CH340模块,检查:
dmesg | tail -20 ls -l /dev/ttyUSB0看到crw-rw---- 1 root dialout 188, 0这样的设备节点就算成功了。如果不放心,可以再用一个回环测试验证串口收发,这一步我放到后面权限章节讲。
有一点提醒:如果你之前手动加载过旧版本模块,一定要先卸载再加载新版:
sudo modprobe -r ch341 sudo modprobe ch341否则内核会一直使用旧的模块文件,新编译的版本不生效,这类问题最容易让人产生"我明明编了怎么还是没用"的错觉。
5. 权限、udev 与串口工具:让 ch340 真正可用
5.1 dialout 用户组与权限问题
驱动加载成功只是第一步。/dev/ttyUSB0的默认权限是crw-rw----,所属用户组是dialout。当前用户如果不是dialout组成员,打开串口时会提示:
open /dev/ttyUSB0: Permission denied把用户加入组:
sudo usermod -aG dialout $USER然后注销重新登录,或者重启一次,让用户组生效。如果不想重启,可以用newgrp dialout临时切换当前shell的活动组,但只对当前终端有效。
我在实际项目里见过有人直接sudo chmod 777 /dev/ttyUSB0,这种操作极其不建议。一方面不彻底,拔插一次USB设备后权限又变回默认值;另一方面权限放得太大,其他用户也能随意访问串口设备,存在安全隐患。
5.2 自定义 udev 规则固定串口设备名
开发中还有一个高频问题:同时插多个USB转串口设备时,系统按枚举顺序命名设备,这次是/dev/ttyUSB0,下次可能变成/dev/ttyUSB1,脚本里写死的设备路径就失效了。解决办法是用udev规则绑定USB设备的VID:PID,甚至绑定物理端口位置。
新建规则文件:
sudo nano /etc/udev/rules.d/99-ch340.rules内容如下:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666", SYMLINK+="ch340"这行规则的作用是:当系统发现VID为1a86、PID为7523的USB设备时,在/dev下额外创建一个名为ch340的符号链接,并把权限设为0666。应用规则:
sudo udevadm control --reload-rules sudo udevadm trigger拔插设备,然后检查:
ls -l /dev/ch340因为CH340和CH341芯片的PID不同(CH340一般是7523,CH341是5523),如果你的模块是CH341,把idProduct改成5523。有些CH340的兼容芯片也使用相同VID:PID,这条规则基本都适用。
5.3 用 picocom 做回环验证
权限搞定后,建议做一个完整的回环测试确认串口真的能收发数据。用一根杜邦线把CH340模块的TX和RX短接,然后安装串口工具:
sudo apt install -y picocom打开串口:
picocom -b 115200 /dev/ttyUSB0把CH340模块的TX和RX短接的情况下,在picocom里随便敲几个字符,屏幕上应该能看到自己敲的内容。如果没有回显,先确认TX/RX是否真的短接好了,再检查波特率。
也可以不用picocom,直接在命令行做非交互式测试:
stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb echo "jetson-test" > /dev/ttyUSB0短接TX/RX的情况下,再执行:
cat /dev/ttyUSB0会输出刚才写入的jetson-test。注意cat是阻塞的,测试完用Ctrl+C结束。这个方法适合在脚本里做自动化自检。
6. 内核更新之后驱动失效的应对套路
6.1 为什么每次内核一变驱动就要重编
很多人会遇到这样一个场景:Jetson用了几个月都正常,某天执行了apt upgrade,重启之后串口没了,ls /dev/ttyUSB*什么都找不到。原因是内核模块和内核版本是强绑定的,升级内核后,系统启动的是新版内核,但/lib/modules/$(uname -r)/kernel/drivers/usb/serial/ch341.ko这个路径本身发生了变化,旧模块不在新内核的目录里,或者与新版内核不兼容。
在Jetson上还有一层麻烦:官方镜像默认的apt源通常不会直接推送tegra内核升级,但如果你自己添加过其他软件源,或者刷了社区维护的镜像,内核算命的概率就大大增加。
面对这种问题,我的建议是分两步走。
第一步,升级前先备份。至少把当前内核版本下能用的模块目录拷贝一份:
sudo cp -r /lib/modules/$(uname -r) ~/module-backup-$(uname -r)第二步,升级后重新编译。不要等串口没了才开始慌,按4.1节的流程确认新内核的headers环境,然后直接重编译一次。
6.2 半自动重建脚本与注意事项
把重复劳动用脚本固化下来,是我在维护多块Jetson板子时最受益的做法。下面这个脚本放在/usr/local/bin/rebuild-ch341.sh,每次内核升级后跑一遍就行:
#!/bin/bash set -e KERNEL_DIR=/lib/modules/$(uname -r)/build DRIVER_SRC=/opt/CH341SER_LINUX if [ ! -d "$KERNEL_DIR" ]; then echo "Kernel build directory not found: $KERNEL_DIR" exit 1 fi cd "$DRIVER_SRC" make clean make sudo make install sudo depmod -a sudo modprobe -r ch341 || true sudo modprobe ch341 dmesg | tail -10 echo "CH341 driver rebuilt for $(uname -r)"如果你从WCH下载的源码不在/opt/CH341SER_LINUX,把第二行的DRIVER_SRC改成实际路径。脚本里modprobe -r ch341 || true的含义是:如果模块当前没加载,卸载操作会报错,但不影响脚本继续执行。
另外,如果板子只是跑项目不做系统实验,我不建议频繁升级内核。Jetson的L4T内核升级通常伴随着Bootloader和DTB的更新,一旦中途断电,板子变砖的风险比普通电脑高不少。保持系统版本稳定,也是边缘设备可靠运行的重要前提。
7. 排查实录:三次典型的 CH340 连接故障
光讲正常流程还不够,实际调试中那些稀奇古怪的问题才是真正耗时间的。这里记录三个我自己的真实排查过程,都是很典型的情况,希望对你有参考价值。
7.1 lsusb 能看到芯片,但 /dev/ttyUSB0 始终不出现
有次我在Jetson Xavier NX上接一个CH340模块,lsusb输出很正常,能看到1a86:7523,但/dev/ttyUSB*就是没有。当时我下意识先走了编译驱动的流程,还查了内核版本,折腾了大半小时之后才想起来先查模块状态。
执行lsmod | grep ch341,没有任何输出,说明内核里没加载这个模块。再执行modinfo ch341,发现模块文件其实是存在的。问题只是模块没有自动加载。
当时为什么没有自动加载呢?因为那个镜像的/etc/modules-load.d下没有配置ch341,而且模块插入早于驱动注册,或者USB设备枚举顺序不同,都会导致模块没被自动绑定。手动sudo modprobe ch341后,重新拔插模块,/dev/ttyUSB0立刻出现。
这个坑提醒我:先查modinfo,比一上来就编译驱动省时间得多。
7.2 dmesg 报错 can't set legal baudrate
另一次是在Jetson Orin NX上连接一个工业设备,设备波特率是9600。用picocom打开串口报错提示我记得很清楚:
Cannot set baud rate to 9600dmesg里跟了一句:
ch341-uart converter: failed to set terminal settings这类问题不是驱动没装,而是CH340芯片内部波特率发生器对某些非标准波特率的支持有限。尤其在新版本内核里,ch341驱动对波特率做了更严格的规定,一些老设备使用的非标准波特率会被拒绝。
处理办法有两个方向。一是换个支持更精确波特率的芯片,比如CP2102或FT232,对于需要特殊波特率的工业场景,这比跟驱动较劲更靠谱。二是在应用层换其他方式,比如如果对方设备支持,把波特率统一成115200这种标准值。CH340本身是低成本方案,在波特率精度和稳定性上跟FT232确实有差距,长时间高负载传输时尤其明显。
7.3 打开串口成功,但收到全是乱码
最后一个例子是接线问题。我当时连的是一个GPS模块,芯片也是CH340,驱动一切正常,设备节点也在,但picocom里收到的全是乱码,偶尔有几个能看懂的字符。
这种情况先检查两件事。第一,波特率是否匹配,模块和GPS模块的波特率设成一致,9600还是115200要对上。第二,如果波特率确认无误,几乎可以肯定是硬件接线问题。
这里有个经典误区:两个设备之间的串口连接,不是TX接TX、RX接RX,而是要交叉连接,也就是设备的TX对目标的RX,RX对目标的TX。还有一个必须要注意的点是GND要共地,很多新手只接TX/RX两根线,不接地线,结果就是数据收发的参考电平不一致,表现就是乱码甚至完全没反应。
当时我重新看了接线,发现TX/RX没交叉,GND也没接,改过来之后就完全正常了。
写在最后的一个习惯
这几块Jetson板子用下来,我养成一个习惯:刷完系统,第一件事不是装CUDA环境,而是先插上CH340模块,确认dmesg能看到ttyUSB0。这个动作看起来不起眼,但能省下后面调试时大量的猜测时间——串口是所有调试手段里最底层的保底通道,它要是不可用,后面出了问题连日志都看不到。
如果你手头也有Jetson板子,建议把这个流程走一遍。重点记住三条:先查modinfo ch341而不是直接编译、编译前确认/lib/modules/$(uname -r)/build存在、权限问题用usermod -aG dialout而不是chmod 777。把这三个关键点处理好,CH340在Jetson上就不会再给你添堵了。