一、简介
QtSerialPort 是 PySide6 用于实现串口异步通信的核心模块,专门用于与串口设备(如单片机、传感器、蓝牙模块、串口打印机等)进行数据交互。
二、QtSerialPort库的核心内容
QtSerialPort 库的核心功能由两个关键类提供,核心操作类QSerialPort 和辅助信息类QSerialPortInfo, 所有串口操作均围绕这两个类展开:
1、核心操作类:QSerialPort
QSerialPort 是实现串口通信的核心类,负责串口的配置、打开 / 关闭、数据读写、错误处理等核心操作,它的核心特性包括:
- 支持串口参数配置(波特率、数据位、校验位、停止位、流控)
- 提供异步读写接口(基于信号与槽,无阻塞)
- 支持串口状态检测和错误反馈
- 支持数据缓冲区操作
QSerialPort 常用属性(串口参数)
串口通信的参数必须与外设保持一致,否则无法正常通信,常用参数如下:
|------|-----------------------------------------------------------|------------------------|
| 参数类型 | 示例 | 说明 |
| 波特率 | QSerialPort.Baud9600,QSerialPort.Baud115200 | 串口通信速率,最常用 9600、115200 |
| 数据位 | QSerialPort.Data8,QSerialPort.Data7 | 每帧数据的位数,最常用 8 位 |
| 校验位 | QSerialPort.NoParity、QSerialPort.EvenParity | 奇偶校验位,最常用无校验(NoParity) |
| 停止位 | QSerialPort.OneStop、QSerialPort.TwoStop | 数据帧结束标志,最常用 1 位 |
| 流控 | QSerialPort.NoFlowControl、QSerialPort.HardwareControl | 流量控制,最常用无流控 |
QSerialPort 常用信号
由于 PySide6 采用信号与槽机制,QSerialPort 的异步操作通过以下核心信号通知上层逻辑:
readyRead():当串口接收缓冲区有新数据到达时触发(读取数据的核心信号)errorOccurred(error):当串口发生错误时触发(如端口不存在、权限不足)bytesWritten(bytes):当数据成功写入串口发送缓冲区时触发
QSerialPort 常用方法
setPortName(portName):设置串口名(如 "COM3"(Windows)、"/dev/ttyUSB0"(Linux))open(openMode):打开串口,打开模式常用QSerialPort.ReadWrite(读写模式)close():关闭串口,释放串口资源write(data):向串口写入数据(需传入字节类型bytes,而非字符串)readAll():读取接收缓冲区中的所有可用数据(返回QByteArray,可转换为 Python 字节 / 字符串)read(maxSize):读取指定长度的字节数据isOpen():判断串口是否已成功打开clear():清空接收 / 发送缓冲区
2. 辅助信息类:QSerialPortInfo
QSerialPortInfo 是辅助类,用于枚举系统中可用的串口设备,获取串口的详细信息,无需打开串口即可使用,核心作用是帮助用户选择有效的串口。
QSerialPortInfo 常用方法
availablePorts():静态方法,返回系统中所有可用串口的QSerialPortInfo列表portName():获取串口名(如 "COM3"、"/dev/ttyUSB0")description():获取串口设备描述(如 "USB-SERIAL CH340")manufacturer():获取串口设备制造商信息isValid():判断当前QSerialPortInfo对应的串口是否有效
三、QtSerialPort 基本使用流程
使用 QtSerialPort 实现串口通信的核心流程分为 5 步,全程基于信号与槽实现异步无阻塞操作:
- 导入核心类
- 枚举可用串口(可选,方便用户选择)
- 配置串口参数并打开串口
- 绑定信号与槽(处理数据接收、错误反馈)
- 发送数据 / 关闭串口
四、完整可运行示例
下面实现一个简单的串口通信工具,支持枚举串口、打开 / 关闭串口、发送数据、接收数据并显示,可直接运行测试(需安装 PySide6 和对应串口驱动)。
python
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QComboBox, QLineEdit, QTextEdit
)
from PySide6.QtCore import Qt
from PySide6.QtSerialPort import QSerialPort, QSerialPortInfo
import sys
class SerialPortTool(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("QtSerialPort 串口通信示例")
self.setFixedSize(600, 400)
# 1. 初始化串口对象(核心)
self.serial_port = QSerialPort()
# 2. 构建界面
self._init_ui()
# 3. 绑定信号与槽
self._bind_signals()
# 4. 初始化时枚举可用串口
self.refresh_serial_ports()
def _init_ui(self):
"""构建界面"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 串口选择区域
port_layout = QHBoxLayout()
self.port_combo = QComboBox()
self.refresh_btn = QPushButton("刷新串口")
self.open_btn = QPushButton("打开串口")
self.close_btn = QPushButton("关闭串口")
self.close_btn.setEnabled(False)
port_layout.addWidget(self.port_combo)
port_layout.addWidget(self.refresh_btn)
port_layout.addWidget(self.open_btn)
port_layout.addWidget(self.close_btn)
# 数据发送区域
send_layout = QHBoxLayout()
self.send_input = QLineEdit()
self.send_input.setPlaceholderText("输入要发送的数据,按回车或点击发送按钮")
self.send_btn = QPushButton("发送数据")
send_layout.addWidget(self.send_input)
send_layout.addWidget(self.send_btn)
# 数据接收区域
self.recv_text = QTextEdit()
self.recv_text.setPlaceholderText("接收的数据将显示在这里...")
self.recv_text.setReadOnly(True)
# 添加到主布局
main_layout.addLayout(port_layout)
main_layout.addLayout(send_layout)
main_layout.addWidget(self.recv_text)
def _bind_signals(self):
"""绑定信号与槽"""
# 界面按钮信号
self.refresh_btn.clicked.connect(self.refresh_serial_ports)
self.open_btn.clicked.connect(self.open_serial_port)
self.close_btn.clicked.connect(self.close_serial_port)
self.send_btn.clicked.connect(self.send_data)
self.send_input.returnPressed.connect(self.send_data)
# 串口核心信号
self.serial_port.readyRead.connect(self.read_serial_data) # 接收数据
self.serial_port.errorOccurred.connect(self.handle_serial_error) # 错误处理
def refresh_serial_ports(self):
"""枚举并刷新可用串口"""
self.port_combo.clear()
# 获取所有可用串口
available_ports = QSerialPortInfo.availablePorts()
for port_info in available_ports:
# 显示格式:串口名 - 设备描述
port_text = f"{port_info.portName()} - {port_info.description()}"
self.port_combo.addItem(port_text, port_info.portName())
def open_serial_port(self):
"""配置并打开串口"""
if self.port_combo.count() == 0:
self._append_recv_text("错误:无可用串口!")
return
# 获取选中的串口名
selected_port = self.port_combo.currentData()
if not selected_port:
self._append_recv_text("错误:无效的串口!")
return
# 1. 配置串口参数
self.serial_port.setPortName(selected_port)
self.serial_port.setBaudRate(QSerialPort.Baud9600) # 波特率 9600
self.serial_port.setDataBits(QSerialPort.Data8) # 数据位 8
self.serial_port.setParity(QSerialPort.NoParity) # 无校验
self.serial_port.setStopBits(QSerialPort.OneStop) # 停止位 1
self.serial_port.setFlowControl(QSerialPort.NoFlowControl) # 无流控
# 2. 打开串口(读写模式)
if self.serial_port.open(QSerialPort.ReadWrite):
self._append_recv_text(f"成功:打开串口 {selected_port}")
self.open_btn.setEnabled(False)
self.close_btn.setEnabled(True)
self.port_combo.setEnabled(False)
self.refresh_btn.setEnabled(False)
else:
self._append_recv_text(f"错误:打开串口 {selected_port} 失败(权限不足/端口被占用)")
def close_serial_port(self):
"""关闭串口"""
if self.serial_port.isOpen():
self.serial_port.close()
self._append_recv_text(f"信息:串口 {self.serial_port.portName()} 已关闭")
self.open_btn.setEnabled(True)
self.close_btn.setEnabled(False)
self.port_combo.setEnabled(True)
self.refresh_btn.setEnabled(True)
def send_data(self):
"""向串口发送数据"""
if not self.serial_manager.serial_port.isOpen():
self._append_recv_text("错误:串口未打开!")
return
# 获取输入框数据并转换为字节类型(必须传入 bytes,字符串需 encode)
input_text = self.send_input.text().strip()
if not input_text:
return
# 判断是否为十六进制格式(去除空格后检查是否全为十六进制字符)
clean_text = input_text.replace(' ', '').replace('\t', '').replace('\n', '')
# 如果是十六进制字符串,则转换为字节数组
if all(c in '0123456789ABCDEFabcdef' for c in clean_text) and len(clean_text) % 2 == 0:
try:
# 将十六进制字符串转换为字节数组
send_bytes = bytes.fromhex(clean_text)
except ValueError:
self._append_recv_text("错误:十六进制数据格式不正确!")
return
else:
# 按普通字符串处理
send_bytes = input_text.encode("utf-8")
# 写入串口(encode 转换为 UTF-8 字节流,外设需对应编码)
written_bytes = self.serial_manager.serial_port.write(send_bytes)
if written_bytes > 0:
# 显示发送的十六进制数据
send_hex = send_bytes.hex().upper()
self._append_recv_text(f"发送:{send_hex}(字节数:{written_bytes})")
# self.send_input.clear()
def read_serial_data(self):
"""读取串口接收的数据(响应 readyRead 信号)"""
if not self.serial_port.isOpen():
return
# 读取所有可用数据(QByteArray -> bytes -> str)
recv_data = self.serial_port.readAll()
# 转换为 Python 字节类型,再解码为字符串(与发送端编码一致)
recv_str = bytes(recv_data).decode("utf-8", errors="ignore")
if recv_str:
self._append_recv_text(f"接收:{recv_str}")
def handle_serial_error(self, error):
"""处理串口错误(响应 errorOccurred 信号)"""
error_msg = ""
# 在 PySide6 中,使用 QSerialPort.SerialPortError 枚举值
if error == QSerialPort.SerialPortError.NoError:
error_msg = "信息:无错误"
elif error == QSerialPort.SerialPortError.DeviceNotFoundError:
error_msg = "错误:串口端口不存在"
elif error == QSerialPort.SerialPortError.PermissionError:
error_msg = "错误:串口权限不足"
elif error == QSerialPort.SerialPortError.OpenError:
error_msg = "错误:无法打开串口(可能已被其他程序占用)"
elif error == QSerialPort.SerialPortError.NotOpenError:
error_msg = "错误:串口未打开"
elif error == QSerialPort.SerialPortError.ResourceError:
error_msg = "错误:串口资源被占用或断开连接"
elif error == QSerialPort.SerialPortError.UnsupportedOperationError:
error_msg = "错误:不支持的操作"
elif error == QSerialPort.SerialPortError.TimeoutError:
error_msg = "错误:操作超时"
elif error == QSerialPort.SerialPortError.UnknownError:
error_msg = "错误:未知串口错误"
else:
error_msg = f"错误:未知串口错误(代码:{error})"
self._append_recv_text(error_msg)
# 错误发生时自动关闭串口
self.close_serial_port()
def _append_recv_text(self, text):
"""向接收区域追加文本并自动滚动"""
self.recv_text.append(text)
# 自动滚动到最下方
scroll_bar = self.recv_text.verticalScrollBar()
scroll_bar.setValue(scroll_bar.maximum())
def closeEvent(self, event):
"""窗口关闭时自动关闭串口"""
if self.serial_port.isOpen():
self.serial_port.close()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SerialPortTool()
window.show()
sys.exit(app.exec())
五、运行与使用说明
- 运行效果:启动程序后,会自动枚举系统中的可用串口,显示在下拉框中。
- 操作步骤 :
- 选择需要连接的串口(如 "COM3 - USB-SERIAL CH340")
- 点击「打开串口」(成功后按钮状态切换)
- 在输入框中输入数据,点击「发送数据」或按回车发送
- 外设返回数据后,会自动显示在下方接收区域
- 结束通信后,点击「关闭串口」释放资源
六、关键注意事项
- 数据格式 :
QSerialPort.write()仅接受字节类型(bytes) ,字符串必须通过encode()转换;读取数据时,readAll()返回QByteArray,需先转换为bytes再decode()为字符串。 - 串口参数一致性:波特率、数据位等参数必须与外设完全一致,否则会出现乱码或无法通信。
- 异步无阻塞 :QtSerialPort 采用异步通信,
readyRead()信号仅在有新数据到达时触发,无需轮询(轮询会导致阻塞,影响界面响应)。 - 资源释放 :串口使用完毕后必须调用
close()关闭,否则会导致端口被占用,下次无法打开。 - 编码一致性:收发数据的编码格式(如 UTF-8、GBK)必须与外设保持一致,否则会出现乱码。
七、总结
- QtSerialPort 库的核心是
QSerialPort(串口操作)和QSerialPortInfo(串口枚举)。 - 串口通信的核心流程是「配置参数→打开串口→绑定信号槽→读写数据→关闭串口」。
- 异步通信依赖
readyRead()信号接收数据,无需轮询,保证界面响应流畅。 - 数据格式转换(字符串 ↔ 字节)和串口参数一致性是串口通信成功的关键。