在 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()
是 QIODevice
(QSerialPort
的父类)中的信号,表示“有新数据可以读取”。
对串口来说,它意味着操作系统串口接收缓冲区中来了新数据,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刷卡串口,刷卡几乎是一个频率很低的,不定时的操作场景,直接放在主线程里写就好了,没必要浪费资源。