在Linux下摆弄串口,是每个玩嵌入式、搞硬件调试、甚至折腾老旧工控设备的开发者都绕不开的活儿。别看书上写得玄乎,实际用起来无非就是“查得到、配得对、读得出、写得进”这几件事。但真上手的时候,你会发现一堆乱七八糟的坑——设备号对不上、权限不够、波特率明明配了却没生效、数据收发总丢字节……这都不是什么疑难杂症,多半是底层的属性和终端参数没有搞明白。
这篇文章我不打算给你掉书袋,从怎么在/dev下找设备、怎么用stty和setserial查看和设置波特率、数据位、停止位、硬件流控,到怎么通过minicom/picocom这类工具验证成果,再到排查串口收发异常时的一些野路子技巧。还会重点讲一下USB转串口模块和原生串口的区别,毕竟现在绝大多数开发场景用的都是CH340、CP2102这类USB转出来的串口,行为特征和板载UART有很大不同。全程以我实际调试过的场景为主线,保证你能照着做。
1. 串口信息怎么查:从硬件到系统的一条完整链路
很多人上来就只知道ls /dev/ttyUSB0,看设备存在就觉得完事了。其实完整的串口排查链路是这样的:物理连接 -> 内核驱动识别 -> 设备节点生成 -> 设备权限可用 -> 属性配置正确 -> 数据通路正常。任何一环断了,后面的功夫全都白搭。
1.1 用dmesg和lsusb确认串口设备到底有没有被识别
先把USB转串口模块插上,然后立刻执行:
dmesg | tail -n 30如果你的模块正常,会看到类似下面的输出:
usb 3-1: new full-speed USB device number 5 using xhci_hcd usb 3-1: Product: USB-Serial Controller usb 3-1: Manufacturer: Silicon Labs cp210x 3-1:1.0: cp210x converter detected usb 3-1: cp210x converter now attached to ttyUSB0注意看最后一行,它告诉我们这个芯片被映射到了ttyUSB0。如果芯片是CH340,那设备名同样会是ttyUSB0,但驱动名字会变成ch341或ch340之类的。这一步的价值在于:它区分了“设备根本没被识别”和“设备识别了但配置不对”两种完全不同的场景。
单纯用lsusb也能看到设备有没有在USB总线上挂载,比如:
lsusb可以看到类似“1a86:7523 QinHeng Electronics USB-Serial Controller”这样的信息。不过lsusb只能证明USB层面通了,内核驱动有没有绑定到设备上,还是得靠dmesg来判断。我记得有次调一块老开发板,lsusb能看到设备,但dmesg里始终没有“attached”的提示,后来发现是内核编译时把对应的驱动编译成了模块,但模块没有被加载,执行modprobe cp210x后立刻就好了。这个坑值得记住。
1.2 /dev目录下的设备节点怎么读懂
Linux下串口设备节点有这么几种常见形式:
- /dev/ttyS0、/dev/ttyS1:这是硬件串口(板载UART),对应老式PC主板上那排针脚或DB9接口。
- /dev/ttyUSB0、/dev/ttyUSB1:USB转串口设备,绝大多数开发板调试和USB转TTL模块都是这种命名。
- /dev/ttyAMA0:树莓派或某些ARM开发板上的板载串口。
- /dev/ttySTM0:部分基于STM32MP1系列芯片的开发板使用这种命名。
查看当前系统里有哪些串口设备,可以在命令行里执行:
ls -l /dev/ttyS* /dev/ttyUSB* /dev/ttyAMA* 2>/dev/null有时候设备节点是存在的,但你往里面读写数据时提示权限不够,看下属性就能明白,比如:
crw-rw---- 1 root dialout 188, 0 Jun 10 10:22 /dev/ttyUSB0这说明只有root用户和dialout用户组的成员才有读写权限。你需要把自己的用户加进dialout组,或者用sudo来临时访问。我建议把当前用户加入dialout组,一劳永逸:
sudo usermod -a -G dialout $USER然后重新登录或执行newgrp dialout,比较符合实际工作习惯。
1.3 setserial命令查看硬件寄存器级别的信息
如果是板载的传统UART(如ttyS0),可以用setserial来查看比较底层的硬件信息:
setserial -g /dev/ttyS0输出类似:
/dev/ttyS0, UART: 16550A, Port: 0x03f8, IRQ: 4这能帮你确认端口基地址、中断号。要是看板卡扩展出来的多串口卡(比如PCIe转8串口),这个命令的价值就更大了。不过插一句,现在大部分人用的USB转串口模块,setserial基本不生效,因为那是虚拟出来的串口,硬件层由USB桥接芯片模拟,跟传统16550 UART两回事。
2. stty命令实战:串口属性配置的核心工具
在Linux下配置串口参数,最核心的命令就是stty。它和C语言里tcsetattr函数全家桶干的是一模一样的事,只不过一个是命令行交互,一个是代码调用。
2.1 查看当前串口属性:stty -F /dev/ttyUSB0 -a
查看串口当前配置,执行:
stty -F /dev/ttyUSB0 -a终端会输出一堆配置项。这里解释一下常见的几个:
- speed 115200 baud:波特率,就是每秒传输的比特数。
- rows 0, columns 0:终端行列数,串口当控制台用时才有意义,做数据透传时可以忽略。
- ispeed 115200、ospeed 115200:输入输出波特率,通常保持一致。
- cs8:数据位是8位。如果是cs7就对应7个数据位。
- -cstopb:1位停止位。带减号表示否定,即“不是两位停止位”。如果看到cstopb,那就是两位停止位。
- -parenb:无校验位。如果看到parenb,说明启用了校验。
- -crtscts:关闭硬件流控(RTS/CTS)。没有减号就是启用了硬件流控。
- -ixon -ixoff:关闭软件流控(XON/XOFF)。串口传二进制数据时,必须关闭软件流控,否则0x13和0x11会被特殊处理,导致数据错乱。
2.2 设置波特率和其他参数:一条命令全搞定
最常用的设置命令,可以直接把波特率、数据位、停止位、校验位一次性配好:
stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb -crtscts -ixon -ixoff如果希望这个设置立即在下次打开时生效就不用操心了,因为stty修改的是内核里tty驱动对象的状态,打开设备时会继承当前状态。在某些场景下,比如你要用cat直接读串口,就可以先配好参数,再cat出来看,比带参数打开工具方便得多。
2.3 原始模式:串口读写二进制数据的保命配置
还有一点非常关键。你如果直接用cat /dev/ttyUSB0这种形式去读设备,默认串口驱动是在“行规范(line discipline)”模式下工作的,你收到的数据会被按行处理,回车换行可能被转换,特殊字符会被解释。这会导致两个问题:一是数据会被破坏,二是读操作会以行缓冲的方式返回,而不是一收到字节就立刻给你。
解决的办法是开启原始模式:
stty -F /dev/ttyUSB0 raw stty -F /dev/ttyUSB0 115200 raw注意,如果先执行了raw后设置波特率,部分老版本stty会把其他参数重置;建议的顺序是波特率后跟raw,或者一条命令写全。实测稳妥的做法是分开两行执行:
stty -F /dev/ttyUSB0 115200 stty -F /dev/ttyUSB0 rawraw模式会关掉行规约的处理,让数据原样进原样出。这就是为什么脚本类工具读串口经常先来一条stty raw的原因。
2.4 清空缓冲区是容易漏的关键动作
串口调试时最易忽略的一步,是切换波特率或重新配置之前,缓冲区里可能积压了旧数据。这时候你打开串口的第一时间会读到一堆乱七八糟的残留数据,容易误判为设备发送的真正的起始数据。
解决的办法是配置完参数后,立刻清空缓冲区:
stty -F /dev/ttyUSB0 -F /dev/ttyUSB0 # 重新配置 python3 -c "import os; fd=os.open('/dev/ttyUSB0', os.O_RDWR|os.O_NOCTTY); os.tcflush(fd, 0); os.close(fd)"用stty本身没有直接的flush选项,所以这里借python一行命令来执行tcflush。tcflush的第二个参数0表示清空接收缓冲区,1表示清空发送缓冲区,2表示两者都清空,一般用2最省事。
实际调试时,我会习惯性地把这段清空动作和stty配置写在同一个shell脚本里,避免每次都重复敲命令。
3. 串口通信实操:从底层读写到工具配置
查看和配置完属性之后,重点自然落到了实际的收发数据上。这里我说两种路径:一是用命令行工具快速验证,二是用python脚本验证较为复杂的数据交互逻辑。
3.1 命令行直接收发数据验证通路
配好串口属性后,最简单的验证方法是往串口写数据:
echo "hello" > /dev/ttyUSB0同时另开一个终端读取:
cat /dev/ttyUSB0如果你的USB转串口模块的TX和RX是短接的(或者通过杜邦线连到另一个模块上),cat端就会收到hello。这个测试虽然简单,但能确认驱动、设备节点、权限、基础属性全部生效。
还有一点要注意,如果你是用开发板和PC连接,开发板的调试串口默认会输出系统日志(console)信息,PC端的cat会不停收到来自开发板的启动日志,这是正常的,不表示数据通路有问题。
3.2 minicom和picocom的快速配置
大部分人不喜欢在纯命令行下敲stty再cat,太原始了。更常用的方式是用minicom或者picocom这类交互式工具。
minicom的配置比较烦人,需要执行sudo minicom -s进入配置菜单,选择“Serial port setup”手动修改设备名和波特率。配置保存后,以后启动才能直接连接。
picocom轻量不少,也适合脚本场景:
picocom -b 115200 -d 8 -p n -s 1 /dev/ttyUSB0退出picocom时注意先按Ctrl+A再按Ctrl+X,这是默认的退出快捷键。如果你直接把终端窗口关掉,可能留下后台进程,下次打开时提示设备被占用。占用问题很常见,排查时可以用:
fuser /dev/ttyUSB0或者:
lsof /dev/ttyUSB0来查看是哪个进程占用了串口,确认后kill掉再重新打开。
3.3 用Python脚本测试收发和超时
工具验证通路没问题后,需要对协议交互逻辑做测试时,python的pyserial库是最顺手的利器。安装很简单:
pip3 install pyserial下面这个脚本是每次调试串口时我必用的基础模板,可以发送一段16进制数据,再等待回应并打印出来:
import serial import time ser = serial.Serial( port='/dev/ttyUSB0', baudrate=115200, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1, write_timeout=1 ) # 清空接收缓冲区 ser.reset_input_buffer() # 发送16进制指令 cmd = bytes.fromhex('01 03 00 00 00 01 84 0A') ser.write(cmd) print('sent:', cmd.hex()) # 读取回应 time.sleep(0.2) data = ser.read(64) print('recv:', data.hex()) ser.close()pyserial的timeout参数是读操作等待的最长时间(秒),单位可以是浮点,比如0.1表示100毫秒。做轮询类指令交互时,这个参数一定要和设备的响应时间匹配,设太长会导致调试卡顿,设太短会频繁读到空数据。
这里有另一个容易踩的坑:在使用pyserial打开串口时,它会用你传入的参数重新配置串口,但如果设备正在被别的工具占用(比如minicom还没退出),serial.Serial会直接抛出SerialException: could not open port,提示设备或资源忙。
3.4 关于串口烧写失败的常见原因
很多人在烧写单片机固件时遇到“串口烧写失败”,这里插一句排查思路。烧写失败的前提是串口通路的物理属性与出厂bootloader要求的参数一致。以STM32为例,芯片内置的bootloader默认就是115200,8N1,无流控。如果烧写软件里配错波特率,或者板子上的BOOT0引脚没有拉高,连接就会失败。
更隐蔽的一个坑是:USB转串口模块的TXD和RXD方向接反了。TXD要接板子的RXD,RXD要接板子的TXD,GND共地。很多人照着插,结果烧录软件提示“芯片超时无应答”,查了半天是线序问题。同样,CH340模块上丝印标注的TXD/RXD是从模块自身角度看的,不是从目标板角度看的,新手特别容易在这里栽跟头。
4. 排查串口问题的思路:典型故障与解决实录
多年调串口下来,遇到过的故障五花八门,但以输出信息为线索,大多能归到几个共性的原因里。我把思考过程和判断依据写出来,比单纯给结论更有参考价值。
4.1 设备节点出现了,但读写无反应
这种场景首先是dmesg确认驱动已attach;ls -l /dev/ttyUSB0的权限也正常;stty配置完也没报错。但echo发出去后另一头就是收不到。这时候多半是硬件接线问题,而不是软件问题。
我在调试一块全志V3s开发板时,就遇到过这种现象。排查步骤是:
- 把TXD和RXD短接,回环测试。echo发出去,cat能不能收到。
- 短接后仍然收不到,用示波器看TX引脚有没有波形翻转。没有示波器的,可以拿万用表量空闲电平和发送数据时的平均电压变化。
- 如果TX引脚电平始终不动,多半是模块坏或者接触不良,换个模块重试。
回环测试是排除串口故障的第一重要手段。它能把问题范围从整条链路迅速缩小到“PC端模块是否有收发能力”,再进一步判断是协议还是时序问题。
4.2 能收到数据,但全是乱码
乱码问题相对好定位。绝大多数情况是波特率不匹配。你配置成115200,设备实际跑的是9600,收到的字节肯定面目全非。要看一眼数据中“看起来像那么回事但不完全对”的状态,很容易和波特率问题混淆。
还有一个原因是校验位和数据位没配对。比如设备是8E1(8位数据位+偶校验+1位停止位),你配置成8N1,接收方在解析时帧错误率会很高,丢帧和乱码交替出现。不过很多USB转串口芯片对校验错误也是直接丢弃数据,所以表现更像“时通时不通”。
还有第三种情况:GND没共地。两个设备之间只连了TXD/RXD,但没有连GND,信号没有统一的参考地电平,数据大概率乱码或者完全不发。这是所有新手最容易犯的错,也不要觉得是低级错误,我见过很多资深工程师临时飞线时也会漏掉GND。
4.3 收发的数据中间丢字节
这类问题常见于高波特率传输大量数据,比如921600或者更高的1.5Mbps、3Mbps。USB转串口的瓶颈往往不在UART芯片本身,而在USB传输的批量模式和系统调度的时延上。
排查思路:
- 降低波特率验证问题是否复现。如果降到460800就不丢了,说明是USB传输和上层调度的极限问题。
- 使用硬件流控RTS/CTS,让对端在缓冲区满时拉低RTS;但这需要对端也支持并且正确接线。
- 修改pyserial的读循环,用大缓存读取并各线程处理,而不是等全部缓存满了一次性读。用read(4096)比read(1)好得多,显著降低丢字节的概率。
如果是开发板和PC之间通信,且通信频率很高时,在应用层加帧头和CRC校验应成为习惯。硬件的丢字节很难完全根除,有的可能和内核USB驱动调度有关,代码加粘包处理和校验是防线。
4.4 串口被占用,打开时报Operation not permitted或Device or resource busy
报错信息区分两种情况。一种是权限问题,提示Operation not permitted,那是当前用户不在dialout组或没有设备节点权限。另一种是Device or resource busy,才是设备真的被别的进程占用了。
排查后者,使用:
sudo lsof /dev/ttyUSB0或者:
sudo fuser -v /dev/ttyUSB0会提示是哪个进程占用的。常见的原因是minicom、picocom、pyserial脚本,甚至systemd启动了串口相关的服务(比如基于串口的ModemManager),它们会自动探测硬件串口,导致你没法独占。ModemManager的问题在有些桌面发行版上尤其明显,直接卸载或禁用:
sudo systemctl disable --now ModemManager这样能有效减少很多莫名其妙的串口占用问题。
4.5 为什么USB转串口模块的波特率有误差
很多国产USB转串口芯片,波特率是有误差的,但这个误差在正常范围内不会导致通信失败。像CH340在波特率较高时误差都在2%以下,UART的标准容忍度大约是±3%,所以短帧通信基本无感。
但如果你的通信数据帧比较长,比如一帧几百字节甚至上千字节,误差累计起来会导致后半帧对不上。处理方式是分帧发送,或者尽量选误差更小的芯片(比如CP2102的精度就好一些)。这个在高速大批量传输时不注意就会变成玄学问题。
5. 不同场景下的串口配置要点
串口没有一把通用的万能配置,不同场景对属性的要求差异很大。我整理了三种典型的场景,写清楚每个场景的配置关注点。
5.1 开发板调试串口(Linux console)
很多ARM开发板的调试串口默认就是控制台(console),Linux启动日志会从这里输出。此时串口参数通常由bootloader传递给内核,比如U-Boot的console=ttyS0,115200。
想临时查看调试串口的输出,直接用PC连接,配成115200 8N1就能看。如果启动日志很多、刷屏很快,可以把终端工具的行数缓冲区加大,或者用script命令把输出存下来:
script -c "picocom -b 115200 /dev/ttyUSB0" boot_log.txt这样能同时保持屏幕显示并把会话记录写到文件,方便事后回看。
5.2 与下位机(MCU)进行协议通信
MCU通信场景注重的是时序和可靠性。通常建议:
- 开启原始模式,防止行规约处理破坏二进制帧。
- 关闭所有流控,除非MCU端明确实现了对应信号。
- 串口参数必须完全匹配MCU固件里UART初始化的配置,不能单方面改PC端。
- 通信时加应用层校验(如CRC16或累加和),物理层错误尽量避免依赖重发机制。
有次我调一个RS485总线上的设备,命令发出去后偶尔会收到设备响应超时,查了很久发现是RS485的方向切换引脚控制时序不对。驱动程序在写数据前先把方向引脚拉高,但没等最后一位发完切回接收状态,导致回应的第一个字节被吞掉。这类问题在串口本身配置层面看不出来,要结合对端设备行为来分析。
5.3 串口作为传感器或数据采集通道
读取GPS模块、气象站、PM2.5传感器这类设备时,一般波特率在9600到115200之间,数据通常是文本或者按行输出的NMEA格式。这种场景下反而不用完全切原始模式,可以保留行规约,配合grep来筛选数据:
cat /dev/ttyUSB0 | grep "$GPRMC"或者用python的readline逐行解析,非常方便。但注意,如果接的是输出二进制数据的模块,还是老老实实开raw模式。
6. 串口相关小工具与工作流建议
平时调试串口,我会组合使用几个小工具,比死磕一个软件顺手得多。
6.1 Linux里的串口调试终端选型
- picocom:适合轻量终端交互,配置简单,支持脚本。
- minicom:功能全面,但配置繁琐,菜单式界面,有些用户觉得不直观。
- tio:比较现代的终端工具,自动保存会话,支持hex显示。
- screen /dev/ttyUSB0 115200:用screen也能连接串口,应急时非常好用,退出用Ctrl+A然后K。
screen /dev/ttyUSB0 115200如果系统没装tio,而screen是每个发行版几乎都有的,那在陌生的服务器上临时调试串口时screen就是最优解。
6.2 16进制显示与监视工具
串口传二进制数据时,cat根本没法看。可以用xxd配合:
cat /dev/ttyUSB0 | xxd或者看纯数据显示+时间戳:
stdbuf -o0 cat /dev/ttyUSB0 | ts '%.s' | tee recv.log用ts命令给每行加时间戳,对分析数据超时问题非常有帮助。这些工具组合起来,不需要图形化串口调试助手也能高效调试。
6.3 写脚本批量设置串口参数
后面需要反复切换不同波特率测试设备时,我会写一个小脚本:
#!/bin/bash # 快速配置串口脚本 DEV=${1:-/dev/ttyUSB0} BAUD=${2:-115200} stty -F $DEV $BAUD cs8 -cstopb -parenb -crtscts -ixon -ixoff raw echo "已配置 $DEV 为 $BAUD 8N1 原始模式"每次调试就是一行命令,省去了查历史命令的麻烦。所谓磨刀不误砍柴工,串口这种事,把命令固化成脚本能减少失误。
7. 一些容易被忽略的串口细节与习惯
最后再聊几个非常细节但我多次因此吃过亏的点。
7.1 拔插USB串口模块后设备名可能变化
如果你同时插了两个USB转串口模块,系统分配ttyUSB0和ttyUSB1的顺序并不一定固定。拔掉一个再插上,原来的设备名可能变了。调试时如果发现设备突然打不开,先用dmesg看一下当前实际分配的名字。如果想固定设备名,可以写udev规则,根据USB设备的序列号或物理端口生成固定的软链(比如/dev/ttymydevice),这样多设备同时跑也不会错乱。
7.2 未使用的引脚也要小心电平冲突
USB转串口模块的TXD在空闲状态是高电平,如果你把两个模块的TXD对接(而不是TXD接RXD),两个高电平驱动打架,轻则数据不通,重则可能损坏芯片。建议先用万用表量量引脚电压再接线,尤其是手头线颜色都一样的杂牌杜邦线,颜色并不能代表实际内部连线。
7.3 串口参数配置后的时效性
stty命令配置的参数只对当前打开的设备对象生效,并不是永久保存的。设备重新插拔或者系统重启后,又会恢复成默认参数。所以脚本化配置在每次会话之前重新执行一遍,别指望之前配好的参数一直有效。
7.4 不要什么都依赖图形化串口助手
在Windows上很多人习惯用友善串口助手、XCOM之类的图形工具,到了Linux上如果只会用GUI工具会非常被动,最小化安装的服务器或者远程调试场景根本没法工作。掌握stty+dmesg+cat/echo这套纯命令行手段,才能在无图形环境下游刃有余。
我把串口调试这件事做了这么多年,最大的体会是:串口本身是极其简单可靠的物理层协议,绝大多数问题出在配置不一致、接线错误、资源占用和工具使用不当上,硬件本身很少掉链子。只要按“硬件识别 -> 权限确认 -> 属性配置 -> 数据收发验证 -> 时序排查”这个顺序一步步来,几乎所有问题都能定位到具体环节。希望这篇文章能让你在Linux下操作串口时少走我踩过的那些弯路。