ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python串口通信实战:pyserial库核心用法与避坑指南

Python串口通信实战:pyserial库核心用法与避坑指南 1. 用Python调串口为什么偏偏是pyserial我最早接触串口通信是在调试一块STM32开发板的时候。那时候的日常工作方式很简单开发板通过USB转TTL模块连到电脑打开串口调试助手手动输入十六进制指令然后盯着接收区看设备返回的原始数据。一两条指令还好但一旦需要连续发送几十条、几百条指令或者需要周期性地采集传感器数据、做回环测试、保存日志串口助手就明显不够用了——手工操作容易漏数据复制出来还要自己整理格式更别提想画个曲线图看看数据趋势几乎不可能。后来我换成了Python配合pyserial库来处理这件事。串口通信的本质其实不复杂就是两个设备通过串行线路按约定的波特率逐位传输数据。而pyserial做的事情就是把这个过程封装成Python里简单的API调用打开串口、读取数据、写入数据、关闭串口。它不依赖额外的硬件不需要复杂的驱动配置只要电脑能识别到串口设备pyserial就能接管它。pyserial能解决的痛点很明确当你的串口通信需求超出了偶尔手动收发几条数据的范畴时用脚本替代人肉操作。它特别适合这几类人——做嵌入式开发需要快速验证通信协议的人写自动化测试脚本需要和设备交互的测试工程师做上位机工具但不打算碰C或C#的Python开发者以及刚入门Python想学硬件控制的学生。我自己用下来的体会是pyserial最大的优势不是功能多而是够用且省事。它不要求你理解底层驱动细节不需要装额外的运行时环境一条pip命令装完就能跑。对于绝大多数串口应用场景——读传感器数据、控制开发板、和工控设备通信——它提供的功能已经完全覆盖。这也是这篇文章想讲清楚的核心pyserial基础到底怎么用有哪些容易踩的坑以及怎么把一套能跑的代码变成稳定可靠的通信代码。2. 安装和第一个连通Demo先把串口打开再说2.1 环境准备pip安装与串口识别装pyserial没什么特殊要求Python 3.6以上版本基本都能直接用。我用的是Windows 10加Python 3.10的组合其他平台流程也一致。安装命令很简单pip install pyserial如果你想装到用户目录避免污染全局环境可以用pip install --user pyserial安装完成后验证一下版本python -c import serial; print(serial.__version__)能打印出版本号就说明装好了。这里有一个很多新手会忽略的问题装完pyserial之后你首先需要确认系统到底认不认你的串口设备。Windows上插上USB转串口模块后打开设备管理器展开端口(COM和LPT)能看到类似USB-SERIAL CH340 (COM3)的条目记住这个COM口号。Linux环境下则不同插入设备后一般会生成/dev/ttyUSB0或/dev/ttyACM0这样的设备节点可以用ls /dev/ttyUSB*查看。提示Windows下如果插上设备没有任何反应先检查驱动。CH340、CP2102这类常用的USB转串口芯片需要装对应驱动否则系统只会识别为一个未知设备。这是新手第一道坎和pyserial本身没关系。2.2 第一个能跑的Demo代码装好之后最想做的事当然是赶紧跑通一个连接。下面这段代码是最小可用的串口通信示例作用很纯粹打开串口、发送一条指令、读取设备返回的数据、关闭串口。import serial # 打开串口参数按实际情况修改 ser serial.Serial( portCOM3, # Windows下用COM口Linux下用/dev/ttyUSB0 baudrate115200, # 波特率和设备端保持一致 timeout1 # 读超时时间单位秒 ) # 确认串口是否真的打开了 if ser.is_open: print(f串口 {ser.port} 打开成功参数: {ser.baudrate}) # 发送一条ASCII指令注意要编码成bytes ser.write(bAT\r\n) # 以AT指令为例 # 读取设备返回的数据最多读64字节 data ser.read(64) print(收到的原始数据:, data) # 关闭串口 ser.close() print(串口已关闭)这段代码看起来简单但里面有几个点必须先搞明白否则后面调试会很痛苦。第一个是serial.Serial()构造函数的执行时机。这行代码一旦执行pyserial就会立即尝试打开指定的串口。如果端口不存在、被占用或者没有权限代码会直接抛出SerialException异常。所以实际项目中不建议裸写这行最好用try/except包一层后面我会专门讲异常处理。第二个是write方法必须传bytes类型。很多新手在这里卡住ser.write(AT\r\n)执行之后报错TypeError: unicode strings are not supported。原因很简单串口传输的是字节流不是字符串所以字符串必须编码成bytes才能发送。bAT\r\n是bytes字面量如果你需要动态拼接字符串就要用AT\r\n.encode(utf-8)这样的方式转换。但这里又有一个坑编码方式要和设备端约定一致。如果设备端默认是ASCII或GBK而你用UTF-8编码了中文字符设备端可能解析不出来。第三个是timeout参数的作用。它控制的是read操作在没有数据时最多等多久。我把timeout1设成1秒意思就是如果串口缓冲区里没有数据ser.read(64)最多阻塞1秒然后返回空字节串。如果不设timeout默认是Noneread会一直等下去直到读满64字节才返回——这在大多数交互式场景下会导致程序看起来像卡死了。2.3 设备端没有响应先分清是发不出去还是收不到跑第一个Demo的时候很多人会遇到一个让人抓狂的情况代码没报错串口也打开了但就是读不到任何数据。这时候不要急着改代码先做一个最简单的自我验证——回环测试。把USB转串口模块的TXD和RXD引脚用杜邦线短接然后运行下面这段代码import serial ser serial.Serial(COM3, 115200, timeout1) ser.write(bhello loopback) # 由于TXD和RXD短接自己发出去的数据会从RXD回到自己的接收缓冲区 data ser.read(64) print(data) # 应该打印 bhello loopback ser.close()如果打印出来的就是你发送的内容说明串口通信链路本身没问题问题大概率出在设备端协议或者接线方式上。如果什么都读不到则需要检查串口号是不是选错了或者USB转串口模块是否正常工作。这个回环测试我强烈建议每个人都做一遍。它不是浪费时间而是在帮你把串口通信链路是否正常和代码逻辑是否正确这两个变量分开排查。硬件的坑和软件的坑混在一起是最难调的先排除一个方向剩下的就好办了。3. serial.Serial参数不是随便填的端口、波特率与超时的细节3.1 端口名Windows和Linux的写法差异串口通信的第一步是找到正确的端口名。Windows下端口名格式是COM3、COM5这样Linux下是/dev/ttyUSB0、/dev/ttyACM0。macOS是/dev/cu.usbserial-xxx。这个看着简单但实际项目里端口名是会变的——今天插的是COM3明天换个USB口可能就变成COM7了。所以正经用pyserial写工具的时候不应该把端口名写死。我的习惯是启动时让用户输入或者自动枚举可用端口。pyserial自带一个枚举串口的方法serial.tools.list_ports.comports()可以列出当前系统所有可用的串口import serial.tools.list_ports ports serial.tools.list_ports.comports() for p in ports: print(p.device, p.description)输出大概是这样的COM3 USB-SERIAL CH340 (COM3) COM7 USB Serial Port (COM7)这样就能在程序里动态选择第一个可用的串口或者通过description字段里的芯片型号来匹配指定的设备。尤其是当电脑上插着多个USB转串口模块时用description去匹配要比让用户自己试错友好得多。3.2 波特率为什么必须和设备端严丝合缝波特率是串口通信里最核心的参数之一。它表示每秒传输的bit数单位是bps。比如115200意味着每秒传115200个bit换算成字节就是差不多每秒11520字节按8bit数据位1bit起始位1bit停止位实际有效数据要打折扣。通信双方的波特率必须一致否则收端采样到的电平时序就是错的解析出来的数据全是乱码。这不难理解就像两个人打电话一个人正常语速说话另一个人用0.5倍速去听听到的内容肯定是支离破碎的。常见的波特率有9600、19200、38400、57600、115200工业设备和老式模块常用9600较新的设备一般都支持115200。具体用哪个以设备手册为准不要凭感觉猜。3.3 timeout的三种设置None、0和正数效果完全不同timeout这个参数是很多初学者最容易理解错的地方。它控制的是读操作在没有数据时的等待行为有三种情况timeout值行为适用场景None阻塞读直到读到指定字节数才返回明确知道设备每次会返回固定字节数的协议0非阻塞读立即返回当前缓冲区已有数据轮询读取不想让程序卡在read上正数最多等待这么多秒超时后返回已读到的数据大多数交互式指令响应场景举个例子更直观。设备每收到一条指令就返回100字节的数据你用ser.read(100)去读timeoutNone会一直阻塞到收到100字节为止期间程序什么都干不了。如果设备出了故障只返回50字节程序就会一直卡在那里。timeout0则完全不等待如果缓冲区还没收到数据read立刻返回空。timeout2则最多等2秒2秒内如果收到了50字节就返回这50字节如果2秒什么都没收到就返回空字节串。第三种设置最实用。它既不会无限等待也不会因为太过抢跑而读不到数据。这里有个隐藏的坑返回的数据长度不代表协议完整。ser.read(100)如果只返回了50字节说明数据还没收完需要继续读或者缓存拼接。这也是很多刚从串口助手转向pyserial的人会困惑的地方——串口助手打开就能持续收到数据流但pyserial的read是一次性行为收多少、等多久都在你的代码控制里。3.4 其他参数数据位、校验位、停止位以及流控除了端口、波特率和超时serial.Serial还有几个默认参数一般不需要改但遇到奇葩设备时你得知道它们存在。默认值是bytesize8、parityN、stopbits1合起来就是8N1这是串口通信事实上的标准配置。bytesize数据位可选5/6/7/8。大多数设备都是8。parity校验位N无校验、E偶校验、O奇校验。如果用校验位相当于在数据帧里多了一位用于错误检测。stopbits停止位1或2。它表示一帧数据结束后的间隔电平持续时长。xonxoff和rtscts软件流控和硬件流控默认都是关闭的。大部分场景不需要开开了反而容易出问题。举一个真实例子。我调试过一个老式的称重仪表它要求数据格式是7位数据位、偶校验、1位停止位7E1。如果我用默认的8N1去打开串口仪表返回的数据解析出来是乱码。当时排查半天最后翻设备手册才意识到是数据格式不匹配。所以当你的通信对象不是普通单片机或开发板的时候先翻手册确认这三个参数不要想当然直接用默认值。4. 数据读写实战从write/read到readline与read_until4.1 发送数据encode与write的配合前面提到过write方法只接受bytes类型。实际开发中需要发送的数据往往不是硬编码常量而是动态拼接的字符串。规范的写法是cmd READ_TEMP ser.write(cmd.encode(ascii) b\r\n)这里的\r\n是常见的行结束符很多设备用它来判定一条指令的结束。不同设备的结束符不一样有的是\r有的是\n有的是\r\n。如果发出去的指令设备端没反应先检查结束符是不是搞错了。我见过有设备用\r做结束符用\n发过去设备完全无视。还有一个细节是flush方法。write只是把数据写入了发送缓冲区并不保证数据已经真正从串口物理端口发出去了。大多数情况下pyserial会立刻发送但在某些场景下比如紧接着就要读数据建议调用ser.flush()强制把缓冲区里的数据全部发送出去再开始读操作。4.2 读取数据raw bytes与文本模式的转换读取是串口通信里花样最多的部分。设备返回的数据本质上是字节流也就是bytes类型。打印出来是b\x01\x02\x10\x00这样的形式或者如果是ASCII文本就是b25.6\r\n。如果你读的是ASCII文本可以把bytes解码成字符串再处理raw ser.read(64) text raw.decode(ascii).strip() print(text)这里要注意decode可能因为字节不完整而抛出UnicodeDecodeError。比如设备分两批返回了25.6\r和\n你先读到的是b25.6\r如果直接decode(ascii)没问题但如果设备有中文输出就要小心GBK和UTF-8的差异了。稳妥的做法是先把读取的字节累积起来等收到了完整的结束符再decode。4.3 readline按行读取的便利与陷阱readline按行读取数据遇到换行符\n就返回。对于输出人类可读文本的设备来说非常方便line ser.readline() print(line) # b25.6\r\n但它有它的限制。readline的行为依赖于行的结束符如果设备返回的数据没有换行符readline会一直读到超时为止。另一个坑是readline读回来的内容包含行结束符你通常需要strip()去掉\r\n。还要注意如果设备发送的一行超过缓冲区限制会被截断默认的行长限制是size参数指定的长度。实测的经验是readline适合设备明确按行返回文本的场景比如AT指令模块、GPS模块、串口打印机。对于返回二进制协议数据的设备我更推荐用read_until或直接按字节数读。4.4 read_until按自定义结束符读取比readline更灵活的是read_until它允许你指定任意结束符# 一直读到收到 bEND 为止 data ser.read_until(bEND)这个在处理有固定帧尾的二进制协议时特别好用。比如某个传感器一帧数据的格式是AA 55 08 00 01 02 03 04 BB帧尾是BB那么可以用ser.read_until(b\xBB)把一整帧读出来。它也是超时机制的超时后返回已经收到的数据所以读取后最好判断一下数据是否真的以指定的结束符结尾。4.5 一个完整的指令-响应示例把上面这些串起来写一个常见的场景发送AT指令读取设备返回的完整响应。import serial ser serial.Serial(COM3, 115200, timeout2) def send_at_cmd(cmd: str) - str: ser.reset_input_buffer() # 清空接收缓冲区避免读到上一次的残留数据 ser.write((cmd \r\n).encode(ascii)) ser.flush() # 设备一般会返回多行文本读到一个空行为止 lines [] while True: try: line ser.readline().decode(ascii).strip() except UnicodeDecodeError: line ser.readline().decode(ascii, errorsignore).strip() if not line: # 读到空行说明响应结束这是一个约定根据设备实际行为调整 break lines.append(line) return \n.join(lines) print(send_at_cmd(AT)) print(send_at_cmd(ATVERSION)) ser.close()这个例子里有几个点值得解释。reset_input_buffer()很重要——如果上一次通信有残留数据没读干净下次发送命令后这些残留数据会混进响应里导致解析出错。读多行响应的循环里判断读到空行就结束是我例举的约定实际项目中设备可能不会返回空行或者以其他方式标识响应结束你需要根据协议手册调整这个条件。总之理解代码里的每个行为再针对你的设备做适配。5. 串口打不开、乱码、丢数据基础阶段最常踩的坑5.1 串口被占用和拒绝访问先怀疑你的调试工具初学者使用pyserial时报错最常见的就是打开串口时报SerialException: could not open port COM3: PermissionError(13, 拒绝访问。, None, 34)。这类报错最常见的原因只有一个:串口被其他程序占用了。很多人在跑pyserial脚本的同时还开着串口调试助手、Arduino IDE的串口监视器、或者其他任何占用串口的工具。串口这种设备同一时刻只允许一个程序独占访问。比如设备的波特率是115200你打开串口时设成9600收回来就全是乱码。这不是pyserial的问题是波特率不匹配的问题。另一种可能多了个校验位配置——设备手册写的是偶校验你却用了默认的无校验。程序检测到串口不可用或无法读数据先看接口层的返回码或日志才是正确方向逐层排查不要一上来就怀疑pyserial本身。提示如果你需要既用pyserial又不关串口助手可以考虑用虚拟串口工具如com0com做串口转发或者干脆只在调试时开一个工具跑脚本时关闭其他占用程序。这个习惯能省掉很多误判的时间。5.3 丢数据与粘包读取时机和缓冲区的处理比乱码更隐蔽的问题是丢数据。现象是设备明明返回了20个字节ser.read(64)却只读到了15个字节或者读到的内容里混着上一次响应的残余数据。丢数据的根因在于你没有给设备足够的时间把数据全部发出来或者你没在正确的时机开始读。串口通信是异步的设备收到指令后需要一定的时间处理和发送数据。你写指令后立刻读设备的数据可能还在路上read超时后返回的自然是半截数据。我的处理习惯是发送命令后根据设备的响应时间经验适当让程序等一下再读取。这个等可以用time.sleep(0.1)实现更精确的做法是先读一点判断是否有数据没有就继续等import time ser.write(bREAD\r\n) time.sleep(0.2) # 给设备留出响应时间 # 循环读取直到读完缓冲区所有数据 data bytearray() while ser.in_waiting: data.extend(ser.read(ser.in_waiting)) time.sleep(0.05) # 给设备更多时间发送后续数据in_waiting属性返回的是接收缓冲区里等待读取的字节数。这个属性在循环读取时很好用可以确保把缓冲区里的数据全部取出来。不过要注意in_waiting也可能一直是0因为设备还没把数据发过来。所以循环里加一个sleep适当让出CPU给串口驱动填充缓冲区。5.4 编写一个带异常处理的健壮串口打开函数最后一个实用建议是把串口打开的逻辑封装成带异常处理的函数。这样在串口不可用的时候程序不会直接崩溃而是给出明确的提示信息。import serial from serial.serialutil import SerialException def open_serial(port, baudrate, timeout2): try: ser serial.Serial(port, baudrate, timeouttimeout) return ser except SerialException as e: print(f串口 {port} 打开失败: {e}) print(请检查串口是否被占用端口名是否正确设备是否连接) return None ser open_serial(COM3, 115200) if ser is None: exit(1)6. 基础向的下一步线程读取、可视化与实测建议pyserial的基础功能到这基本讲完了。但基础只是起点实际应用里往往需要更工程化的处理。这里给几个我认为最有价值的下一步方向。**线程化读取。**前面所有示例都是发送一条指令、等一条响应的同步模式。但一些设备会主动向上位机推送数据比如心率传感器每秒发一次数据包或者GPS模块持续输出定位信息。如果业务逻辑还是写一条读一条会漏掉大量推送数据。更合理的方法是开一个后台线程循环读数据并把数据放入队列主线程从队列里取数据进行处理import serial import queue import threading import time data_queue queue.Queue() def read_from_port(ser): while True: if ser.in_waiting 0: data ser.read(ser.in_waiting) data_queue.put(data) time.sleep(0.01) ser serial.Serial(COM3, 115200, timeout0.1) t threading.Thread(targetread_from_port, args(ser,), daemonTrue) t.start() while True: if not data_queue.empty(): data data_queue.get() print(收到:, data)这种模式的好处是读数据不阻塞主线程想干嘛就干嘛数据来了自然会被后台线程接收。**数据可视化。**串口数据流配合matplotlib或者pyqtgraph可以实时画曲线。比如读取温度传感器数据存成列表后用plt.plot()画出来比在串口助手里看数字直观得多。很多做硬件开发的朋友就是用这种方式搭建简易的上位机界面。**日志保存。**用pyserial配合pandas、csv模块或者sqlite把串口数据落盘方便后续分析和回放。这在长时间的测试记录场景非常有用。实测建议方面我想说一件很多教程不会提的事串口通信的稳定性很大程度上取决于你的硬件链路而不是代码。USB转串口模块的质量、杜邦线的长短、周围是否有强电磁干扰——这些都会影响通信质量。如果你的数据时不时出错先用手头的示波器或逻辑分析仪看看波形再回来怀疑代码。总把锅甩给pyserial是不公平的它就是一根管子管子本身很少出问题。最后说一下我自己的习惯基础阶段的代码我一般会先写成一次性脚本跑通了再封装成类。类方法把打开、关闭、发送、接收、解析这几个动作分别抽出来之后无论是做GUI还是做自动化测试都能直接复用。如果你们第一次接触pyserial建议按这个路线走一遍装库、回环测试、读写demo、处理异常、加上线程读取。每一步都跑通了串口通信这块的基础就算是打牢了。之后再看pyserial-asyncio这类异步扩展也会顺畅很多。
返回列表