嵌入式开发笔记:QSerialPort 完整使用指南——从基础 API 到工程实战

嵌入式开发笔记: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 可以避免阻塞主线程。典型做法:

  1. 创建一个继承 QObject 的工作类,内部持有 QSerialPort 对象
  2. 将该工作类 moveToThread() 到工作线程
  3. 通过信号槽进行跨线程通信
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 社区相关技术博文
相关推荐
min(a,b)2 小时前
学习第 6 天:类型注解、装饰器与高级特性
python·学习
minglie13 小时前
线性电枢和分圆多项式
学习
汉堡包0013 小时前
【工具分享】--威胁情报平台大全
学习·安全·web安全·信息安全
西城微科方案开发3 小时前
LCD智能婴儿秤方案
单片机·嵌入式硬件
旺仔学长 哈哈4 小时前
55384 消防安全教育与逃生演练小程序:知识学习、案例浏览和在线演练一站式实现
学习·小程序·消防教育·逃生演练
疯狂打码的少年4 小时前
【面向对象】面向对象概述:从“按步骤做”到“找谁来做”
笔记
今夜有雨.5 小时前
C++JSON 解析器
c++·笔记·后端·学习·json
三佛科技-134163842125 小时前
灭蚊灯MCU方案开发,FT60E112-RB作为灭蚊灯主控芯片优势
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
Mr+范5 小时前
电源诱骗芯片CH224K
单片机·学习