行空板串口通讯全攻略:从PySerial安装到多线程数据采集实战

发布时间:2026/7/28 6:06:40
行空板串口通讯全攻略:从PySerial安装到多线程数据采集实战 1. 项目缘起为什么行空板上的串口通讯值得单独聊聊最近在折腾行空板想用它连接一些传感器或者老旧的单片机设备发现一个挺普遍的需求通过USB转串口进行通讯。这听起来是个基础操作但真上手了才发现行空板这个基于Linux的Python学习平台在串口通讯这块儿和我们在Windows上用串口助手或者在Arduino IDE里点点鼠标体验完全不一样。网上搜“行空板 pyserial”资料要么太零碎只给一两行代码要么就直接跳过了最关键的权限和端口识别问题让新手一头雾水。我自己也是踩了几个坑才跑通的。比如明明插上了USB转TTL模块ls /dev/tty*却找不到设备或者找到了设备一运行pyserial就报权限错误PermissionError: [Errno 13] Permission denied再或者读写数据时一片寂静根本不知道是代码问题还是硬件没接对。所以我觉得有必要把从硬件连接到软件调试的完整链条梳理清楚尤其是那些教程里常常一笔带过但实际卡住你半小时一小时的“魔鬼细节”。这篇文章我就结合自己的实操把行空板上用pyserial玩转USB串口的门道讲透目标是让你看完就能自己动手连起来、调起来。2. 行空板串口通讯基础硬件连接与系统认知在行空板上进行串口通讯第一步不是写代码而是先理解它的硬件接口和Linux系统对串口设备的管理方式。这和直接用Arduino的Serial库有本质区别。2.1 行空板的USB端口与串口转换行空板通常自带多个USB-A型接口。这些接口本身是USB主机口不能直接输出TTL电平的串口信号TX、RX、GND。因此我们需要一个USB转TTL串口模块比如常见的CH340、CP2102、FT232等芯片的方案作为桥梁。连接示意图与要点行空板USB口 --USB线-- USB转TTL模块 --杜邦线-- 目标设备如Arduino、STM32、传感器关键的一步是电平匹配。行空板的GPIO和大多数USB转TTL模块的工作电压是3.3V。在连接前务必确认你的模块支持3.3V电平输出通常模块上有一个跳线帽或开关来选择3.3V或5V。如果误用了5V电平的TX线连接到行空板或其它3.3V设备的RX引脚有损坏硬件的风险。注意连接时遵循“交叉互联”原则模块的TX引脚接目标设备的RX引脚模块的RX引脚接目标设备的TX引脚两边的GND地线必须相连。这是串口通讯的基石接反了数据无法传输。2.2 Linux下的串口设备文件这是核心概念。在Windows上串口叫COM1、COM2在Linux系统包括行空板运行的Debian里一切皆文件串口被抽象成/dev目录下的一个设备文件。USB转串口模块插入后系统会自动为其创建这样的文件。最常见的命名是/dev/ttyUSB0、/dev/ttyUSB1按插入顺序递增。如果你的模块使用的是CDC-ACM驱动像某些Arduino板载的USB转串口它可能会显示为/dev/ttyACM0。在行空板的终端里你可以通过以下命令来查看ls /dev/ttyU* /dev/ttyA*插入模块前执行一次插入后再执行一次多出来的那个设备文件就是你需要的串口。例如插入后出现了/dev/ttyUSB0那么在你的Python代码里就需要指定这个端口。2.3 权限问题为什么直接运行会报错这是新手遇到的第一个“拦路虎”。在Linux系统中普通用户默认没有直接访问硬件设备文件如/dev/ttyUSB0的权限。当你用pyserial去打开这个端口时就会触发PermissionError。有几种解决方法各有利弊使用sudo运行程序在终端执行sudo python your_script.py。这是最快捷的测试方式但不适合作为最终方案因为让Python脚本以root权限运行存在安全风险且在某些图形化或远程开发环境中不方便。将用户加入dialout组这是推荐的做法。dialout组在Linux上传统拥有访问串口设备的权限。在行空板终端执行sudo usermod -a -G dialout $USER执行后必须注销当前用户并重新登录或者重启行空板这个组权限变更才会生效。之后你的普通用户账号就有权限访问串口了。修改设备文件权限临时每次插入模块后手动修改一次权限sudo chmod 666 /dev/ttyUSB0这种方法每次插拔后都需要重新执行比较麻烦仅适用于临时测试。实操心得我强烈建议采用第二种方法加入dialout组。这是一次性的配置一劳永逸。配置完成后后续的所有开发、调试都可以在普通用户权限下顺畅进行安全又方便。记得执行命令后一定要重新登录这是很多教程里没强调导致大家以为配置失败的关键点。3. PySerial库详解从安装到核心API解决了硬件和权限我们进入软件层面。pyserial是Python生态中操作串口的事实标准库功能强大且接口清晰。3.1 安装与版本确认行空板默认可能没有安装pyserial。可以通过pip进行安装。打开行空板的终端执行pip install pyserial如果提示权限不足可以尝试pip install --user pyserial。安装完成后可以进入Python交互环境验证import serial print(serial.__version__)确保安装成功。行空板自带的Python环境通常很干净直接安装即可。3.2 核心API与参数解析pyserial的核心是serial.Serial类。初始化一个串口对象时有一系列参数需要配置它们直接决定了通讯能否建立以及通讯的质量。import serial # 创建一个串口对象示例 ser serial.Serial( port/dev/ttyUSB0, # 端口号根据你的实际情况修改 baudrate9600, # 波特率必须与对方设备严格一致 bytesizeserial.EIGHTBITS, # 数据位8位是最常见的 parityserial.PARITY_NONE, # 校验位通常为无校验 stopbitsserial.STOPBITS_ONE, # 停止位通常为1位 timeout1, # 读超时时间秒None为阻塞模式0为非阻塞 write_timeout1, # 写超时时间秒 xonxoffFalse, # 是否启用软件流控一般不启用 rtsctsFalse, # 是否启用硬件RTS/CTS流控一般不启用 dsrdtrFalse # 是否启用硬件DSR/DTR流控一般不启用 )关键参数深度解读port字符串类型。就是我们在/dev目录下找到的设备文件路径。这是最容易出错的地方务必确认路径正确。baudrate波特率表示每秒传输的符号数。必须与通讯的另一方设备如Arduino的串口初始化波特率设置完全一致否则接收到的全是乱码。常用值有9600, 19200, 38400, 57600, 115200等。越高速率对线路质量要求越高。timeout这是一个极其重要的参数。它决定了ser.read()方法的行为。timeoutNone(默认)阻塞模式。执行ser.read(n)会一直等待直到收满n个字节才返回。timeout0非阻塞模式。立即返回当前接收缓冲区中的所有数据可能为空。timeout1等待模式。等待最多1秒在超时前如果收满了n个字节就立即返回如果超时则返回已收到的所有数据可能少于n个字节。在交互式通讯或需要响应控制的场景设置一个合理的超时如1-5秒可以防止程序永远卡住。bytesize, parity, stopbits数据位、校验位、停止位。这三者合称“串口帧格式”。绝大多数现代嵌入式设备Arduino、ESP32、STM32默认串口都使用8N1格式即 8位数据位、无校验、1位停止位。除非你明确知道对方设备使用了其他格式如7E1用于某些老式Modbus设备否则就用8N1。实操心得初始化串口后一个良好的习惯是进行简单的连通性测试。可以尝试ser.is_open检查端口是否成功打开或者尝试写入一个已知的指令如果对方设备支持看是否有返回。另外在程序结束或异常退出时务必调用ser.close()关闭端口。不关闭端口下次再打开可能会失败。一个常见的做法是用try...except...finally语句块确保串口被关闭。4. 完整通讯流程与代码实战让我们通过两个最典型的场景发送指令和接收数据来串联起整个代码流程。4.1 场景一发送指令控制设备假设我们通过行空板控制一个继电器模块继电器模块的协议是发送ASCII字符A打开继电器发送B关闭继电器。import serial import time def control_relay(port, command): 控制继电器开关 :param port: 串口设备路径如 /dev/ttyUSB0 :param command: 命令字符A 或 B try: # 1. 初始化并打开串口 ser serial.Serial( portport, baudrate9600, timeout2 # 设置2秒超时等待响应 ) if ser.is_open: print(f串口 {port} 已成功打开。) # 2. 发送指令 # 需要将字符串编码为字节流 command_byte command.encode(ascii) ser.write(command_byte) print(f已发送指令: {command} - {command_byte}) # 3. 可选等待并读取设备返回的确认信息 # 假设设备会返回 OK\r\n time.sleep(0.1) # 给设备一点反应时间 if ser.in_waiting: # 检查接收缓冲区是否有数据 response ser.read(ser.in_waiting).decode(ascii, errorsignore) print(f收到响应: {response}) else: print(设备未返回响应。) except serial.SerialException as e: print(f串口操作出错: {e}) except Exception as e: print(f发生其他错误: {e}) finally: # 4. 确保关闭串口 if ser in locals() and ser.is_open: ser.close() print(串口已关闭。) # 使用示例 if __name__ __main__: # 请根据实际情况修改端口 my_port /dev/ttyUSB0 control_relay(my_port, A) # 打开继电器 time.sleep(1) # 等待1秒 control_relay(my_port, B) # 关闭继电器代码要点解析ser.write()方法接受的是字节bytes类型的数据所以我们需要用.encode()方法将字符串转换。ser.in_waiting属性返回当前接收缓冲区中等待读取的字节数非常有用。time.sleep(0.1)是一个小技巧。在发送指令后稍作延时给目标设备留出处理指令和准备返回数据的时间再读取缓冲区可以提高读取到完整响应的成功率。异常处理很重要。serial.SerialException是pyserial定义的主要异常用于捕获串口相关的错误如端口不存在、权限不足、配置错误等。4.2 场景二持续读取传感器数据更常见的情况是我们需要持续地从传感器如温湿度传感器、GPS模块读取数据。这些数据通常以一定的间隔主动发送可能包含换行符作为一帧数据的结束。import serial import threading import time class SerialDataReader: def __init__(self, port, baudrate9600): self.port port self.baudrate baudrate self.ser None self.is_running False self.read_thread None def start(self): 打开串口并启动数据读取线程 try: self.ser serial.Serial( portself.port, baudrateself.baudrate, timeout1 # 设置超时让 readline 可以定期返回 ) self.is_running True self.read_thread threading.Thread(targetself._read_loop) self.read_thread.daemon True # 设置为守护线程主程序退出时自动结束 self.read_thread.start() print(f开始在 {self.port} 上监听数据...) except Exception as e: print(f启动串口监听失败: {e}) self.stop() def _read_loop(self): 在后台线程中持续读取数据的循环 while self.is_running and self.ser and self.ser.is_open: try: # 方法1: 按行读取如果数据以换行符结尾如\n或\r\n # line self.ser.readline().decode(utf-8, errorsignore).strip() # if line: # print(f[数据] {line}) # 方法2: 读取固定长度或直到超时 if self.ser.in_waiting: # 一次读取缓冲区所有数据 data self.ser.read(self.ser.in_waiting) decoded_data data.decode(utf-8, errorsignore) # 处理数据例如按特定分隔符拆分 # 假设数据格式为 TEMP:25.6,HUMI:60.5\n print(f[原始数据] {decoded_data}, end) # end 防止自动换行打乱格式 except (serial.SerialException, OSError) as e: print(f读取数据时发生错误: {e}) break except UnicodeDecodeError: # 如果数据不是UTF-8文本可能是二进制数据 print(f[二进制数据] {data.hex()}) # 以16进制显示 time.sleep(0.01) # 短暂休眠避免循环空转消耗CPU def stop(self): 停止读取并关闭串口 self.is_running False if self.read_thread: self.read_thread.join(timeout2) # 等待读取线程结束 if self.ser and self.ser.is_open: self.ser.close() print(串口监听已停止。) def send_command(self, command): 向串口发送命令在主线程中调用 if self.ser and self.ser.is_open: try: self.ser.write(command.encode()) print(f已发送: {command}) except Exception as e: print(f发送命令失败: {e}) # 使用示例 if __name__ __main__: reader SerialDataReader(/dev/ttyUSB0, 115200) # 例如连接一个GPS模块波特率115200 reader.start() try: # 主程序可以在这里做其他事情或者等待 while True: time.sleep(1) # 例如每小时发送一次查询指令 # reader.send_command($PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n) except KeyboardInterrupt: print(\n用户中断程序。) finally: reader.stop()代码要点与高级技巧多线程的必要性串口读取尤其是readline()或等待数据通常是阻塞的。如果放在主线程中会卡住整个程序。使用后台线程专门负责读取主线程可以自由地处理用户界面、逻辑控制或发送指令这是串口编程的常见模式。readline()vsread()readline()非常方便但前提是对方发送的数据以换行符\n或\r\n结尾。它会一直读取直到遇到换行符或超时。对于不按行发送的数据如二进制流或不定长帧readline()可能会一直等待或拆错帧。read(size)或read(ser.in_waiting)更灵活。你可以指定读取的字节数或者一次性读取当前缓冲区的所有内容。然后需要自己解析数据帧例如根据特定的帧头帧尾、固定长度或校验和来分割。解码与错误处理.decode()时使用errorsignore参数可以忽略无法解码的字节防止因少量传输错误导致整个程序崩溃。如果通讯协议是二进制的比如某些自定义的传感器协议就不要解码直接处理bytes对象通过data[0]访问特定字节用struct.unpack()来解析。守护线程设置线程为daemonTrue这样当主程序退出时这个读取线程也会被强制结束避免程序无法正常退出。5. 高级话题与深度排错指南当基础通讯建立后你会遇到更复杂的情况和更棘手的问题。这一章我们深入探讨。5.1 处理二进制数据与结构体解析很多传感器协议是二进制的效率高但可读性差。例如一个温度传感器可能返回4个字节0xAA帧头、0x01传感器ID、两个字节的温度值高位在前、0x55帧尾。import serial import struct def parse_binary_protocol(data_bytes): 解析自定义二进制协议示例 if len(data_bytes) ! 4: return None header, sensor_id, temp_high, temp_low data_bytes if header ! 0xAA: return None # 将两个字节组合成一个16位整数大端序 # temp_raw (temp_high 8) | temp_low # 或者使用struct模块更通用 temp_raw struct.unpack(H, bytes([temp_high, temp_low]))[0] # H 表示大端无符号短整型 temperature temp_raw / 10.0 # 假设实际温度值需要除以10 return sensor_id, temperature ser serial.Serial(/dev/ttyUSB0, 9600, timeout2) # 假设我们知道一帧是4字节或者通过帧头0xAA来寻找起始位置 while True: # 方法寻找帧头然后读取固定长度 byte ser.read(1) if byte b\xaa: # 找到帧头 frame byte ser.read(3) # 再读3个字节 result parse_binary_protocol(frame) if result: print(f传感器ID: {result[0]}, 温度: {result[1]:.1f}°C)struct模块是处理二进制数据的利器表示小端表示大端H是2字节无符号整数I是4字节f是4字节浮点数等。5.2 流控Flow Control到底要不要开在串口设置中我们看到了xonxoff,rtscts,dsrdtr这些流控参数。对于绝大多数与单片机、传感器、Arduino的通讯这些都应该设置为False禁用。流控是为了防止数据丢失而设计的机制。当接收方缓冲区快满时通过发送特定的控制信号软件流控的XOFF字符或硬件流控的RTS/CTS电平变化告诉发送方“暂停发送”。然而99%的简单嵌入式设备根本不实现流控功能。如果你在行空板这边开启了流控而对方设备不理解可能会导致通讯完全中断因为行空板一直在等一个永远不会来的“允许发送”信号。什么情况下需要流控当双方都是具有完整串口驱动能力的设备且数据传输速率极高、数据量巨大时。例如两台计算机通过串口传输大文件。在行空板连接常见电子模块的场景中基本可以忽略。5.3 深度排错当通讯完全失败时如果按照上述步骤仍然无法通讯可以按照以下链路系统性排查硬件层排查确认USB转TTL模块是否被识别执行dmesg | grep tty或lsusb插入模块前后对比看系统内核是否识别到了新USB设备并加载了驱动如ch341,cp210x。确认端口号再次用ls /dev/ttyU*确认设备文件名称特别是当你有多个串口设备时。检查接线TX-RX是否交叉连接GND是否共地这是最常犯的错误。用万用表通断档检查连线。检查电平确认USB转TTL模块输出的是3.3V电平而非5V。检查供电目标设备是否已上电有些设备需要独立供电。软件与权限层排查确认用户组执行groups命令确认当前用户是否在dialout组内。尝试直接读写设备文件高级在终端用echo test /dev/ttyUSB0和cat /dev/ttyUSB0来测试最基本的写入和读取需要先配置好波特率等参数可以用stty命令但较复杂。这可以绕过Python直接测试系统层串口是否通畅。协议与配置层排查波特率一致性这是乱码或无数据的头号元凶。百分百确认行空板代码中的baudrate与对方设备如Arduino代码中的Serial.begin(9600)设置的波特率一字不差。哪怕都是9600一个9600一个19200也无法通讯。帧格式一致性确认数据位、停止位、校验位8N1双方匹配。逻辑电平反相极少数情况下有些古老的设备或电路使用“反相逻辑”即高电平代表0低电平代表1。现代USB转TTL模块都是标准正逻辑。这种情况非常罕见。使用逻辑分析仪或示波器这是终极手段。将探头接到TX或RX线上可以看到实际传输的电平波形。你可以直观地看到是否有数据波形发出波形的波特率是否正确测量一个位的时间波特率1/位时间数据内容是否是你期望发送的需要根据波形手动解码或由分析仪软件解码一个实用的“回环测试”方法如果不确定是发送问题还是接收问题可以做一个简单的硬件回环。将USB转TTL模块的TX引脚和RX引脚用杜邦线短接起来。然后在行空板上运行一个发送并读取自己信息的程序。如果程序能收到自己发送的内容证明从行空板到串口模块的整个发送和接收通路是好的问题很可能出在模块到目标设备的连线或目标设备本身。