Qt 串口类QSerialPort实现机制

在 Qt 中,串口通信主要是通过 QSerialPort 类实现的。这个类属于 Qt Serial Port 模块,提供了跨平台对串口(COM 口、串行口、USB 虚拟串口等)的访问。

一、基本类:QSerialPort

作用:

QSerialPort 提供了与系统串口的异步或同步访问,类似于文件的读写操作。

所属模块:

#include <QSerialPort>
#include <QSerialPortInfo>

使用前要在 .pro 文件中添加:

QT += serialport

二、核心机制

1、串口打开、关闭以及参数设置:

QSerialPort serial;
serial.setPortName("COM3");

serial.setBaudRate(QSerialPort::Baud9600);
serial.setDataBits(QSerialPort::Data8);
serial.setParity(QSerialPort::NoParity);
serial.setStopBits(QSerialPort::OneStop);
serial.setFlowControl(QSerialPort::NoFlowControl);


serial.open(QIODevice::ReadWrite);

以上是一个串口配置和打开的示例,在不同平台底层实现略有不同,Qt已经将差异抹平。

QIODevice::ReadWrite:串口是以 QIODevice 派生类存在的,可以读写数据。

调用 open() 后,Qt 内部会使用系统 API 打开串口设备文件:

Windows:CreateFile 打开 COM 口

Linux/macOS:open("/dev/ttyS0") 之类

2、数据读写机制

数据读写支持 异步读写同步阻塞读写 两种方式。

其中异步读写方式使用较多,数据比较稳定,推荐使用这种方式。

(1)异步读写方式

以下是一个使用示例:

connect(&serial, &QSerialPort::readyRead, this, &MyClass::onReadyRead);

void MyClass::onReadyRead() {
    QByteArray data = serial.readAll();
    // 处理数据
}

readyRead()QIODeviceQSerialPort 的父类)中的信号,表示“有新数据可以读取”。

对串口来说,它意味着操作系统串口接收缓冲区中来了新数据,Qt 会通知你——不是你去主动轮询,而是 Qt 来“推送”。

QT底层实现机制如下:

Qt 是基于事件驱动的框架,事件循环(Event Loop) 是它的核心机制。

事件循环的作用是:

  • 不断检测是否有事件(比如鼠标点击、键盘输入、串口数据到来);

  • 有事件时,就触发对应的槽函数;

  • 没有事件时,就空转等待。

可以简单理解为: 

while (appIsRunning) {
    if (有事件) {
        处理事件(比如调用槽函数)
    }
}

qt的异步串口读写就是基于事件循环的。

当串口缓冲区有新数据时,Qt 会通过底层平台 API 监视文件描述符(Linux)或注册 I/O 通知(Windows)。

  • 时间循环 检测到有数据
  • 把它放入事件队列(event queue);
  • 当前线程的事件循环会异步处理这个事件;
  • 最终发出 readyRead() 信号;
  • 如果你 connect() 了这个信号,就会调用你提供的槽函数。

也就是建立信号与槽机制,当底层检测到有数据时,会执行你定义的onReadyRead函数:

connect(&serial, &QSerialPort::readyRead, this, &MyClass::onReadyRead); 

(2)同步读写方式

写入之后用 waitForReadyRead() 和 read() 实现阻塞通信:

serial.write("AT\r\n");
if (serial.waitForReadyRead(1000)) {
    QByteArray data = serial.readAll();
}

等待时间1000ms,然后读数据,这种写法个人不推荐,有可能丢数据或者数据读不全,或者占用时间。

(3)串口枚举:QSerialPortInfo

用于列出可用串口:

foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
    qDebug() << info.portName() << info.description();
}

底层也是封装了操作系统对串口设备的查询:

  • Windows:使用注册表或设备管理器接口。

  • Linux/macOS:扫描 /dev/tty*

 三、串口在线程中的应用

     QSerialPort 不能跨线程直接用,正确方式是把整个 QSerialPort 对象 move 到子线程,然后在子线程中运行事件循环,适用于后台长时间通信的场景。

    一般情况,例如下位机有数据每1s中通过串口发送过来,建议使用将串口放到线程中使用的方式。

示例如下:

void MainWindow::initMCU() {
    // 1. 创建线程对象
    QThread* thread = new QThread(this);

    // 2. 创建 MCU 实例(不要给 parent,因为要放到线程中)
    mMcu = new MCU();

    // 3. 将 MCU 移动到线程中
    mMcu->moveToThread(thread);

    // 5. 启动线程
    thread->start();

    // 6. 在线程启动后调用 MCU 的初始化(openSerial)
    connect(thread, &QThread::started, mMcu, &MCU::init);

    // 7. MainWindow 连接 MCU 的信号槽,例如:处理接收到的数据
    connect(mMcu, &MCU::receivedData, this, &MainWindow::handleMcuData);

    // 8. 安全退出处理(可选)
    connect(this, &MainWindow::destroyed, thread, &QThread::quit);
    connect(thread, &QThread::finished, mMcu, &QObject::deleteLater);
    connect(thread, &QThread::finished, thread, &QObject::deleteLater);
}

将MCU对象创建后放到子线程里,然后整个串口都在MCU里运行:

void MCU::init() {
    mSerial = new QSerialPort(this);
    connect(mSerial, &QSerialPort::readyRead, this, &MCU::handleReadyRead);
    openSerial();
}

void MCU::openSerial() {
    mSerial->setPortName("ttyS1");
    mSerial->setBaudRate(QSerialPort::Baud115200);
    mSerial->setDataBits(QSerialPort::Data8);
    mSerial->setParity(QSerialPort::NoParity);
    mSerial->setStopBits(QSerialPort::OneStop);
    mSerial->setFlowControl(QSerialPort::NoFlowControl);

    if (!mSerial->open(QIODevice::ReadWrite)) {
        qDebug() << "Failed to open port:" << mSerial->errorString();
    } else {
        qDebug() << "Serial port opened: ttyS1";
    }
}

 也有例外的情况,比如NFC刷卡串口,刷卡几乎是一个频率很低的,不定时的操作场景,直接放在主线程里写就好了,没必要浪费资源。

四、串口接收数据容错处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值