
如果你最近在做硬件调试、写上位机、或者接了一个和STM32、Arduino、传感器模块相关的项目大概率会搜到pyserial这个名字。简单说pyserial是Python生态里最成熟的串口通信库没有之一。它把操作系统底层的串口API封装成了一个统一的接口让你用几行代码就能打开串口、收发数据完全不用关心Windows、Linux还是macOS之间的差异。这篇内容我按“从零到能用”的思路来写适合刚接触串口通信的Python开发者也适合那些以前只用串口助手、现在想把收发过程自动化的小伙伴。我会把pyserial的安装、核心API、典型读写流程、以及我在实际调试中踩过的坑都过一遍并且给出一份可以直接抄作业的代码模板。这篇内容不是官方文档的翻译而是结合真实调试经验整理的实操笔记看完你应该能独立完成一个简单的串口收发程序。1. 为什么要用pyserial做串口通信在开始写代码之前先搞清楚pyserial在整个串口通信里扮演什么角色比你急着pip install有用得多。1.1 pyserial到底解决了什么问题串口通信本身是个老掉牙的协议RS232、RS485、UART这些名词你可能都听过但它们的底层细节对应用层开发者来说非常繁琐。你想想如果直接调用操作系统APIWindows下是CreateFile、ReadFile、WriteFile那套Win32接口Linux下是open、read、write配合termios结构体macOS又是另一套行为差异。真要自己封装光是处理不同平台的打开参数、超时行为、缓冲区机制就能耗掉你一个礼拜。pyserial把这一切都做了。它对外暴露的是统一的serial.Serial类你只需要告诉它“串口号是多少、波特率多少”剩下的事情库内部处理。实测下来同一个Python脚本在Windows上指定COM3在Linux上指定/dev/ttyUSB0代码逻辑几乎不用改只是端口名字不同。对于做嵌入式联调、写测试脚本、做设备工装的人来说这种跨平台能力省下的时间非常可观的。pyserial能做什么呢几个典型场景和单片机通信发送指令控制LED、电机、舵机这类外设读取传感器模块输出的数据比如GPS模块、指纹模块、激光雷达配合pyqt或tkinter写一个简易上位机界面自动化产线上做设备测试通过串口发指令并校验返回值调试路由器、交换机等网络设备通过console口进去敲命令只要设备露出来的是一个串口pyserial基本都能接管。我自己最常用的是写自动化测试脚本以前要人工拿着串口助手一条条发AT指令现在脚本一键跑完几百条用例谁用谁知道。1.2 和C/C、C#、LabVIEW相比Python这条路值不值这个问题在嵌入式圈子经常被争论。搞硬件的写惯了C觉得Python太“软”实时性不行搞上位机的用C#和WinForm觉得生态成熟LabVIEW粉丝觉得图形化才是王道。我的看法是工具看场景。如果你要给工业设备做一套长期稳定运行的上位机还要求界面漂亮、部署方便C#或Qt C可能更合适。但如果你只是调试期用、写测试脚本、或者做算法验证Pythonpyserial就是最优解。理由很直接Python写起来快改起来更快而且数据处理能力强。比如你通过串口收回来一堆IMU姿态数据想顺手用matplotlib画个曲线Python这边一行导入就行用C#至少得多写几十行临时代码。pyserial本身不承诺硬实时——这是Python的GIL和操作系统调度决定的正好热词里有人提到“Python协程”“多进程”注意这些和串口的硬实时不是一回事。但对绝大多数串口应用来说数据量也就每秒几十到几百字节Python的处理速度绰绰有余。真到了每秒几兆字节的吞吐场景我会直接劝你换个技术栈别为难pyserial。2. 环境准备装好pyserial只是第一步很多新手在“安装”这一步就卡住了而且多半不是pyserial本身的问题是Python环境本身就乱。2.1 先确认Python环境再说安装打开终端或命令提示符输入python --version能正常显示版本号说明Python已经装好。如果Windows下提示“Python was not found; run without arguments to install from the Microsoft Store”说明你的Python没装好或者没加入PATH这和pyserial无关先去把Python正确安装配置好再回来继续。我记得文章开头列的那些热搜词里就有一大堆“python安装”“python环境配置”可见这一关确实拦住了不少人。建议直接用Python 3.8以上的版本太老的版本虽然pyserial也支持但没必要给自己添麻烦。另外强烈建议在虚拟环境里操作避免把系统的Python环境搞乱python -m venv serial_env # Windows: serial_env\Scripts\activate # Linux/macOS: source serial_env/bin/activate当然如果你只是临时跑个脚本不建虚拟环境也能用但万一你电脑上同时有多个Python项目依赖互相打架的时候别怪我没提醒。2.2 安装pyserial的几种方式和验证方法pyserial的安装非常简单PyPI上的包名就叫pyserial用pip安装即可pip install pyserial国内网络如果下载慢可以换用镜像源pip install pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后验证一下是否能用python -c import serial; print(serial.__version__)能打印出版本号就说明安装成功。如果提示ModuleNotFoundError: No module named serial通常是当前解释器和pip的版本对应不上检查一下你是不是在同一个虚拟环境里执行的。另外有个小细节pyserial的操蛋之处在于它的导入名是serial不是pyserial。很多人写成import pyserial结果报错这是新手最常犯的错误。记住安装用pyserial导入用serial。2.3 串口驱动与权限不解决会一直报错这一段是纯经验之谈搜热词的时候看到“win32串口通信”“嵌入器串口通信实验”这些词就知道你们很多都在Windows上做。Windows下插上USB转串口模块比如CH340、CP2102、FT232一般会自动装好驱动在设备管理器里能看到对应的COM口编号。如果你Device Manager里看不到新的COM口大概率是驱动没装去芯片厂商官网下载对应驱动装上就好。Linux下情况不太一样。设备节点通常是/dev/ttyUSB0或/dev/ttyACM0插上后用ls /dev/tty*可以查看。然而即使设备识别到了普通用户打开串口经常报serial.serialutil.SerialException: could not open port /dev/ttyUSB0: [Errno 13] Permission denied: /dev/ttyUSB0这是因为当前用户不在dialout组没有访问串口设备的权限。解决方法sudo usermod -a -G dialout $USER加完组后注销重新登录权限问题就没了。这个坑我当年踩过一次当时还以为是pyserial的bug折腾了半天最后发现就是系统权限。Linux下手边没有组的机器也可以用sudo临时跑脚本但正规做法还是加组别用sudo跑Python权限太大会带来别的问题。3. pyserial基础API一个类打天下pyserial的核心就一个类serial.Serial。别被它庞大的参数列表吓到90%的场景你只需要关心几个关键参数。3.1 Serial构造参数先弄懂这些参数再动手先看一个最基本的打开方式import serial ser serial.Serial( portCOM3, # Windows下是COM3Linux下是/dev/ttyUSB0 baudrate115200, # 波特率要和设备端一致 bytesize8, # 数据位通常8 parityN, # 校验位N无校验E偶校验O奇校验 stopbits1, # 停止位通常1 timeout0.5 # 读超时单位秒 )这些参数每一个都必须和你的设备端配置一致否则收到的就是乱码或者根本收不到数据。端口号、波特率大家肯定都懂我重点说说容易理解偏差的几个。bytesize大多数设备用8位数据位。部分老设备可能用7位看到7就别奇怪是历史遗留。parity最常用的是N如果通信偶发乱码、以及数据感觉对不齐检查一下设备是不是开了奇偶校验两边不一致就会丢字节或报错。stopbits常见的是1也有极少数用2尤其在低速、长线传输时。timeout这个参数极其重要我单独说。timeout是读取操作的超时时间单位秒。它有三种取值行为完全不同timeoutNone阻塞式读取一直等到要求的字节数到齐才返回。如果设备一直不回数据你的程序会永远卡在read那里。timeout0非阻塞读取有数据立即返回没数据立即返回b不等待适合轮询模式。timeout0.5有数据就返回没数据最多等0.5秒。这个折中方案在实际里用得最多。还有两个参数你可能在别的代码里见过xonxoff和rtscts。xonxoff是软件流控rtscts是硬件流控大多数设备都不需要保持默认False即可千万不要乱开。我见过有人代码里开了rtscts结果数据收发完全失败排查半天发现是这个参数在捣乱。3.2 打开与关闭不要小看资源管理pyserial的Serial类有个特点实例化的时候就尝试打开串口。所以你在上面那段代码执行后串口其实已经打开了。当然你也可以先用serial.Serial()创建对象不指定port等拿到端口名后再用ser.open()手动打开。手动打开适合程序里动态扫描端口的情况比如你不想硬编码COM口号而是让用户选择或者程序自动侦测设备插在哪个COM口上。关闭串口用ser.close()但是更推荐的做法是用上下文管理器。pyserial支持with语法代码块执行完自动关闭串口不用手动管with serial.Serial(COM3, 115200, timeout0.5) as ser: ser.write(bAT\r\n) response ser.readline() print(response) # 到这里串口已经自动关闭了这个写法的好处是即使程序中途抛异常串口也能被正常释放。串口资源是很宝贵的一个程序没释放串口其他程序就开不了设备管理器里会显示“该端口已被占用”。用with是养成好习惯的第一步。3.3 读写API与编码细节串口读写的最小单位是字节。write方法接受bytes类型不接受str。所以你要发送字符串必须手动编码ser.write(bAT\r\n) # 直接写字节 ser.write(AT\r\n.encode()) # 字符串转字节 ser.write(AT\r\n.encode(ascii)) # 也可以指定编码读这边有几种方式data ser.read(10) # 读取10个字节 data ser.readline() # 读取一行遇到换行符结束 data ser.read_until(b\r\n) # 读到你指定的终止符 data ser.read_all() # 读取当前缓冲区所有数据 chunk ser.read(ser.in_waiting) # 读走当前已收到的全部字节read和readline是最常用的。readline有一个坑它虽然“按行”读但依赖换行符来判定结束。很多串口设备返回的行结尾不是常见的\n而是\r\n你用readline()时如果设备只发\r而不是\r\n会一直等不到换行导致超时。后面我在问题排查部分再展开。接收回来的字节同样是bytes类型要转成字符串的话text data.decode(utf-8)这里有个很容易炸的坑设备返回的编码不一定是UTF-8有很多老设备用的是GBK或ASCII。直接decode(utf-8)遇到无法解析的字节会抛UnicodeDecodeError。稳妥做法是用errorsignore或errorsreplace参数来容错text data.decode(utf-8, errorsignore)另外还有一个细节数据不一定一次到位。串口数据是流式的你第一次read(10)可能只读到3个字节第二次才读到7个。不是因为pyserial有问题而是底层缓冲区就是这样。所以做完整协议解析时通常要自己拼包和分包这里先记住这个知识点后面实操部分我会给出处理思路。4. 从零写一个可用的串口收发程序光看API不够直接上一份能跑的完整代码。这个例子模拟的是“发AT指令、等设备回应”的过程也是我做模组调试时最常用的套路。4.1 最小可用demo轮询读取的写法代码逻辑打开串口发送指令轮询等待数据回包超时则放弃。import serial import time def send_at_command(ser, cmd, wait_time1.0): # 清空接收缓冲区避免读到上一次的残留数据 ser.reset_input_buffer() # 发送指令 ser.write(cmd.encode(ascii)) # 等待设备响应 deadline time.time() wait_time response b while time.time() deadline: # 读取当前所有可用数据 chunk ser.read(ser.in_waiting) if chunk: response chunk time.sleep(0.05) # 避免空转占用过高CPU return response.decode(utf-8, errorsignore) if __name__ __main__: try: with serial.Serial(COM3, 115200, timeout0.2) as ser: ret send_at_command(ser, AT\r\n) print(设备返回:, repr(ret)) except serial.SerialException as e: print(串口打开失败:, e)这段代码里有几个细节我说一下。send_at_command里先调用reset_input_buffer是为了清掉上一次通信留下的旧数据。串口不像网络连接没有“会话”概念缓冲区里残留的字节会混入本次响应导致解析错乱。每次发指令前清一次缓冲是串口调试的基本习惯。然后是while循环里的time.sleep(0.05)。有人可能觉得多此一举直接无限循环读不好吗不好。不加sleep的话这个循环会以极快的速度狂刷readCPU占用会飙升而且会增加系统调用的开销。加个50毫秒的休眠对0.5秒到1秒级别的响应来说完全够用CPU占用还低。ser.read(ser.in_waiting)这里的in_waiting属性返回当前接收缓冲区中的字节数。这么写的效果是有多少读多少不会阻塞等待。配合timeout0.2万一在sleep间隔里来了数据read也能及时取走不会丢数据。4.2 阻塞式读取与readline的坑轮询方式适合“发一条、收一条”的交互式通信。但有些设备是主动上报数据的比如GPS模块每秒输出一帧NMEA语句。这种场景下用阻塞式读取更自然import serial ser serial.Serial(COM3, 115200, timeout1.0) try: while True: line ser.readline() if line: print(line.decode(utf-8, errorsignore).strip()) except KeyboardInterrupt: print(停止接收) finally: ser.close()这段代码的意思是一直读每读到一行就打印一行。readline的行为是阻塞等待直到收够一个换行符才返回。注意我把timeout设成了1.0意思是如果1秒内一个字节都没收到readline返回b这样主循环还能继续跑不至于永久卡死。如果timeoutNone那readline会一直等直到设备蹦出个换行符来。对主动上报型设备来说timeoutNone反而可能更合适因为设备会稳定输出不太会“断流”。这里有个真实踩坑案例。我之前调一个工业传感器模块设备文档说数据以\r\n结尾。用readline()去读读出来的数据总是缺前面的部分而且经常超时。排查半天后发现问题出在设备实际发送的是\r不是\r\n。而readline默认只认\n作为行结束符看到\r不结束继续傻等。后来我改用read_until(b\r)问题立刻解决。所以当你发现readline不按预期工作先别急着怀疑pyserial用串口助手看一下设备到底发的什么字节再选择合适的终止符。4.3 用线程做持续接收再进阶一步。如果你的程序既要持续接收设备数据又要响应用户输入、或者同时处理界面事件就不能在主线程里死循环读串口了否则界面会卡死。解决方案是开一个后台线程专门收数据主线程干别的。下面是一个简单的线程化接收模板import serial import threading import queue import time class SerialReader(threading.Thread): def __init__(self, ser, data_queue): super().__init__(daemonTrue) self.ser ser self.data_queue data_queue self.running True def run(self): while self.running: try: # 等待最多0.5秒避免无限阻塞无法退出 data self.ser.read(1024) if data: self.data_queue.put(data) except serial.SerialException: break def stop(self): self.running False if __name__ __main__: ser serial.Serial(COM3, 115200, timeout0.5) q queue.Queue() reader SerialReader(ser, q) reader.start() try: while True: try: data q.get(timeout0.5) print(收到:, data.hex()) except queue.Empty: pass # 这里可以干别的事比如处理界面事件 except KeyboardInterrupt: reader.stop() reader.join() ser.close()为什么要把读到的数据放到queue里因为串口对象不是线程安全的如果多个线程同时调用read或write可能出现数据错乱。用队列做解耦生产线程串口读线程只管往队列塞数据消费线程主线程只管从队列取数据两边不直接碰串口对象避免了竞争问题。发送操作也建议集中管理。如果你有多个地方都要往串口写数据最好定义一个writer线程或者给write操作加锁。否则两个线程同时write字节会交错在一起设备那边就懵了。5. 真实调试中会踩的坑我帮你提前排掉最后这部分是精华。这些坑都是实际调设备时遇到过的每一条都有人线上问过。我整理成速查表你在排查时按图索骥就行。5.1 端口打不开的几种原因报错信息一般是serial.serialutil.SerialException: could not open port COM3。逐项排查原因现象解决办法端口号不对报错提示PortNotFoundError去设备管理器/设备树确认实际端口号串口被其他程序占用报错PermissionError关闭串口助手、其他Python脚本、固件下载工具驱动没装好设备管理器找不到COM口安装CH340/CP2102等驱动权限不足Linux下Permission denied把用户加到dialout组或查一下是不是用sudo跑的USB线/模块故障设备管理器里设备带黄色感叹号换线、换USB口、换模块测试这里特别说一句很多调试板子自带的串口芯片是CH340这个芯片在某些便宜的USB Hub上供电不足会导致无法识别遇到“设备偶尔出现、偶尔消失”的情况优先想想供电问题。另外如果你用的是串口助手它会占用串口此时再运行Python脚本就打不开。开发时一定要记住串口是独占的同一时间只能有一个程序打开。我一般调代码的时候会把串口助手先关掉改回用Python的日志打印来看数据这个习惯能避免很多无谓的“串口打不开”问题。5.2 数据乱码和不完整的处理思路乱码首先检查波特率。设备用9600你代码里用115200收出来的就是一堆“砖块”。波特率对齐是一切通信的基础其次是数据位、校验位、停止位这四个参数任何一位不对数据都不可能正确。排除参数问题后再考虑编码问题。设备输出的是GBK编码的汉字你用utf-8去解码肯定乱码。这一步需要你查设备的用户手册或者用串口助手先看原始字节才能判断正确的编码。还有一种隐蔽情况半包和粘包。串口数据流被操作系统切成一段一段的一次read不一定能读到完整的一帧数据。比如设备发送的数据帧是“AA 55 01 02 03 04”如果你在中间某个时刻去read可能只读到“AA 55 01”剩下的“02 03 04”还在路上。处理这种问题不能在read之后马上按照“一帧数据”去解析而是要维护一个接收缓冲区不断追加新数据然后尝试从缓冲区里取出完整的一帧。协议解析的经典做法是定义一个帧头、帧长度然后通过状态机或者简单的循环来拆包buffer b while True: chunk ser.read(1024) if not chunk: continue buffer chunk # 假设帧头是0xAA 0x55帧长度在第3个字节 while len(buffer) 4: if buffer[0] 0xAA and buffer[1] 0x55: frame_len buffer[2] if len(buffer) frame_len: frame buffer[:frame_len] buffer buffer[frame_len:] print(完整帧:, frame.hex()) else: # 数据还没齐继续等 break else: # 没找到帧头丢弃一个字节 buffer buffer[1:]先别急着套你的协议理解这个思路就行串口数据是流式的协议解析必须自己组帧。5.3 硬件联调时的几个习惯调串口和调网络程序心态不一样。串口没有TCP那样的重传机制发出去的字节丢了就是丢了设备不回也没有错误提示。所以开发流程上我有几个固定习惯第一个习惯先确认链路通不通。写了半天代码发现设备没反应先用串口助手手动发一条指令看有没有回包。如果串口助手都没有回包那就是设备、接线、参数的问题别急着改代码。等串口助手里确认数据能通再上pyserial调自己的代码。这一条能帮你把“硬件问题”和“软件问题”快速分开。第二个习惯把波特率、端口号、超时这些参数做成配置文件或者命令行参数不要硬编码在代码里。设备换了调试口或者波特率改了改一行配置重启程序就行不用重新编辑代码。我自己常用的做法是用argparse或者configparser简单维护一个配置文件。第三个习惯用好hex视图。串口调试时文本视图会隐藏很多细节比如\u0000这种控制字符看起来就是个方框。用hex格式打印接收数据才能准确判断设备到底发了什么字节尤其调试二进制协议时必须上hex。第四个习惯日志记录。用Python的logging模块把每次发送的指令和接收的原始数据记录下来特别是跑自动化测试时日志是定位问题的重要线索。pyserial本身没有日志功能但你在自己的代码层加几行日志非常简单。6. 结语我的一点经验做串口通信这些年最大的感受是pyserial真的很简单复杂的是它背后的设备、协议和硬件。别把精力都花在研究Serial类的参数上多花点时间搞清楚你的设备端发的是什么、收的是什么、协议怎么定义的这些才是串口调试的核心能力。我见过很多人在网上问“为什么readline收不到数据”然后在屁股后面刷屏回复几十条。这种问题的答案往往就藏在一个细节里设备回车的字节形式。用串口助手看一眼设备原生返回胜过盲目改代码猜半天。如果你现在刚开始接触pyserial我的建议是先把波特率、端口号确认好然后用最简单的一段代码把收发跑通再一点点加功能。串口调试本身就是个迭代的过程代码写得再漂亮设备连不通一切都是白搭。先把车跑起来再去考虑方向盘怎么打这是最快的路径。