行空板串口通讯全攻略:从PySerial安装到多线程数据采集实战
2026/7/28 6:06:25 网站建设 项目流程

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上,串口叫COM1COM2;在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

有几种解决方法,各有利弊:

  1. 使用sudo运行程序:在终端执行sudo python your_script.py。这是最快捷的测试方式,但不适合作为最终方案,因为让Python脚本以root权限运行存在安全风险,且在某些图形化或远程开发环境中不方便。

  2. 将用户加入dialout:这是推荐的做法dialout组在Linux上传统拥有访问串口设备的权限。在行空板终端执行:

    sudo usermod -a -G dialout $USER

    执行后,必须注销当前用户并重新登录,或者重启行空板,这个组权限变更才会生效。之后,你的普通用户账号就有权限访问串口了。

  3. 修改设备文件权限(临时):每次插入模块后,手动修改一次权限:

    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', # 端口号,根据你的实际情况修改 baudrate=9600, # 波特率,必须与对方设备严格一致 bytesize=serial.EIGHTBITS, # 数据位,8位是最常见的 parity=serial.PARITY_NONE, # 校验位,通常为无校验 stopbits=serial.STOPBITS_ONE, # 停止位,通常为1位 timeout=1, # 读超时时间(秒),None为阻塞模式,0为非阻塞 write_timeout=1, # 写超时时间(秒) xonxoff=False, # 是否启用软件流控,一般不启用 rtscts=False, # 是否启用硬件(RTS/CTS)流控,一般不启用 dsrdtr=False # 是否启用硬件(DSR/DTR)流控,一般不启用 )

关键参数深度解读:

  • port:字符串类型。就是我们在/dev目录下找到的设备文件路径。这是最容易出错的地方,务必确认路径正确。
  • baudrate:波特率,表示每秒传输的符号数。必须与通讯的另一方设备(如Arduino)的串口初始化波特率设置完全一致,否则接收到的全是乱码。常用值有9600, 19200, 38400, 57600, 115200等。越高速率对线路质量要求越高。
  • timeout:这是一个极其重要的参数。它决定了ser.read()方法的行为。
    • timeout=None(默认):阻塞模式。执行ser.read(n)会一直等待,直到收满n个字节才返回。
    • timeout=0:非阻塞模式。立即返回当前接收缓冲区中的所有数据(可能为空)。
    • timeout=1:等待模式。等待最多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( port=port, baudrate=9600, timeout=2 # 设置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', errors='ignore') 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.SerialExceptionpyserial定义的主要异常,用于捕获串口相关的错误(如端口不存在、权限不足、配置错误等)。

4.2 场景二:持续读取传感器数据

更常见的情况是,我们需要持续地从传感器(如温湿度传感器、GPS模块)读取数据。这些数据通常以一定的间隔主动发送,可能包含换行符作为一帧数据的结束。

import serial import threading import time class SerialDataReader: def __init__(self, port, baudrate=9600): 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( port=self.port, baudrate=self.baudrate, timeout=1 # 设置超时,让 readline 可以定期返回 ) self.is_running = True self.read_thread = threading.Thread(target=self._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', errors='ignore').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', errors='ignore') # 处理数据,例如按特定分隔符拆分 # 假设数据格式为 "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(timeout=2) # 等待读取线程结束 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()时使用errors='ignore'参数可以忽略无法解码的字节,防止因少量传输错误导致整个程序崩溃。如果通讯协议是二进制的(比如某些自定义的传感器协议),就不要解码,直接处理bytes对象,通过data[0]访问特定字节,用struct.unpack()来解析。
  • 守护线程:设置线程为daemon=True,这样当主程序退出时,这个读取线程也会被强制结束,避免程序无法正常退出。

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, timeout=2) # 假设我们知道一帧是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 深度排错:当通讯完全失败时

如果按照上述步骤仍然无法通讯,可以按照以下链路系统性排查:

  1. 硬件层排查

    • 确认USB转TTL模块是否被识别:执行dmesg | grep ttylsusb,插入模块前后对比,看系统内核是否识别到了新USB设备并加载了驱动(如ch341,cp210x)。
    • 确认端口号:再次用ls /dev/ttyU*确认设备文件名称,特别是当你有多个串口设备时。
    • 检查接线:TX-RX是否交叉连接?GND是否共地?这是最常犯的错误。用万用表通断档检查连线。
    • 检查电平:确认USB转TTL模块输出的是3.3V电平,而非5V。
    • 检查供电:目标设备是否已上电?有些设备需要独立供电。
  2. 软件与权限层排查

    • 确认用户组:执行groups命令,确认当前用户是否在dialout组内。
    • 尝试直接读写设备文件(高级):在终端用echo "test" > /dev/ttyUSB0cat < /dev/ttyUSB0来测试最基本的写入和读取(需要先配置好波特率等参数,可以用stty命令,但较复杂)。这可以绕过Python,直接测试系统层串口是否通畅。
  3. 协议与配置层排查

    • 波特率一致性:这是乱码或无数据的头号元凶。百分百确认行空板代码中的baudrate与对方设备(如Arduino代码中的Serial.begin(9600))设置的波特率一字不差。哪怕都是9600,一个9600一个19200也无法通讯。
    • 帧格式一致性:确认数据位、停止位、校验位(8N1)双方匹配。
    • 逻辑电平反相:极少数情况下,有些古老的设备或电路使用“反相逻辑”(即高电平代表0,低电平代表1)。现代USB转TTL模块都是标准正逻辑。这种情况非常罕见。
  4. 使用逻辑分析仪或示波器:这是终极手段。将探头接到TX或RX线上,可以看到实际传输的电平波形。你可以直观地看到:

    • 是否有数据波形发出?
    • 波形的波特率是否正确?(测量一个位的时间,波特率=1/位时间)
    • 数据内容是否是你期望发送的?(需要根据波形手动解码,或由分析仪软件解码)

一个实用的“回环测试”方法:如果不确定是发送问题还是接收问题,可以做一个简单的硬件回环。将USB转TTL模块的TX引脚和RX引脚用杜邦线短接起来。然后在行空板上运行一个发送并读取自己信息的程序。如果程序能收到自己发送的内容,证明从行空板到串口模块的整个发送和接收通路是好的,问题很可能出在模块到目标设备的连线或目标设备本身。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询