很多嵌入式开发者和运维同学,在Linux下第一次跟串口打交道时,都会经历一段“明明插上了设备,却不知道它叫什么名字”的迷茫期。插上USB转串口线,ls /dev/里冒出一堆ttyS0、ttyUSB0、ttyACM0,到底哪个才是我的设备?敲个stty想看属性,满屏的参数也不知道怎么改。这篇文章就把这层窗户纸捅破,我结合自己调板子、写驱动、救设备时攒下来的经验,把Linux下查看和设置串口信息这件事讲透,适合刚接触Linux串口开发的学生、做嵌入式产品的工程师,以及需要临时在服务器上配置串口终端的老运维。
1. 把系统里的串口设备“捞”出来:设备节点与驱动识别
1.1 设备节点命名规律:ttyS*、ttyUSB*、ttyACM*、ttyAMA* 分别代表什么
Linux下一切皆文件,串口自然也不例外。所有串口设备都会在/dev目录下暴露一个设备节点,操作串口本质就是对这个文件做读写。但不同驱动框架、不同硬件接口,生成的文件名完全不一样,这是第一个容易懵的地方。
ttyS0、ttyS1:这是传统的16550A UART驱动注册的节点,一般对应主板上的物理串口,也就是老式电脑主板上那个九针的DB9接口。很多工控机、开发板的原生UART,也走这个框架。ttyUSB0、ttyUSB1:这是USB转串口芯片(比如CH340、CP2102、FT232、PL2303)通过usb-serial驱动注册出来的节点。你手上那条USB转TTL的小板子,插上Linux后几乎都会生成这种名字。注意编号从0开始累加,拔插顺序不同,编号会变。ttyACM0、ttyACM1:这是USB CDC ACM协议注册的节点,常见于STM32的USB虚拟串口、Arduino开发板、部分4G模块、GPS模块。它和ttyUSB的区别在于:一个是厂商私有协议(USB转串口芯片),一个是标准化的CDC协议。ttyAMA0、ttyTHS0:这俩常见于ARM开发板。ttyAMA是ARM PrimeCell UART(PL011)驱动,树莓派的板载串口就是ttyAMA0;ttyTHS是NVIDIA Tegra平台串口,部分Jetson Nano、Orin的串口走这个名字。
所以第一步永远不要凭感觉猜设备,先执行:
ls -l /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null我看到很多人在这一步就踩坑:明明插了个USB转串口,却在/dev/ttyS0上折腾半天,当然调不通。命名规律搞清楚,后面就顺了。
1.2 dmesg 与 udevadm:确认设备是否被内核识别、驱动是否绑定成功
光看到节点还不够,还得确认内核到底有没有认出这个设备、认成了什么。dmesg是排查串口问题的第一现场,插上设备后马上执行:
dmesg | tail -n 50正常情况下,USB转串口芯片插上后会看到一串日志,类似:
usb 1-1: new full-speed USB device number 5 using xhci_hcd usb 1-1: Product: USB-Serial Controller usb 1-1: Manufacturer: QinHeng Electronics ch341-uart now attached to ttyUSB0看到ch341-uart now attached to ttyUSB0这行,就说明CH340芯片已经被ch341驱动接管。如果插上设备后dmesg完全没有反应,大概率是硬件接触不良、线材损坏,或者USB口供电不足。如果看到usb 1-1: device descriptor read/64, error -71,八成是接触不良或线材质量太差。
dmesg是“过去时”,只能看已经发生的日志。如果想要设备当前的完整信息,用udevadm更直接:
udevadm info -a -n /dev/ttyUSB0这条命令会输出设备树上所有层级的属性,包括idVendor(厂商ID)、idProduct(产品ID)、driver(绑定的驱动)、sysfs路径等。这些信息在后面写udev规则固定设备名时是必需品,建议养成插上设备就查一下的习惯。
1.3 权限问题:为什么明明识别到了却打不开
设备节点存在,不代表你有权限操作。这是Linux串口开发里最经典的坑——Permission denied。Linux对设备的访问权限由文件权限位决定:
ls -l /dev/ttyUSB0如果输出是crw-rw---- 1 root dialout 188, 0 ...,意味着只有root用户和dialout组成员才能读写。普通用户直接去 open 这个文件,自然被拒之门外。
解决办法有三种:
临时加权限(不推荐,重启即失效):
sudo chmod 666 /dev/ttyUSB0把当前用户加入
dialout组(推荐,一劳永逸):sudo usermod -aG dialout $USER改完必须重新登录或者
newgrp dialout才会生效。永久修改权限规则,创建一个udev规则文件。
第三种方式我放到后面第4章,因为要结合固定设备名一起讲。这里重点记住:管串口的是dialout组,不是uucp也不是tty组。有些发行版(比如旧版Arch)用的是uucp,具体看ls -l输出里的组名就行。
2. 看懂串口属性:从一个-a参数逆向理解 termios 体系
2.1 stty -F /dev/ttyUSB0 -a:一屏参数到底在说什么
确认设备有权限后,就能查看它的当前属性了。标准命令是:
stty -F /dev/ttyUSB0 -a-F后面跟设备文件,-a表示列出所有属性。输出内容很多,但核心就这几类,我拆开讲:
speed 9600 baud; rows 24; columns 80; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; ...第一段speed 9600 baud,这就是波特率,也就是每秒传输多少个符号。嵌入式开发里最常见的几种波特率是9600、115200、460800、921600。串口通信双方必须在波特率上保持一致,否则收到的全是乱码。
再看标志位部分,比如:
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts这些标志位分四组,对应内核termios结构体里的四个字(或者叫flag组):
c_iflag:输入模式标志,管的是输入处理。比如icrnl表示把回车符转成换行符,ixon表示启用软件流控(Ctrl+S/Ctrl+Q)。c_oflag:输出模式标志,管的是输出处理。比如opost表示启用输出处理,onlcr表示把换行符转成回车加换行。c_cflag:控制模式标志,管的是硬件层面。比如波特率、数据位(cs5/cs6/cs7/cs8)、停止位(-cstopb是一位停止位,cstopb是两位)、校验位(parenb启用校验,-parenb关闭)、硬件流控(crtscts启用RTS/CTS)。c_lflag:本地模式标志,管的是终端交互行为。比如icanon启用行缓冲(回车才把数据交给程序)、echo回显输入、isig启用信号字符(Ctrl+C产生SIGINT)。
行缓冲这个值得多提一句。icanon开着的时候,程序read()串口会卡在那里,直到收到换行符才返回。很多人用C语言直接read()串口收到不完整数据,就是这个问题——数据早到了,但没换行符,内核不敢给。后面写程序时,第一件事就是用cfmakeraw()把这些行缓冲、回显、信号字符全关掉。
还有一个容易被忽略的参数是clocal。-clocal表示启用调制解调器控制,也就是依赖DCD(数据载波检测)信号。如果这个标志在-clocal状态,而你的设备又没有有效载波信号,程序打开串口后可能直接被挂起或收到挂断信号。一般操作普通TTL串口时,把它设置成clocal,也就是忽略调制解调器控制线。
2.2 5个高频stty设置场景:照抄就能用
理解了上面的参数,实际设置就很简单了。stty的语法是stty [设置项] -F [设备文件],多个设置放一起写,但不能设置当前正在被其他程序占用且没有-F直连的串口。
场景一,设置为115200波特率、8数据位、无校验、1停止位,也就是嵌入式最常用的“8N1”:
stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb场景二,打开硬件流控:
stty -F /dev/ttyUSB0 crtscts场景三,关闭硬件流控:
stty -F /dev/ttyUSB0 -crtscts场景四,把串口设为raw模式,保证数据原样通过、不做任何转换:
stty -F /dev/ttyUSB0 raw场景五,设置校验位为偶校验:
stty -F /dev/ttyUSB0 parenb -parodd奇校验则是parenb parodd。注意:设置校验位时,数据位要相应调整。偶校验配8位数据位的话,实际上传的数据只有7位有效。如果你不确定对端发的是什么格式,尽量默认用8N1,兼容性最好。
2.3 为什么改了属性后马上又变回原样
这是个高频求助帖。很多人stty -F /dev/ttyUSB0 115200之后,立刻用stty -F /dev/ttyUSB0 -a一看,还是9600,或者一会儿又变回去了。原因很简单:stty设置的是当前打开这个设备fd的终端属性,如果之前有程序(比如modemmanager、串口调试工具)打开着这个设备,或者你现在只是临时改,没有程序持续持有,某些桌面发行版的服务会自动把串口恢复成默认配置。
更准确地说,内核里串口属性不是永久保存的。你stty改的是一份termios状态,它归属到具体的终端设备对象上。如果设备被 close 后重新 open(尤其是没有用O_NOCTTY这种限制),某些系统服务或者modemmanager会重新初始化串口。遇到这种情况,要么把modemmanager禁掉:
sudo systemctl stop ModemManager sudo systemctl disable ModemManager要么在每次打开串口后,由你的程序主动设置属性,而不是依赖外部stty。这就是为什么所有正经的串口通信程序,都会在open之后、通信之前,先调用tcsetattr()把termios结构体设一遍。
3. 自己动手改属性:C语言和Python的两种标准姿势
3.1 C语言操作termios结构体:为什么不能用直接write
很多初学者会问:“我直接open("/dev/ttyUSB0", O_RDWR | O_NOCTTY)然后write()一个AT指令,为什么设备没反应?” 问题就在于没有初始化串口属性。新打开的串口默认是什么属性?可能是9600波特率、行缓冲开启、各种转换开启,这对绝大多数场景都是错的。真正要操作串口,至少要四步:
- 用
open()打开设备节点。 - 用
tcgetattr()读取当前属性到struct termios。 - 修改
termios结构体里的字段。 - 用
tcsetattr()把修改后的属性写回内核。
其中第三步,最快的方式是调用cfmakeraw(),它会把上述所有转换、回显、行缓冲全部关闭,得到最“纯净”的串口通道。然后单独设置波特率和数据位。
一个标准的初始化函数长这样:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <termios.h> #include <string.h> #include <errno.h> int set_serial(int fd, int baud) { struct termios tty; // 1. 读取当前属性 if (tcgetattr(fd, &tty) != 0) { perror("tcgetattr"); return -1; } // 2. 设为raw模式:关闭行缓冲、回显、信号字符、所有输入/输出转换 cfmakeraw(&tty); // 3. 设置波特率,输入和输出波特率必须同步 cfsetispeed(&tty, baud); cfsetospeed(&tty, baud); // 4. 8N1:8数据位、无校验、1停止位 tty.c_cflag &= ~CSIZE; tty.c_cflag |= CS8; tty.c_cflag &= ~PARENB; tty.c_cflag &= ~CSTOPB; // 5. 启用接收,忽略调制解调器控制线 tty.c_cflag |= CREAD | CLOCAL; // 6. 关闭硬件流控,让RTS/CTS完全由我们控制 tty.c_cflag &= ~CRTSCTS; // 7. 设置读取超时:每字节0.5秒超时,最多等100字节就返回 tty.c_cc[VTIME] = 5; tty.c_cc[VMIN] = 100; // 8. 写入内核。TCSAFLUSH表示等待输出传输完成,并丢弃未读输入 if (tcsetattr(fd, TCSAFLUSH, &tty) != 0) { perror("tcsetattr"); return -1; } return 0; }注意cfsetispeed()和cfsetospeed()是两个独立的调用。很多设备要求收发波特率一致,但也有特殊的设备收发波特率不一样,比如某些4G模块的AT指令通道是9600接收、115200发送,这种场景就必须把输入输出分开设置。br也就是B115200这些波特率常量,定义在<asm/termbits.h>里,最常见的几个是B9600、B38400、B115200、B460800、B921600。
关于VMIN和VTIME,这俩值得专门解释。VMIN是read操作最少要读到的字节数,VTIME是等待超时,单位是0.1秒。当VMIN > 0且VTIME > 0时,read会阻塞,直到等到VMIN个字节或者超时。很多人直接抄网上的代码设VMIN=0, VTIME=0,那表示非阻塞读——没数据立刻返回,程序里就要自己做超时和缓冲,处理不好很容易把CPU打满。我通常习惯设VMIN=1, VTIME=5,意思是最少等1个字节,每字节间隔超过0.5秒就返回,兼顾实时性和CPU占用。
3.2 Python pyserial:三行代码搞定属性设置
如果你不需要C语言那么底层的控制,Python的pyserial库是最快的路径。它底层封装了termios,但把细节藏得很好。安装:
pip install pyserial然后设置属性直接写在构造函数里:
import serial ser = serial.Serial( port='/dev/ttyUSB0', baudrate=115200, # 波特率 bytesize=serial.EIGHTBITS, # 8数据位 parity=serial.PARITY_NONE, # 无校验 stopbits=serial.STOPBITS_ONE, # 1停止位 timeout=1, # 读超时,单位秒 write_timeout=1, # 写超时 rtscts=False, # 关闭硬件流控 dsrdtr=False, # 关闭DSR/DTR流控 exclusive=True # 有些平台独占打开,防止别的程序抢 ) # 如果已经创建了对象,可以这样临时改波特率 ser.baudrate = 460800 # 读取 data = ser.read(64) # 读最多64字节,timeout决定阻塞多久 # 发送 ser.write(b'AT\r\n')这里有三个值得注意的坑:
第一,timeout设为None表示阻塞读直到拿到所需字节数,设为0表示非阻塞读,设为浮点数则是超时。多数场景给个1秒就够了。
第二,如果打开串口时报SerialException: [Errno 32] Broken pipe,八成是设备已经被别的程序占用了。Linux下同一时刻只能有一个进程以阻塞模式打开串口。有些调试助手的“打开”按钮按住不放,另一个进程就完全无法访问。
第三,某些Linux发行版的Python3默认对/dev/ttyUSB0的设备节点进行了“古老”的权限控制,普通用户打不开。可以先在终端里执行id看自己是不是在dialout组,不在就按1.3节加入。
3.3 非标准波特率怎么设:从stty到setserial
碰到非标波特率,比如很多电力采集终端用的2400、CAN卡用的333333,要分情况说。Bxxx常量覆盖不了所有波特率。对于标准UART,内核会尝试把一个时钟频率分频出接近目标值的真实波特率。如果你的设备恰好是标准UART,直接:
stty -F /dev/ttyUSB0 333333内核会自动计算分频因子。如果内核不支持这个值,会报invalid argument,这时有两种思路:
思路一,尝试通过baud_base配合spd_cust设置。对于16550A UART,可以查它的基准时钟:
setserial -g /dev/ttyS0如果设备挂在ttyUSB上,setserial通常管不着。ttyUSB的波特率走的是USB串口芯片自己的分频算法,支持的波特率范围写死在芯片固件里,CH340支持的最高波特率一般是2Mbps,CP2102可以到1Mbps,FT232H可以到12Mbps。并不是说想设多少就能设多少,超过芯片能力会直接报错。
思路二,如果程序侧设置,在Python里可以直接传非标值:
ser = serial.Serial('/dev/ttyUSB0', baudrate=333333)pyserial会把波特率传给内核的termios层,如果底层驱动支持就能生效。这类需求我在实际项目里遇到过几次,比如和某型号电能表通信,用的就是333333这个诡异的波特率。实测CH340能跑,CP2102会报ioctl(TCGETS2): Invalid argument,最后只能换硬件方案。所以非标波特率这个事,往往不是软件能暴力解决的,芯片本身的硬件能力是硬上限。
4. 实战排查链路:五个高频串口苦恼的完整拆解
4.1 乱码:波特率之外的三个隐藏变量
乱码是串口调试里出现频率最高的问题。很多人第一反应就是“波特率不对”,但实际排查时,即使波特率完全正确,也可能出现下面几种“诡异乱码”,每一种的根因都不同。
第一种,波特率确实不匹配。这种乱码有特征:字符整体还是能看出轮廓的,比如发0xA5收到0x61这种规律性错位。通过示波器或逻辑分析仪抓一下波形,算一下实际波特率即可确认。
第二种,串口电平不匹配。我排查过的最经典案例:TTL电平的板子和RS232电平的设备直接对接,TTL的高电平是3.3V或5V,RS232的高电平是-12V,低电平是+12V,两者电平标准完全不同,差之毫厘谬以千里。这种乱码不管怎么调波特率都没用,必须加电平转换芯片,比如MAX3232。
第三种,数据位和校验位不匹配。比如对端是7位偶校验,你这边是8位无校验,收到的每一个字节都会出现一个bit的整体偏移,表现出来就是随机乱码。这时用stty -F /dev/ttyUSB0 cs8 -parenb和stty -F /dev/ttyUSB0 cs7 parenb -parodd来回切换测试,能定位很大一部分“玄学乱码”。
第四种,Linux的termios转换在搞鬼。终端设备默认带了很多转换,比如icrnl会把回车(0x0D)转成换行(0x0A),这样你从串口收到的字节流就不再是原始字节了。对于数据通信场景,这个问题几乎必现,解决办法就是用raw模式把所有转换关闭。我之前遇到过一台设备发来的数据里,0x0D全部凭空消失,起初以为是对方固件逻辑bug,后来排查到是icrnl悄悄把回车吃掉了。
4.2 接收数据丢失:从缓冲区到流控的完整链路
“Linux从串口接收数据丢失”是热搜词里非常典型的一个问题。丢失的症状很多:读到的数据和设备实际发出来的不一样,中间缺一段,或者开头几个字节没了。我按排查顺序给你捋一遍。
第一步,确认内核缓冲区确实收到了这些数据。在设备端上报数据的同时,开一个终端跑cat /dev/ttyUSB0 | xxd,如果能看到数据,说明数据到达了内核,丢失发生在用户态读取层。如果cat都看不到,说明问题出在硬件或驱动层。
第二步,如果数据在内核层就丢了,检查是不是缓冲区溢出。Linux串口驱动有一个软件FIFO缓冲,如果用户态读得不够快,新来的数据会把旧数据覆盖或者直接丢弃。可以用cat /proc/tty/driver/serial查看串口统计信息,看有没有rx溢出计数在增加。
第三步,检查流控。如果开启了硬件流控crtscts,而你的设备没有拉高CTS信号线,对端发来的数据就会被丢掉。嵌入式开发中我见过太多人开开心心开着硬件流控,但线根本没接RTS/CTS,数据丢得莫名其妙。排查思路是关闭硬件流控再测一次。
第四步,用户态读取策略问题。前面说过的VMIN/VTIME如果设置不合理,比如VMIN=0, VTIME=0非阻塞读,程序里又没有做好数据拼包,就会出现“丢数据”的假象。正确处理是维护一个环形缓冲区,读到的每一段数据都先入环,然后从环里按协议解析。
# 伪代码示例:串口读取线程维护环形缓冲 buffer = bytearray() while True: chunk = ser.read(256) if not chunk: continue buffer.extend(chunk) # 从buffer里按帧头/帧尾解析完整帧 while len(buffer) >= frame_len: frame = buffer[:frame_len] buffer = buffer[frame_len:] process_frame(frame)第五步,检查是不是USB转串口芯片的批量传输粒度问题。有些USB串口芯片在空闲时会自动进入低功耗模式,第一个字节经常触发不上报,导致数据缺失。这种情况只能在硬件侧给芯片加些活动电流,或者把设备端的串口发送改成持续轮询。这属于硬件特性,软件很难完全规避。
4.3 串口烧写失败:别把锅都甩给引导程序
嵌入式开发中“串口烧写失败”基本是必经之路。很多人一上来就怀疑固件bootloader坏了,但实际上串口烧写失败的原因大多是环境问题。以最常见的STM32和ESP32为例,完整的排查链路如下。
第一步,确认串口节点存在且权限正确。CH340在Linux下的驱动是内核自带的ch341模块,2.6.24之后的内核都支持。如果插上USB转串口后没有生成/dev/ttyUSB0,检查:
lsmod | grep ch341 modprobe ch341lsmod没有输出就要modprobe加载。如果modprobe报错,那说明内核编译时根本没把CONFIG_USB_SERIAL_CH341编进去,需要重新编译内核或者换一条用FT232芯片的线,后者驱动覆盖更广。
第二步,确认烧写软件读到的串口名。STM32CubeProgrammer、esptool等工具都支持指定端口,比如-p /dev/ttyUSB0,如果端口写错(比如写了COM3)就会连接失败。这里有个细节:很多烧写工具在打开串口前会先拉低DTR/RTS来自动复位目标板芯片进入bootloader,如果你的USB转串口线不支持DTR/RTS引出,或者你根本没接这两根线,自动复位就不会发生,设备一直跑在应用代码里,烧写当然失败。
第三步,硬件连接状态。烧写场景下,串口的TXD要接目标板的RXD,RXD接TXD,GND必须共地,这是最容易被忽略的。我见过不少人是“照着卖家图接的”结果发现只连了三根线,忘了共地,导致串口通信时好时坏。另外,如果你的USB转串口小板上有跳线帽选了5V供电,有些3.3V的MCU会被灌坏,烧写失败是轻的,芯片直接烧了也不奇怪。
第四步,boot模式引脚。以STM32为例,BOOT0引脚电平决定芯片上电后从哪个区域启动。烧写程序时要把BOOT0拉高进入system memory bootloader,烧完后再拉低恢复正常运行。很多人BOOT0沒接跳线或者悬空,芯片直接跑应用,烧写自然失败。这种问题的根因不在Linux,但排查链路是通的。
第五步,如果以上全部正常还是失败,查看烧写软件输出的错误信息。esptool会告诉我们Connecting...后又等多久,如果A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header,多半是自动复位电路没生效,这时可以手动复位:先让软件进入等待连接状态,再按住目标板的复位键,点烧写,等开始检测到设备后松开复位键。
4.4 拔插后设备名变了:udev规则固定设备名
ttyUSB0变成ttyUSB1、ttyUSB2,对于调试助手这种工具不是大事,但对于写死在脚本和自动化流程里的设备名,这就是灾难。解决办法是用udev规则,根据设备芯片的VID/PID(或者物理位置)固定一个别名链接。
先查出芯片的VID/PID,用前面讲过的命令:
udevadm info -a -n /dev/ttyUSB0 | grep idVendor udevadm info -a -n /dev/ttyUSB0 | grep idProductCH340的VID/PID是1a86/7523,CP2102的是10c4/ea60,FT232的是0403/6001。然后创建udev规则文件:
sudo vi /etc/udev/rules.d/99-usb-serial.rules写入:
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="ttyCH340", MODE="0666"前面写的SYMLINK+="ttyCH340",意思是创建一个名为/dev/ttyCH340的软链接,指向真实的ttyUSB0或ttyUSB1。后面MODE="0666"一步到位解决权限问题,省得每次加组。然后重载udev规则:
sudo udevadm control --reload-rules sudo udevadm trigger插拔一次USB后,/dev/ttyCH340就会出现。脚本里从此只写/dev/ttyCH340,不管系统里插多少USB转串口,这个名字都唯一指向那块CH340芯片。
这里要特别提醒:如果同时插了两块一模一样的CH340,那这两块设备的idVendor/idProduct完全一样,单靠VID/PID区分不开。这时要在规则里加上物理端口信息,比如KERNELS=="1-1.2:1.0",这个值可以从udevadm info -a -n /dev/ttyUSB0输出的父设备信息里找。加了KERNELS后,规则就只匹配插在特定USB口的那个设备,插别的口不生效。这也是很多工业设备出厂前锁定USB口的原因。
4.5 虚拟串口与调试工具:没有硬件时的替代方案
在没有真实硬件的情况下,Linux下可以用socat创建虚拟串口对,对开发调试非常有用。它会在系统里创建两个互联的伪终端(pty),往一端写数据,另一端能读到,完全模拟物理串口的通信行为。
socat -d -d pty,raw,echo=0 pty,raw,echo=0执行后会输出类似:
PTY is /dev/pts/3 PTY is /dev/pts/5这时打开两个终端,一个跑screen /dev/pts/3 115200,另一个跑screen /dev/pts/5 115200,就能互相收发数据。配合Python写的模拟对端程序,完全可以在没有硬件的情况下把串口应用的上层逻辑先跑通,包括协议解析、超时重传、粘包处理等等。
如果你更习惯图形化工具,Linux下比较顺手的串口调试工具是cutecom和moserial,装一个就能用:
sudo apt install cutecom不过给实际设备调数据时,我最常用的还是终端三件套:screen、minicom、picocom。三者的区别是:minicom功能最全但配置文件多,screen最少依赖临时用最方便,picocom轻量、退出方式清晰(Ctrl+A Ctrl+X),适合脚本调用。我个人的建议是调试普通串口用picocom:
picocom -b 115200 /dev/ttyUSB0如果一端需要主动发文件,比如把固件通过串口发给设备,rz/sz配合minicom是经典组合。这类工具细节这里不展开,但要记住一个通用经验:不管用哪个工具,打开串口前先用stty按你的协议属性初始化一遍,再用工具连接,能少遇到90%的“打开就乱码”问题。
5. 底层信令与杂项:设备节点之外的“看不见的串口状态”
5.1 如何查看RTS、CTS、DTR、DSR等控制信号电平
串口不只是TXD和RXD两根数据线,还有一堆控制信号线。很多半双工通信设备需要看RTS、DTR的状态来判断是否处于发送态。Linux下可以这样看:
cat /proc/tty/driver/serial输出里会包含每路串口的状态位信息,其中有一个tx和rx的统计字段。不过更直观的做法是用一个小工具ioctl读取modem状态位。C代码里对应的宏是TIOCMGET:
#include <sys/ioctl.h> #include <asm/termbits.h> int status; ioctl(fd, TIOCMGET, &status); if (status & TIOCM_CTS) printf("CTS is active\n"); if (status & TIOCM_DSR) printf("DSR is active\n"); if (status & TIOCM_RI) printf("RI is active\n"); if (status & TIOCM_DCD) printf("DCD is active\n");Python里用pyserial也能查,ser.cts、ser.dsr、ser.dcd、ser.ri对应四个输入信号,ser.rts、ser.dtr是输出信号,直接读属性就能拿到当前状态。如果某根信号线电平不对,重点检查接线和芯片供电,软件上能做的只有通过TIOCMSET强制拉高或拉低某些输出信号。
5.2 串口数据的流量统计与故障定位
排查“数据到了应用层但不对”这类问题时,流量统计能给关键线索。Linux的serial驱动统计在/proc/tty/driver/serial:
cat /proc/tty/driver/serial重点关注rx、tx后面的计数,以及fe(帧错误)、pe(校验错误)、brk(break中断)、oe(溢出错误)。如果fe在持续增长,大概率是波特率不匹配。如果oe在增长,是用户态读太慢。这四类错误各有明确的物理含义,看到哪个涨就往对应方向排查,比盲目猜快得多。
5.3 串口权限与日志:tty相关的内核日志和系统服务
最后提醒一个很多人忽视的点:ModemManager和brltty这两个系统服务会主动占用串口设备。ModemManager会尝试探测每个新出现的tty*设备,把它当成3G/4G模块来初始化。brltty是盲文终端服务,它有时会抢ttyUSB*。这两个服务是“串口明明没程序打开却断连”的常见元凶。如果你的设备每次插上后几秒钟内出现了异常流量,先停掉ModemManager:
sudo systemctl stop ModemManager sudo systemctl disable ModemManager这算是我在实际项目里被坑过后总结出来的优先级较高的排查项。很多工程师在嵌入式开发板上从没遇到过这个问题(开发板通常没装ModemManager),但一换成Ubuntu桌面版或虚拟机里的Linux,就莫名“串口被占”,根因其实就是它。
我在实际调试中的经验是:把串口开发中所有“玄学”问题都当成“某个环节存在一个可测量的量”,用dmesg看驱动,用stty -a看属性,用/proc/tty/driver/serial看错误计数,用逻辑分析仪看物理波形,每一步都有据可依,90%的问题都能在三层之内定位到根因。剩下10%的问题,往往是硬件本身——检查接线、测量电平、换个USB口,总比重新编译内核来得快。