简介:面向刚接触QT开发的初学者,一套可直接运行的串口通讯入门例程,帮助理解QSerialPort库在串口配置、数据收发与调试中的用法。资源共11个文件,压缩包约5.51MB,主体包含C++源文件(.cpp)、头文件(.h)、工程文件(.pro)和界面文件(.ui),同时附带了串口调试助手与虚拟串口工具的可执行程序,便于在无实体硬件时模拟串口对,直接观察发送和接收到的数据。已有380人学习下载。通过这套例程,读者可以掌握打开串口、设置波特率、数据位、停止位、校验位等参数,以及调用write与readAll完成通讯的写法;界面文件展示了如何利用QT Designer布局调试窗口,结合配套工具进行数据收发验证。完整工程结构清晰、代码量适中,适合小白按步骤对照练习。
1. 工作台上放着一块开发板,串口连接线两边都是USB口,QT程序里写着串口通讯的调用逻辑,可打开串口调试助手一切正常,换到自己的代码就提示“无法打开串口”,或者能打开但什么都收不到。这种状态用一句话概括:串口通讯没有可视化调试面板,出错后你只有黑匣子。问题往往不在硬件,而在你对QSerialPort类的打开时机、参数配置和信号槽的理解。这份QT串口通讯小白入门例程,把从新建工程、枚举端口、配置波特率,到发送接收、断线重连、多线程拆分的完整链路一次给齐。适合刚学QT但被串口折磨过的开发者,也适合从其它语言切过来的经验者快速看边界。
2. 环境准备与串口基础:虚拟串口、端口枚举与QSerialPort类选型
2.1 为什么必须有一对虚拟串口再动手写代码
串口通讯的很多坑与开发语言无关,但QT这门开发语言(实际上是一套C++框架)有其特有的封装方式。在没有真实设备的时候,虚拟串口是排查问题的最佳路径。常用做法是装一对虚拟串口,把COM3和COM4用虚拟线缆连起来。然后QT程序打开COM3,串口调试助手打开COM4,程序发出去的内容助手能看到,助手发的程序也能收到。
这一步能让你把真实硬件问题暂时排除:如果虚拟串口都收发不了,那问题必然在代码或配置里;如果虚拟串口收发正常,再接硬件,就少猜一半“玄学”。虚拟串口原理不复杂,内核层面创建一对通信端口,它们之间建立缓冲区管道。在Windows上安装后通常就多出一组驱动端口,设备管理器能看到。我一般建议先通电确认端口能被系统枚举,再用QT读取。
2.2 从QT5到QT6:串口模块的变化
QT从5开始把串口作为官方模块,即QSerialPort和QSerialPortInfo,引入方式基本一致。QT6延续了这套API,但有几个细节会影响小白:pro文件里必须写QT += serialport,只写include会报找不到类;其次,QT6默认使用C++17,有些旧代码里char*隐式转换到QByteArray的写法会编译不过。建议新项目直接用QT6,这套入门例程大多兼容QT5和QT6,但编译前先检查.pro里是否已经带上了serialport。
另一个变化是错误信号。QT6里error信号已更名为errorOccurred,如果你沿用旧代码里的connect(m_serial, SIGNAL(error(...))),在QT6下会静默失效,且找不到任何编译期提示。更推荐写法是使用新的函数指针语法连接errorOccurred信号,这样即使拼错方法名,编译器也能报错。
2.3 项目文件配置与模块引入
新建一个Qt Widgets Application之后,先把qmake的pro文件改明白。下面是最小的一套配置:
QT += core gui serialport greaterThan(QT_MAJOR_VERSION, 4): QT += widgets CONFIG += c++11 TARGET = serial_demo TEMPLATE = app SOURCES += \ main.cpp \ mainwindow.cpp HEADERS += \ mainwindow.h第三行是关键:QT += core gui serialport。注意serialport必须和core、gui并列,少一个都会在include 时报找不到文件。
在.h文件里这样写:
#include <QMainWindow> #include <QSerialPort> #include <QSerialPortInfo>然后声明成员:
private: QSerialPort *m_serial;这里用指针而不是栈对象,是因为串口对象生命周期要跨越按钮点击、异步信号等多个作用域,指针配合new后在窗口析构时delete最清晰。另一种做法是把它做成MainWindow成员变量,用值类型,但那样构造函数里需要初始化列表,新手容易踩默认构造的坑。指针虽然是裸指针,在小例程里足够直白。
2.4 枚举串口:把设备列表读进下拉框
正确姿势是调用QSerialPortInfo::availablePorts(),它返回一个列表,里面每个QSerialPortInfo对象都带有portName()、description()、manufacturer()等。示例代码:
void MainWindow::refreshPorts() { ui->comboBoxPort->clear(); const auto infos = QSerialPortInfo::availablePorts(); for (const QSerialPortInfo &info : infos) { ui->comboBoxPort->addItem(info.portName()); } }这里注意几点:availablePorts()返回的是系统当前能枚举到的所有串口,包括真实硬件和虚拟串口。要在程序启动后调用一次,并且在用户插拔USB转串口后需要手动点“刷新”按钮,因为串口不是即插即知,QT不会自动通知。
comboBox里只显示端口名,但description往往能帮你区分哪个是USB转串口哪个是蓝牙虚拟端口。如果需要,可以把description放到tooltip:
ui->comboBoxPort->addItem(info.portName() + " " + info.description());但注意,如果description包含中文或特殊字符,某些界面上会显示乱码,这个不算QT问题,是系统驱动描述编码。建议只在调试时加,正式工具还是只显示COM号。
2.5 参数设置的顺序和意义
打开一个串口前,先设置端口名、波特率、数据位、校验位、停止位,最后调用open。顺序其实有讲究:先设置参数再open,否则有些驱动会因为参数为空而拒绝打开。规范的顺序如下:
m_serial->setPortName(ui->comboBoxPort->currentText()); m_serial->setBaudRate(115200); m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); m_serial->setStopBits(QSerialPort::OneStop); m_serial->setFlowControl(QSerialPort::NoFlowControl); if (m_serial->open(QIODevice::ReadWrite)) { statusBar()->showMessage("打开成功"); } else { statusBar()->showMessage("打开失败:" + m_serial->errorString()); }参数表罗列一下常见配置的对应值:
| 参数 | 常用值 | 说明 |
|---|---|---|
| 波特率 | 9600 / 115200 / 460800 | 必须与从机匹配,115200大多数情况够用 |
| 数据位 | Data8 | 8位数据最常用 |
| 校验位 | NoParity | 与设备约定,NoParity配合8N1最常见 |
| 停止位 | OneStop | 默认1位 |
| 流控 | NoFlowControl | 用三线串口时设为No,硬件流控会在RTS/CTS上出问题 |
上面的代码里errorString()会返回最近一次错误,英文提示“No such file or directory”通常意味着端口名不存在,“Permission denied”则是占用或权限不足。这套配置对绝大多数设备都适用,但不排除个别设备要求7位数据或奇校验,需要看设备手册。参数错了不会报错,只会收到乱码或完全不接收,这也是串口让人头疼的原因之一。
2.6 虚拟串口一对联调时的典型配置
虚拟串口创建完成后,在QT程序里打开COM3,在调试助手里打开COM4,两边都用115200 8N1,就能互发。如果调试助手打开COM4时提示“端口已被占用”,检查QT程序是不是把COM3和COM4都打开了。虚拟串口是一对管道,程序只要打开其中一个,驱动就会自动占用另一个,但不是绝对,有些工具创建的是“桥接”模式,允许同时打开两个端口。无论如何,建议在QT程序中只打开一个,另一个留给对端工具。
这个细节可能很多人忽略:QSerialPortInfo::availablePorts枚举到的很多虚拟串口同时存在,但你不能随意选一个就去打开。有的虚拟串口工具创建了两个端口,但只有其中一个处于“被占用”状态,另一个才可以被打开。怎么验证这对端口是否可用呢?最简单的办法是用调试助手分别打开两个端口,看能否互操作;能互操作,再把其中一个关掉,让QT程序去用。
2.7 波特率非标准时的驱动限制
有些设备支持自定义波特率,比如1000000或250000。但是,使用非标准波特率时要小心系统驱动过滤。Windows下大部分USB转串口的驱动支持任意波特率,内核将其转换为小数分频,但有些驱动只支持一个频率集合,设置不为它识别的值会失败。遇到setBaudRate返回值false,不代表是你的程序错了,而是驱动限制。此时先用调试助手试试能否设同样的值,如果调试助手都设不了,就换硬件方案,而不是死磕代码。
3. 打开与配置串口:初始化流程、错误处理与断线检测
3.1 把打开逻辑封装成一个函数
写一个openSerialPort函数,把参数和状态收敛在一起:
bool MainWindow::openSerialPort(const QString &portName, QSerialPort::BaudRate baud, QSerialPort::DataBits dataBits, QSerialPort::Parity parity, QSerialPort::StopBits stopBits) { if (m_serial->isOpen()) m_serial->close(); m_serial->setPortName(portName); m_serial->setBaudRate(baud); m_serial->setDataBits(dataBits); m_serial->setParity(parity); m_serial->setStopBits(stopBits); if (m_serial->open(QIODevice::ReadWrite)) { m_serial->clear(); return true; } return false; }说明:clear()清空缓冲区,确保打开瞬间不会有上次残留字节,否则readyRead立刻触发读到的却是脏数据。这个函数把参数和状态都收敛到一起,界面按钮只需要调用它。
注意setBaudRate等函数返回bool,可以检查返回值来判断参数是否有效,但大部分时候参数值由下拉框限制。波特率有个小坑:setBaudRate(115200)在Windows下没问题,在嵌入式Linux的某些版本下,非标准波特率如250000被内核忽略。建议与设备协商采用标准波特率,250000实际是125千波特,不属于常用档位。
3.2 错误信号与断线检测
串口打开以后不是稳定不变的:USB转串口被拔掉、对端设备掉电、驱动异常,都可能导致数据中断。QT提供了errorOccurred信号,但要注意它只在出错时触发一次,比如设备被拔出,触发error后串口对象会进入不可用状态。此时需要关闭串口并重新枚举。
connect(m_serial, &QSerialPort::errorOccurred, this, [this](QSerialPort::SerialPortError error) { if (error == QSerialPort::ResourceError) { statusBar()->showMessage("串口资源丢失,正在尝试关闭"); m_serial->close(); refreshPorts(); } });ResourceError对应“串口被移除或硬件错误”,收到就不要再做读写操作,先关闭再让用户重新打开。这个错误是异步出现的,如果你在槽函数里还调用m_serial->write,往往会直接崩溃,所以先close。
还要处理另一类错误:打开后立刻发生ReadError,可能是数据堵塞或驱动异常。如果不想让程序挂死,可以在槽里继续读取一次,把缓冲区的数据清掉。这个我们后面避坑章还会提到。
3.3 波特率不一致时的表现和排查
很多人以为波特率只是传输速度,错了。波特率是按位的采样频率,发送方和接收方必须对每个bit的宽度达成一致。如果设置不一致,接收到的数据大概率是乱码,或者根本收不到一个完整帧。在实际项目中,经常会遇到设备默认波特率是19200,而界面下拉框里只有9600和115200的情况。这时你直接选,无法正常通讯。
正确的做法是先查设备手册,然后在下拉框里添加自定义波特率。QSerialPort::setBaudRate接受qint32,你可以直接写数值,不需要依赖枚举项。比如:
m_serial->setBaudRate(19200);只要驱动支持,非标准值也能用。但要注意某些USB转接芯片只识别常用的几个档位,这种情况就只能在设备端改配置了。我们可以提供一个“自定义”输入框,但小白阶段先把标准档位用熟。
3.4 缓冲区的正确理解:bytesAvailable、readAll与DTR信号
换个角度理解缓冲区。QSerialPort内部维护两个状态:发送缓冲区和接收缓冲区。bytesAvailable()返回接收缓冲区中当前可读字节数。注意它不是硬件的FIFO状态,而是应用层已经读取到QT的缓冲。每次readyRead发生时,系统已经把数据从驱动FIFO拷贝到了QSerialPort的接收缓冲。调用readAll就会一次性取出全部。
关于DTR/RTS:有些设备需要DTR引脚处于高电平才发送数据。QT的setDataTerminalReady可以用来控制DTR,setRequestToSend控制RTS。很多工控板默认需要DTR高电平才能接收指令,如果你不控制,就会遇到“发送无回应”。在虚拟串口上DTR没有物理连线,所以验证不到这个需求。接入真实设备时,如果通讯完全没反应,可以尝试设置这两个引脚:
m_serial->setDataTerminalReady(true); m_serial->setRequestToSend(false);但要注意,打开串口时这几行要在open之后调用,有些驱动在open前调用会被忽略。这又是一个容易踩的坑。
3.5 关于write返回值不等于数据长度的排查
write返回的是成功写入内部缓冲区的字节数,不是发送到端口的字节数。如果缓冲区满,返回值会小于传入长度。打开串口后,缓冲区通常有8KB或更大的空间,很少一下子满。但如果连续高频write,缓冲区满了,write会返回可写入的部分,剩下数据直接丢弃。解决方法是监听bytesWritten信号,并在信号里继续补写:
void MainWindow::onBytesWritten(qint64 bytes) { m_sendBuffer.remove(0, bytes); if (!m_sendBuffer.isEmpty()) { m_serial->write(m_sendBuffer); } }m_sendBuffer是需要发送但还没完全写入的缓冲区。这样就能在底层缓冲空闲后自动续传。对普通小数据量场景,这个操作可能多余,但对批量下载固件这种场景非常实用。
3.6 打开失败的错误码与处理策略
QSerialPort::open失败后,errorString()会把底层驱动信息翻译成一句话。以下是我见过的几种,列成排查参考:
| 错误字符串 | 含义 | 标准处理 |
|---|---|---|
| No such file or directory | 端口号不存在 | 刷新枚举,检查虚拟串口是否被删除 |
| Permission denied | 权限不足或端口被占用 | 关闭占用程序,Linux添加用户到dialout组 |
| Invalid argument | 参数非法 | 检查波特率/数据位组合是否被驱动支持 |
| Device not configured | 设备未配置或驱动异常 | 重新插拔USB,或重启程序 |
注意,不同操作系统返回的字符串可能不同,不要完全依赖文本来做分支。正确做法是根据errorEnum()来判断,比如QSerialPort::PermissionError对应权限问题。你的程序至少要给用户显示errorString(),否则“打开失败”四个字没有排查价值。
4. 数据收发与界面联动:从点击到显示的三种典型做法
4.1 发送一条HEX命令:从文本框解析到QByteArray
自制的调试工具通常提供两种发送模式:ASCII文本和HEX。HEX模式是把“01 03 00 00 00 0A C5 CD”这类字符串转成16进制字节数组。QT里常见做法:
QByteArray hexStringToArray(const QString &hex) { QString cleaned = hex.trimmed(); cleaned.remove(QRegularExpression("\\s+")); if (cleaned.length() % 2 != 0) return QByteArray(); QByteArray array; for (int i = 0; i < cleaned.length(); i += 2) { bool ok; uchar byte = cleaned.mid(i, 2).toUInt(&ok, 16); if (!ok) return QByteArray(); array.append(static_cast<char>(byte)); } return array; }逻辑说明:先用trimmed去掉两侧空格,再用remove去掉中间空格。如果剩余长度不是偶数说明输入不完整。每两个hex字符转成一个字节,利用toUInt(base=16)。注意QByteArray::append(char)会截断高位,所以要用uchar强转。
发送时,按钮的点击事件里这样写:
void MainWindow::on_btnSend_clicked() { if (!m_serial->isOpen()) { statusBar()->showMessage("串口未打开"); return; } QByteArray data = hexStringToArray(ui->textEditSend->toPlainText()); if (data.isEmpty()) { statusBar()->showMessage("输入内容不是合法的HEX"); return; } qint64 written = m_serial->write(data); if (written != data.size()) { qDebug() << "写入不完整:" << written << "/" << data.size(); } }qint64 written返回实际写入的字节数。write是异步的,函数返回时数据只进了发送缓冲区,并没有真正送到物理端口。如果返回值不等于你传入的长度,说明缓冲区有限或者设备出错。常见误用是把write当同步发送,紧接着关闭串口,导致数据没发出就断开。这种异步特性在后续线程化时尤其要注意。
4.2 接收的异步处理:readyRead信号为什么不能只读一次
串口数据是“流式”的,没有包的概念。一条AT指令的应答可能在几十毫秒内分两三次到达,也可能一次到达好几帧。QT的readyRead信号只有在数据从内核缓冲区搬到应用层时触发,它会随着数据持续到达而多次触发。
正确的接收逻辑应该结合一个持久缓冲区:
void MainWindow::readyReadSlot() { m_buffer.append(m_serial->readAll()); processBuffer(&m_buffer); }m_buffer是类成员QByteArray,收到多少就累计多少,然后交给解析函数。这样即使一帧数据被分多次送达,也无所谓,因为m_buffer会攒齐。解析函数里用分隔符判断一帧是否完整:
void MainWindow::processBuffer(QByteArray *buffer) { // 假设协议以0x0A结尾 int index; while ((index = buffer->indexOf('\n')) != -1) { QByteArray line = buffer->left(index); buffer->remove(0, index + 1); if (line.endsWith('\r')) line.chop(1); appendLog(QString::fromLatin1(line)); } }这里用indexOf查找换行分隔符,把每一条数据切出来。注意在二进制协议中不能用\n作为边界,而要用固定帧头帧尾或长度字段。这个缓冲累计的方式可以避免readyRead次数和数据帧数不对应的问题,算是串口开发中最值得立即改用的模式。
4.3 把收到的数据按行显示到QTextEdit
界面显示有个玄学点:如果直接在readyRead里调ui->textEdit->append,数据大的时候界面会卡成PPT。QT的信号槽是事件循环驱动的,readyRead信号到来很快,append每行都会触发一次重绘,几百行就卡。常见做法是先把行累积到QStringList,再用QTimer定期冲:
void MainWindow::appendLog(const QString &line) { m_logLines.append(line); m_logTimer.start(50); // ms }定时器50ms一次,把累积的行一次性写入:
void MainWindow::flushLog() { if (!m_logLines.isEmpty()) { ui->textEditLog->append(m_logLines.join("\n")); m_logLines.clear(); } }m_logLines是QStringList成员,appendLog只往列表里塞,flushLog才是真正的界面更新。这样把频繁的界面更新压制到每秒20次,基本不卡。如果你需要显示实时数据波形,也可以用类似思路抛出一个signal给绘图控件,而不要在串口接收槽里直接操作UI。
4.4 自动发送的另类坑:发送间隔别设得太短
自动循环发送文本时,很多人把QTimer的间隔设为1ms,期望高速发送。但真实情况是:QTimer的精度通常在15ms左右,在高负载时会漂移,而且串口发送缓冲区有上限。如果设备端处理不过来回ACK,发送端却在拼命发,很快缓冲区满,write返回写入不完整或持续报错。我一般把自动发送间隔设为10ms以上,且根据设备要求加上应答判断,而不是盲目持续发。这是很多小白把“串口丢数据”归因于硬件实际是软件节奏不对的根源。
4.5 按帧头帧尾截包的通用循环
很多设备的协议并不是按行分隔,而是固定格式,比如AA 55 + 长度 + 数据 + CRC。此时不能用indexOf查找换行,而要在缓冲区里循环找帧头,再通过长度字段判断一帧完整。示意代码如下:
void processBinaryBuffer(QByteArray &buffer) { while (buffer.size() >= 6) { // 帧头2字节 + 长度2字节 + 数据2字节 if (static_cast<uchar>(buffer[0]) == 0xAA && static_cast<uchar>(buffer[1]) == 0x55) { quint16 len = static_cast<uchar>(buffer[2]) << 8 | static_cast<uchar>(buffer[3]); if (buffer.size() >= 4 + len) { QByteArray payload = buffer.mid(4, len); // 校验payload并处理 buffer.remove(0, 4 + len); continue; } } buffer.remove(0, 1); // 没找到帧头或长度不够则跳过1字节 } }如果长度不足,说明还没有攒够一帧,继续等待后续数据。注意这个循环里buffer.remove(0,1)在没找到帧头时逐字节滑动,效率尚可。CRC校验建议放在payload解析后,不一致时丢弃整帧而不是重新滑动,因为CRC错误一般意味着该帧不可信。
5. 避坑:串口开发中常见的五类翻车现场
5.1 现象:串口打开成功但之后什么都收不到
现象:连接设备,调试助手正常,你的QT程序却永远收不到第一帧数据。
原因:多半是串口信号槽没有连接,或者连接时机不对。常见错误是在构造函数里先connect了readyRead,之后才new m_serial,这样connect时m_serial还是空指针,连接失败但编译器不报错。另一种是打开串口后没有把m_serial绑定到on_readyRead槽。
解决:检查connect语句放在new之后,且用对象指针。可以在打开成功后调用connect(m_serial, &QSerialPort::readyRead, this, &MainWindow::readyReadSlot); 把连接和打开放一起,确保串口对象已存在。另外还有一种可能:对端设备没有主动发数据,你的程序一直在等,此时先用串口调试助手发一条命令做应答验证。
5.2 现象:发送数据总是丢头几个字节或出现0x00
现象:发送HEX命令,设备返回“命令不完整”或者逻辑分析仪看到起始字节被吞。
原因:最常见的是在串口刚open之后立刻write。open是异步过程,驱动还没准备好,数据被丢弃。也有可能是USB转串口驱动在启动后需要几毫秒初始化。
解决:在open成功后加一点等待,比如QThread::msleep(50),或者把发送逻辑放到QTimer::singleShot(50, ...)里。另一种原因是用户把QT的write当成同步,write返回后直接关闭程序,数据还留在发送缓冲区里没来得及离开。此时需要等待bytesWritten信号或flush,不过flush对底层驱动不保证生效。
5.3 现象:接收数据显示乱码,特别是中文
现象:设备返回“温度:25.6℃”,界面上显示一堆乱码。
原因:设备通常按ASCII或GBK编码返回,你的QT程序用UTF-8解码。QString::fromUtf8会把GBK字节解释成错误字符,反之亦然。
解决:先确认设备说明书里写的编码。如果是ASCII,直接fromLatin1;如果是中文GBK,用QTextCodec或直接QString::fromLocal8Bit。在Windows系统上fromLocal8Bit默认是GBK,在Linux上默认UTF-8,不要指望一行代码跨平台。推荐做法是把编码做成可选项,让用户选择。代码实例:
QString decodeData(const QByteArray &data, const QString &encoding) { if (encoding == "UTF-8") return QString::fromUtf8(data); if (encoding == "GBK") return QTextCodec::codecForName("GBK")->toUnicode(data); return QString::fromLatin1(data); }5.4 现象:程序关闭后,其他程序打不开同一个串口
现象:关闭QT程序后,再用串口调试助手打开COM3,提示端口被占用,必须插拔USB才能恢复。
原因:程序关闭时析构函数没有调用m_serial->close(),或者close后缓冲区仍有数据。更隐蔽的是如果用了线程,线程没有退出干净,串口对象仍持有句柄。
解决:重写MainWindow::closeEvent,在窗口关闭时先停止收发,再关闭串口:
void MainWindow::closeEvent(QCloseEvent *event) { m_serial->close(); m_serial->deleteLater(); event->accept(); }注意deleteLater不会立即删除,但close已经释放句柄,其他程序可以打开了。如果你的对象是栈成员而不是指针,析构时会自动close,但使用指针时就要显式处理。
5.5 现象:波特率设置正确,但通讯卡顿严重
现象:数据能通,但响应慢,平均几十毫秒甚至几百毫秒。
原因:收发各线程之间事件循环堵塞,或者界面刷新占用了太多时间。比如在readyRead回调里做了重计算、写日志、重绘控件,数据接收就会延迟。
解决:把耗时逻辑放到子线程,UI只负责显示。如果不想引入线程,可以先把接收数据累积到QByteArray,然后通过QTimer以固定周期批量读取并处理,避免每次数据都触发界面操作。另一个类似的情况是:接收卡顿不是因为QT,而是因为设备端发送速率不均匀,发送端一棒子发完,接收端要缓冲再处理。这种卡顿在加大缓冲区后消失。要避免通过软件sleep来“降压”,那会让数据延迟变得不稳定。另外还要检查是否开了软件流控。有些设备固件默认开启XON/XOFF,接收端如果发现缓冲区满会发暂停字符,你的程序没响应,设备就一直在等。在QT里,如果你setFlowControl(QSerialPort::HardwareControl)但线没接RTS/CTS,也可能竞争。建议统一使用NoFlowControl。
6. 进阶:把收发挪进独立线程,并做一个十分钟的自动回环验证
6.1 线程化串口的最小模板
界面线程负责按钮和显示,工作线程持有QSerialPort。别把QSerialPort对象直接创建在run()里也不对,因为你需要在启动线程前配置好参数。常见做法是创建一个QObject子类SerialWorker,moveToThread到QThread中。
class SerialWorker : public QObject { Q_OBJECT public: SerialWorker(const QString &portName) { m_serial = new QSerialPort(this); m_serial->setPortName(portName); } public slots: void start() { m_serial->setBaudRate(115200); if (!m_serial->open(QIODevice::ReadWrite)) { emit errorOccurred(m_serial->errorString()); return; } connect(m_serial, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead); } void send(const QByteArray &data) { if (m_serial->isOpen()) m_serial->write(data); } signals: void dataReceived(const QByteArray &data); void errorOccurred(const QString &msg); private slots: void onReadyRead() { emit dataReceived(m_serial->readAll()); } };把这个worker对象moveToThread,然后通过信号槽跨线程调用它的槽。注意跨线程的信号槽参数要注册元类型,QByteArray可以直接用。线程收尾时要先quit再wait,否则串口对象被销毁的时机不对。
6.2 自动回环验证思路
不管你写了多少功能,建议在接真实设备前先跑一次虚拟回环:程序自己打开一个端口,用虚拟串口对把发送数据从另一个端口收回来,然后定时发送一组带帧序号的数据,接收端校验收到的内容是否完全一致。如果发送1000帧无一错误,再换真实设备。实现时可以启动一个QTimer每100ms发一帧:“HELLO_0001\n”,然后把接收到的行和发送序号比对,不一致就记录错误。这个测试不依赖外部设备,能帮你快速分辨底层驱动、参数配置、协议解析三个层面的问题。
6.3 一个更狠的习惯
我自己的习惯是,但凡写串口程序,先写一个“读取所有端口并打印description到控制台”的命令行版本,确认枚举无误,再套GUI。等GUI完成,再用虚拟串口跑满1000帧回环,任何一帧乱了都停下来查原因。这个过程虽然多花半小时,但省下的排插时间远超于此。每次在这上面偷懒,后面都逃不掉翻车。希望这个习惯对你有帮助。
本文还有配套的精品资源,点击获取