嵌入式开发笔记:QSerialPort 完整使用指南------从基础 API 到工程实战
文章目录
- [嵌入式开发笔记:QSerialPort 完整使用指南------从基础 API 到工程实战](#嵌入式开发笔记:QSerialPort 完整使用指南——从基础 API 到工程实战)
-
- [1. 前言:串口通信与 QSerialPort](#1. 前言:串口通信与 QSerialPort)
- [2. 核心类:QSerialPort 与 QSerialPortInfo](#2. 核心类:QSerialPort 与 QSerialPortInfo)
- [3. 环境搭建](#3. 环境搭建)
-
- [3.1 在 .pro 文件中添加模块](#3.1 在 .pro 文件中添加模块)
- [3.2 包含头文件](#3.2 包含头文件)
- [4. 枚举可用串口](#4. 枚举可用串口)
- [5. 串口配置与打开](#5. 串口配置与打开)
-
- [5.1 标准初始化流程](#5.1 标准初始化流程)
- [5.2 串口命名规则](#5.2 串口命名规则)
- [5.3 使用 QSerialPortInfo 设置端口](#5.3 使用 QSerialPortInfo 设置端口)
- [6. 数据发送](#6. 数据发送)
-
- [6.1 基本发送](#6.1 基本发送)
- [6.2 十六进制发送](#6.2 十六进制发送)
- [6.3 异步发送通知](#6.3 异步发送通知)
- [7. 数据接收](#7. 数据接收)
-
- [7.1 异步接收(推荐)](#7.1 异步接收(推荐))
- [7.2 同步接收(阻塞)](#7.2 同步接收(阻塞))
- [7.3 设置读缓冲区大小](#7.3 设置读缓冲区大小)
- [8. 错误处理](#8. 错误处理)
-
- [8.1 连接 errorOccurred 信号(推荐)](#8.1 连接 errorOccurred 信号(推荐))
- [8.2 检查 open() 的返回值](#8.2 检查 open() 的返回值)
- [8.3 常见错误码](#8.3 常见错误码)
- [8.4 端口被占用问题](#8.4 端口被占用问题)
- [9. 高级主题](#9. 高级主题)
-
- [9.1 粘包与拆包问题](#9.1 粘包与拆包问题)
- [9.2 超时与重试机制](#9.2 超时与重试机制)
- [9.3 多线程中使用 QSerialPort](#9.3 多线程中使用 QSerialPort)
- [9.4 平台特定问题](#9.4 平台特定问题)
- [10. 完整示例:简易串口助手](#10. 完整示例:简易串口助手)
- [11. 常见问题与避坑指南](#11. 常见问题与避坑指南)
- [12. 总结](#12. 总结)
1. 前言:串口通信与 QSerialPort
串口通信在嵌入式开发、工业控制和物联网领域有着非常广泛的应用。无论是调试单片机、配置工业设备,还是读取传感器数据,串口都是默认首选。
在 Qt 的世界里,QSerialPort 就是帮我们管理串口通信的得力助手。它最大的魅力在于跨平台------同一套代码,在 Windows、Linux 和 macOS 上都能编译运行。
QSerialPort 继承自 QIODevice ,这意味着你可以像操作文件一样使用 read()、write()、open()、close() 等熟悉的接口。不过,QSerialPort 并非"开箱即用"的傻瓜式类------它把选择权交给了开发者,也把责任一并交还。正确理解它的工作原理和使用方法,是写出稳定可靠串口程序的关键。

2. 核心类:QSerialPort 与 QSerialPortInfo
Qt 串口模块包含两个核心类:
| 类名 | 作用 |
|---|---|
| QSerialPort | 串口通信的主角,负责打开/关闭、参数设置、数据读写 |
| QSerialPortInfo | 串口"情报员",负责枚举系统中所有可用的串口设备及其信息 |
QSerialPortInfo 使用静态函数 availablePorts() 生成可用串口列表,列表中的每个对象代表一个串口,可以查询端口的名称、系统位置、描述和制造商等信息。
⚠️ 重要提醒 :
QSerialPortInfo::availablePorts()返回的只是系统当前"注册"的串口节点快照,它不验证物理存在,也不测试访问权限。一个端口出现在列表里,不代表你一定能打开它。
3. 环境搭建
3.1 在 .pro 文件中添加模块
在项目目录下的 .pro 文件中添加:
qmake
QT += serialport
保存后,Qt Creator 通常会提示重新运行 qmake。
3.2 包含头文件
在需要使用串口的头文件或源文件中包含:
cpp
#include <QSerialPort>
#include <QSerialPortInfo>
4. 枚举可用串口
cpp
#include <QSerialPortInfo>
#include <QDebug>
void listAvailablePorts() {
// 获取所有可用串口列表
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : ports) {
qDebug() << "端口名称:" << info.portName();
qDebug() << "系统位置:" << info.systemLocation();
qDebug() << "描述:" << info.description();
qDebug() << "制造商:" << info.manufacturer();
qDebug() << "VID/PID:" << info.vendorIdentifier()
<< "/" << info.productIdentifier();
}
}
在实际项目中,通常会将这些信息展示在 UI 的 ComboBox 中,供用户选择要连接的设备。
5. 串口配置与打开
5.1 标准初始化流程
正确的初始化顺序至关重要:
cpp
QSerialPort serialPort;
// 步骤1:设置端口名称
serialPort.setPortName("COM3"); // Windows
// serialPort.setPortName("/dev/ttyUSB0"); // Linux
// 步骤2:设置通信参数(必须与对端设备匹配!)
serialPort.setBaudRate(QSerialPort::Baud115200); // 波特率
serialPort.setDataBits(QSerialPort::Data8); // 数据位:8位
serialPort.setParity(QSerialPort::NoParity); // 校验位:无
serialPort.setStopBits(QSerialPort::OneStop); // 停止位:1位
serialPort.setFlowControl(QSerialPort::NoFlowControl); // 流控制:无
// 步骤3:打开串口
if (serialPort.open(QIODevice::ReadWrite)) {
qDebug() << "串口打开成功!";
} else {
qDebug() << "打开失败:" << serialPort.errorString();
}
5.2 串口命名规则
| 操作系统 | 命名格式 | 示例 |
|---|---|---|
| Windows | COM + 数字 | COM1, COM3, COM10 |
| Linux | /dev/tty* | /dev/ttyUSB0, /dev/ttyS0, /dev/ttyAMA0 |
| macOS | /dev/cu.* 或 /dev/tty.* | /dev/cu.usbserial-XXXX |
💡 跨平台建议 :不要硬编码端口名称,应通过
QSerialPortInfo::availablePorts()获取列表后让用户选择。
5.3 使用 QSerialPortInfo 设置端口
你也可以直接将 QSerialPortInfo 对象传递给 QSerialPort:
cpp
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
if (!ports.isEmpty()) {
serialPort.setPort(ports.first());
}
6. 数据发送
QSerialPort 提供了多种写入方式:
6.1 基本发送
cpp
// 发送字节数组
QByteArray data = "Hello, Device!\r\n";
qint64 bytesWritten = serialPort.write(data);
if (bytesWritten == -1) {
qDebug() << "写入失败:" << serialPort.errorString();
} else {
qDebug() << "成功写入" << bytesWritten << "字节";
}
// 等待数据实际发送完成(阻塞)
if (serialPort.waitForBytesWritten(3000)) {
qDebug() << "数据已发送到串口";
} else {
qDebug() << "发送超时";
}
6.2 十六进制发送
cpp
QByteArray hexData = QByteArray::fromHex("A0 01 02 03");
serialPort.write(hexData);
6.3 异步发送通知
write() 是异步的------它立即返回,数据实际发送完成后会发出 bytesWritten() 信号:
cpp
connect(&serialPort, &QSerialPort::bytesWritten,
this, [](qint64 bytes) {
qDebug() << "已发送" << bytes << "字节";
});
7. 数据接收
7.1 异步接收(推荐)
最常用的方式是连接 readyRead() 信号:
cpp
// 连接信号
connect(&serialPort, &QSerialPort::readyRead,
this, &YourClass::handleReadyRead);
// 槽函数
void YourClass::handleReadyRead() {
// 读取所有可用数据
QByteArray data = serialPort.readAll();
// 或按行读取
// QByteArray line = serialPort.readLine();
// 处理数据...
qDebug() << "收到:" << data.toHex(' ');
}
readyRead() 在有新数据到达时触发,但不保证一次读完一帧完整数据。对于粘包/拆包问题,见第 9 节。
7.2 同步接收(阻塞)
在非 GUI 线程中,可以使用同步 API:
cpp
// 等待新数据到达
if (serialPort.waitForReadyRead(3000)) {
QByteArray data = serialPort.readAll();
qDebug() << "收到:" << data;
} else {
qDebug() << "读取超时或错误";
}
⚠️ 警告 :不要在 GUI 线程(主线程)中使用
waitForReadyRead(),它会阻塞界面响应。
7.3 设置读缓冲区大小
cpp
// 限制读缓冲区大小
serialPort.setReadBufferSize(1024);
当缓冲区满时,新数据会被丢弃。默认大小为 0(无限制)。
8. 错误处理
8.1 连接 errorOccurred 信号(推荐)
现代 Qt 编程中,最佳做法是连接 errorOccurred 信号进行实时错误响应:
cpp
connect(&serialPort, &QSerialPort::errorOccurred,
this, &YourClass::handleSerialError);
void YourClass::handleSerialError(QSerialPort::SerialPortError error) {
if (error == QSerialPort::NoError) return;
switch (error) {
case QSerialPort::ResourceError:
qDebug() << "设备被拔出或断开连接"; //
break;
case QSerialPort::PermissionError:
qDebug() << "端口被占用或权限不足"; //
break;
case QSerialPort::OpenError:
qDebug() << "端口名称无效或无法打开"; //
break;
default:
qDebug() << "错误:" << serialPort.errorString(); //
break;
}
// 发生严重错误时,通常需要关闭端口
if (serialPort.isOpen()) {
serialPort.close();
emit portDisconnected();
}
}
8.2 检查 open() 的返回值
cpp
if (!serialPort.open(QIODevice::ReadWrite)) {
QSerialPort::SerialPortError error = serialPort.error();
QString errorString = serialPort.errorString();
qDebug() << "打开失败 - 错误码:" << error << "描述:" << errorString; //
}
8.3 常见错误码
| 错误码 | 含义 | 常见原因 |
|---|---|---|
ResourceError |
资源错误 | 设备被拔出或断开 |
PermissionError |
权限错误 | 端口被占用、权限不足 |
OpenError |
打开错误 | 端口名称无效 |
DeviceNotFoundError |
设备未找到 | 端口不存在 |
8.4 端口被占用问题
串口始终以独占访问 方式打开,其他进程或线程不能同时访问。如果 open() 返回 false,检查是否有其他程序(如串口调试助手)占用了该端口。
9. 高级主题
9.1 粘包与拆包问题
readyRead() 触发的时机不确定,一次触发可能只收到半帧数据,也可能收到多帧数据。常见的解决方案:
方案一:定时器超时法
cpp
// 在 readyRead 中持续追加数据到缓冲区,启动定时器
void YourClass::handleReadyRead() {
m_buffer.append(serialPort.readAll());
m_timer.start(50); // 50ms 无新数据认为一帧结束
}
void YourClass::onTimeout() {
if (!m_buffer.isEmpty()) {
emit frameReady(m_buffer); // 完整一帧
m_buffer.clear();
}
}
方案二:协议解析法
根据协议中的帧头、帧尾或长度字段来切分数据包。
9.2 超时与重试机制
cpp
bool queryWithTimeout(const QByteArray &request, QByteArray &response, int timeoutMs) {
if (!serialPort.isOpen()) return false;
serialPort.clear(); // 清空缓冲区
serialPort.write(request);
if (!serialPort.waitForBytesWritten(timeoutMs)) {
return false;
}
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < timeoutMs) {
if (serialPort.waitForReadyRead(50)) {
response.append(serialPort.readAll());
// 根据协议判断是否接收完整
if (response.contains("\r\n")) {
return true;
}
}
}
return false;
}
9.3 多线程中使用 QSerialPort
在非 GUI 线程中使用 QSerialPort 可以避免阻塞主线程。典型做法:
- 创建一个继承
QObject的工作类,内部持有QSerialPort对象 - 将该工作类
moveToThread()到工作线程 - 通过信号槽进行跨线程通信
cpp
// Worker 类在子线程中运行
class SerialWorker : public QObject {
Q_OBJECT
public:
void doWork() {
// 这里可以使用 waitForReadyRead() 等阻塞 API
// 不会阻塞 GUI 线程
}
};
// 主线程中
QThread *thread = new QThread(this);
SerialWorker *worker = new SerialWorker();
worker->moveToThread(thread);
thread->start();
9.4 平台特定问题
macOS:自 macOS 11 起,系统默认禁用第三方 USB 转串口驱动(如 CH340),需在"系统设置 → 隐私与安全性 → 完全磁盘访问"中授权终端或 App。
Linux :普通用户访问 /dev/ttyUSB0 需要 dialout 组权限:
bash
sudo usermod -a -G dialout $USER
# 需要重新登录生效
10. 完整示例:简易串口助手
cpp
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QSerialPort>
#include <QSerialPortInfo>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void onScanPorts();
void onOpenPort();
void onClosePort();
void onSendData();
void onReadyRead();
void onErrorOccurred(QSerialPort::SerialPortError error);
private:
Ui::MainWindow *ui;
QSerialPort m_serialPort;
};
#endif
cpp
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow) {
ui->setupUi(this);
// 连接信号
connect(&m_serialPort, &QSerialPort::readyRead,
this, &MainWindow::onReadyRead);
connect(&m_serialPort, &QSerialPort::errorOccurred,
this, &MainWindow::onErrorOccurred);
// 扫描端口
onScanPorts();
}
MainWindow::~MainWindow() {
if (m_serialPort.isOpen()) {
m_serialPort.close();
}
delete ui;
}
void MainWindow::onScanPorts() {
ui->comboPort->clear();
QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : ports) {
QString text = info.portName();
if (!info.description().isEmpty()) {
text += " (" + info.description() + ")";
}
ui->comboPort->addItem(text, info.portName());
}
}
void MainWindow::onOpenPort() {
if (m_serialPort.isOpen()) {
m_serialPort.close();
}
QString portName = ui->comboPort->currentData().toString();
m_serialPort.setPortName(portName);
m_serialPort.setBaudRate(QSerialPort::Baud115200);
m_serialPort.setDataBits(QSerialPort::Data8);
m_serialPort.setParity(QSerialPort::NoParity);
m_serialPort.setStopBits(QSerialPort::OneStop);
m_serialPort.setFlowControl(QSerialPort::NoFlowControl);
if (m_serialPort.open(QIODevice::ReadWrite)) {
qDebug() << "打开成功:" << portName;
ui->statusLabel->setText("已连接: " + portName);
} else {
qDebug() << "打开失败:" << m_serialPort.errorString();
ui->statusLabel->setText("打开失败");
}
}
void MainWindow::onClosePort() {
if (m_serialPort.isOpen()) {
m_serialPort.close();
ui->statusLabel->setText("已断开");
}
}
void MainWindow::onSendData() {
if (!m_serialPort.isOpen()) {
qDebug() << "串口未打开";
return;
}
QString text = ui->editSend->toPlainText();
QByteArray data = text.toUtf8();
qint64 written = m_serialPort.write(data);
if (written == -1) {
qDebug() << "发送失败:" << m_serialPort.errorString();
} else {
qDebug() << "发送" << written << "字节";
}
}
void MainWindow::onReadyRead() {
QByteArray data = m_serialPort.readAll();
QString text = QString::fromUtf8(data);
ui->textReceive->append(text);
}
void MainWindow::onErrorOccurred(QSerialPort::SerialPortError error) {
if (error == QSerialPort::NoError) return;
qDebug() << "串口错误:" << error << m_serialPort.errorString();
ui->statusLabel->setText("错误: " + m_serialPort.errorString());
if (m_serialPort.isOpen()) {
m_serialPort.close();
}
}
11. 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
open() 返回 false |
端口不存在、被占用或权限不足 | 检查端口名;关闭占用程序;检查权限 |
| 收不到数据 | 波特率/参数不匹配 | 确保与对端设备参数完全一致 |
| 数据乱码 | 波特率错误或编码问题 | 核对波特率;确认数据编码格式 |
readyRead() 不触发 |
端口未正确打开或信号未连接 | 检查 isOpen();确认 connect 正确 |
| 跨平台编译失败 | 未在 .pro 中添加模块 | 添加 QT += serialport |
| Linux 下无法打开 | 用户不在 dialout 组 | sudo usermod -a -G dialout $USER |
| macOS 下无法识别 | 系统默认禁用第三方驱动 | 在"隐私与安全性"中授权 |
12. 总结
| 知识点 | 核心要点 |
|---|---|
| 环境配置 | .pro 中添加 QT += serialport;包含 <QSerialPort> |
| 端口枚举 | QSerialPortInfo::availablePorts() |
| 参数配置 | setPortName() → setBaudRate() → ... → open() |
| 数据发送 | write() 异步;bytesWritten() 通知完成 |
| 数据接收 | 连接 readyRead(),在槽中 readAll() |
| 错误处理 | 连接 errorOccurred() 信号 |
| 粘包/拆包 | 定时器超时法或协议解析法 |
QSerialPort 是 Qt 提供的强大串口通信工具,其 API 设计清晰、跨平台能力强。掌握它,你就能快速开发出稳定可靠的串口通信应用程序。
📚 参考资料
- Qt 官方文档:QSerialPort / QSerialPortInfo
- Qt 官方示例:Terminal / Blocking Receiver / Command Line Writer Async
- CSDN 社区相关技术博文