news 2026/10/25 18:39:55

Qt串口编程实战:QSerialPort与QSerialPortInfo的深度应用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt串口编程实战:QSerialPort与QSerialPortInfo的深度应用指南

1. 从零开始:为什么你需要Qt串口编程?

如果你正在开发一个需要和硬件设备打交道的软件,比如工业控制、数据采集、智能家居中控,或者自己DIY一个机器人、无人机的地面站,那么串口通信几乎是你绕不开的一道坎。在嵌入式开发的世界里,串口就像设备的“嘴巴”和“耳朵”,是调试和通信最基础、最可靠的方式之一。

以前在Qt4时代,搞串口开发还得去网上找第三方库,比如qextserialport,配置起来麻烦不说,跨平台兼容性也是个头疼事。但自从Qt5开始,官方终于把串口模块Qt Serial Port纳入了标准库,这简直是开发者的福音!它提供了两个核心类:QSerialPort和QSerialPortInfo。前者负责实际的读写操作,就像你操作一个文件一样去操作串口;后者则是个“侦察兵”,帮你探查系统里有哪些可用的串口设备,以及它们的详细信息。

我刚开始接触这块的时候,也被一堆波特率、数据位、校验位的概念搞得头晕。但实际用下来发现,Qt把这套封装得非常友好,只要你理解了基本流程,上手其实很快。这篇文章,我就把我这些年用QSerialPort和QSerialPortInfo踩过的坑、总结的经验,用最直白的方式分享给你。咱们不搞复杂的理论堆砌,直接看代码、讲场景,目标是让你看完就能动手写出一个稳定可用的串口通信程序。

2. 侦察兵先行:使用QSerialPortInfo发现你的串口

在动手连接串口之前,我们得先知道电脑上到底插了哪些串口设备。特别是现在很多设备通过USB转串口线连接,在系统里显示的端口号(比如Windows的COM3、COM4,Linux的/dev/ttyUSB0、/dev/ttyS0)可能会变。这时候,QSerialPortInfo就派上大用场了。

2.1 获取所有可用串口列表

QSerialPortInfo的核心功能是通过一个静态函数availablePorts()实现的。这个函数会返回一个QList<QSerialPortInfo>列表,里面的每一个对象都代表一个当前系统可用的串口。

#include <QSerialPortInfo> #include <QDebug> void scanSerialPorts() { QList<QSerialPortInfo> portList = QSerialPortInfo::availablePorts(); if (portList.isEmpty()) { qDebug() << "没有找到可用的串口设备。"; return; } qDebug() << "找到" << portList.count() << "个串口设备:"; for (const QSerialPortInfo &portInfo : portList) { qDebug() << "-----------------------------------"; qDebug() << "端口名称:" << portInfo.portName(); qDebug() << "系统位置:" << portInfo.systemLocation(); qDebug() << "描述信息:" << portInfo.description(); qDebug() << "制造商:" << portInfo.manufacturer(); qDebug() << "序列号:" << portInfo.serialNumber(); // 判断并显示供应商ID和产品ID(如果有的话) if (portInfo.hasVendorIdentifier()) { qDebug() << "供应商ID:" << QString::number(portInfo.vendorIdentifier(), 16); } if (portInfo.hasProductIdentifier()) { qDebug() << "产品ID:" << QString::number(portInfo.productIdentifier(), 16); } } }

运行这段代码,你可能会看到类似这样的输出(以连接了一个USB转串口适配器为例):

找到 1 个串口设备: ----------------------------------- 端口名称:COM3 系统位置:\\.\COM3 描述信息:USB-SERIAL CH340 制造商:wch.cn 序列号: 供应商ID:1a86 产品ID:7523

这些信息有什么用?举个例子,“描述信息”和“供应商/产品ID”就像硬件的“身份证”。如果你的软件需要自动连接某个特定型号的设备(比如特定的传感器模块),你可以通过检查这些信息来精准定位,而不是让用户去猜现在设备是COM几。这在工业自动化场景下非常实用。

2.2 动态监测串口插拔

一个更进阶的需求是:我们希望程序能实时感知串口设备的插入和拔出。QSerialPortInfo本身没有直接提供信号,但我们可以借助Qt的事件循环和定时器,或者结合操作系统的特定API(在Windows上可以注册设备通知)来实现。一个简单实用的方法是启动一个定时器,定期扫描端口列表并与之前的状态对比。

// 在类头文件中 private slots: void checkPortsChanged(); // 在类实现中 QList<QSerialPortInfo> m_lastPortList; // 保存上一次的端口列表 void MyClass::startMonitoring() { // 初始化记录 m_lastPortList = QSerialPortInfo::availablePorts(); // 启动一个定时器,比如每秒检查一次 QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MyClass::checkPortsChanged); timer->start(1000); } void MyClass::checkPortsChanged() { QList<QSerialPortInfo> currentList = QSerialPortInfo::availablePorts(); // 检查是否有新增端口 for (const QSerialPortInfo &info : currentList) { if (!m_lastPortList.contains(info)) { // 注意:需要为QSerialPortInfo实现比较操作符或比较portName qDebug() << "新串口插入:" << info.portName(); emit serialPortAdded(info); // 可以发射一个自定义信号 } } // 检查是否有端口被移除 for (const QSerialPortInfo &info : m_lastPortList) { bool stillExists = false; for (const QSerialPortInfo &currentInfo : currentList) { if (currentInfo.portName() == info.portName()) { stillExists = true; break; } } if (!stillExists) { qDebug() << "串口移除:" << info.portName(); emit serialPortRemoved(info); } } m_lastPortList = currentList; }

这个方案虽然有点“笨”,是轮询的方式,但对于大多数桌面应用来说,每秒一次的检查开销完全可以接受,而且实现简单,跨平台性好。在实际项目中,我常用这种方法来让我的串口调试工具具备“自动发现”设备的能力,用户体验提升很明显。

3. 核心操作手:QSerialPort的配置与打开

找到串口之后,下一步就是配置参数并建立连接了。QSerialPort继承自QIODevice,所以它的很多操作(如read、write)和文件操作很像。但串口通信有一系列特定的参数,必须正确设置才能和设备“对上话”。

3.1 五大关键参数详解

配置串口主要是设置以下五个参数,我把它比喻成两个人用对讲机通话前需要约定的规则:

  1. 波特率 (Baud Rate):这是通信速度,表示每秒传输的符号数。常见的有9600, 19200, 38400, 115200等。必须和设备端严格一致,否则收到的全是乱码。速度越高传输越快,但传输距离和抗干扰能力会下降。对于大多数单片机通信,115200是个比较通用的高速选择。

  2. 数据位 (Data Bits):表示每个字节实际有多少位数据。可选5、6、7、8位。现在几乎99%的设备都使用8位,因为一个字节就是8位。除非你对接一些非常古老的设备(比如某些老式调制解调器),否则无脑选Data8就行。

  3. 校验位 (Parity):用于简单的错误检测。有“无校验”(NoParity)、“奇校验”(OddParity)、“偶校验”(EvenParity)等。如果设备没有特殊说明,通常选“无校验”。奇偶校验只能检测一位错误,在现代通信中作用有限,很多场合都被更高级的协议校验取代了。

  4. 停止位 (Stop Bits):表示一个字节传输结束的标志。可以是1位、1.5位(仅Windows)或2位。绝大多数情况用1位(OneStop)。1.5位很少见,2位有时用于增加可靠性,但会降低有效数据速率。

  5. 流控制 (Flow Control):管理数据传输的节奏,防止接收端缓冲区溢出。分为“无流控”(NoFlowControl)、“硬件流控”(HardwareControl)和“软件流控”(SoftwareControl)。如果设备不要求,就用“无流控”。硬件流控需要RTS/CTS引脚连线,软件流控(XON/XOFF)效率较低。我遇到的需要流控的场景,主要是一些高速调制解调器或老式打印机。

3.2 代码实战:配置并打开串口

假设我们已经从UI下拉框里拿到了用户选择的端口名和参数,下面是如何用代码进行配置:

#include <QSerialPort> QSerialPort *m_serial = new QSerialPort(this); // 建议作为成员变量 bool openSerialPort(const QString &portName, qint32 baudRate) { // 1. 设置端口名 m_serial->setPortName(portName); // 2. 尝试以读写方式打开串口 if (!m_serial->open(QIODevice::ReadWrite)) { qDebug() << "打开串口失败:" << m_serial->errorString(); return false; } // 3. 打开成功后,再设置通信参数 m_serial->setBaudRate(baudRate); // 例如 QSerialPort::Baud115200 m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); m_serial->setStopBits(QSerialPort::OneStop); m_serial->setFlowControl(QSerialPort::NoFlowControl); // 4. 连接关键信号 // 当有数据可读时触发 connect(m_serial, &QSerialPort::readyRead, this, &MyClass::handleReadyRead); // 当发生错误时触发 connect(m_serial, &QSerialPort::errorOccurred, this, &MyClass::handleError); qDebug() << "串口" << portName << "打开成功,参数:" << baudRate << "8N1"; return true; }

这里有个非常重要的细节:setPortName必须在open之前调用,而其他参数设置(setBaudRate等)在open之前或之后都可以。但根据Qt官方文档和我的经验,更推荐在open之后设置。因为有些操作系统(尤其是Windows)在打开端口时可能会应用一些默认设置,打开后再设置能确保参数被正确应用。

另外,open的模式除了ReadWrite,还有ReadOnly和WriteOnly,但串口通信通常需要双向收发,所以ReadWrite是最常用的。记得,串口是独占式访问的,一个串口被你的程序打开后,其他程序(包括其他串口调试助手)就无法再访问它了。

4. 数据收发实战:异步信号与槽模型

串口打开后,最核心的就是收发数据了。Qt推荐使用异步非阻塞的方式,这也是最符合Qt事件驱动哲学的做法。简单说,就是“订阅通知,来了再处理”,而不是傻傻地在那里等。

4.1 接收数据:readyRead信号

这是最常用的数据接收方式。当串口接收缓冲区有新的数据到达时,QSerialPort会发射readyRead()信号。我们只需要将这个信号连接到一个槽函数,在槽函数里读取数据即可。

private slots: void handleReadyRead(); void MyClass::handleReadyRead() { // 1. 一次性读取所有可用数据 QByteArray data = m_serial->readAll(); // 2. 处理数据(例如:显示到UI、解析协议等) // 注意:data可能是不完整的“一帧”数据,需要根据你的协议进行拼接和解析 qDebug() << "收到数据(原始字节):" << data.toHex(' '); // 以16进制打印 QString text = QString::fromLocal8Bit(data); // 尝试转换为本地编码字符串 emit newDataReceived(text); // 发送信号更新UI // 3. 另一种方式:使用read()读取指定字节数 // qint64 bytesAvailable = m_serial->bytesAvailable(); // if (bytesAvailable >= EXPECTED_PACKET_SIZE) { // QByteArray packet = m_serial->read(EXPECTED_PACKET_SIZE); // processPacket(packet); // } }

这里有一个巨大的“坑”需要注意:readyRead信号触发的时机。它并不是每收到一个字节就触发一次。为了提高效率,底层驱动可能会在收到一定数量的数据,或者经过一个很短的时间延迟后才通知上层。这意味着,你的一次readAll()调用,读到的可能是一个完整的数据包,也可能是半个包,或者好几个包粘在一起。

所以,在handleReadyRead里直接readAll()然后当成完整消息处理,在99%的真实项目中都是错误的!正确的做法是:在这个函数里,只负责把数据读出来,追加到你自定义的一个缓冲区(比如QByteArray m_receiveBuffer)里,然后调用一个专门的解析函数,从这个缓冲区里根据你的通信协议(例如,特定的帧头帧尾、长度字段、校验和)来切割出完整的帧。

4.2 发送数据:write方法

发送数据就简单直接多了,调用write方法即可。write是异步的,调用后会立即返回,数据会被放入写入缓冲区,由Qt在后台调度发送。

void sendData(const QByteArray &data) { if (!m_serial || !m_serial->isOpen()) { qDebug() << "串口未打开,无法发送。"; return; } qint64 bytesWritten = m_serial->write(data); if (bytesWritten == -1) { qDebug() << "写入数据失败:" << m_serial->errorString(); } else if (bytesWritten != data.size()) { qDebug() << "数据未完全写入,已写入" << bytesWritten << "字节,总大小" << data.size(); // 这种情况通常发生在缓冲区满的时候,但Qt会帮我们缓冲剩余数据 } else { qDebug() << "数据已成功提交发送,长度:" << bytesWritten; } // 如果需要确保数据立即发出(例如在无事件循环的线程中),可以调用: // m_serial->flush(); // 或者使用 waitForBytesWritten(),但要注意避免阻塞主线程 }

关于flush()和waitForBytesWritten():在普通的GUI程序中,因为有事件循环,数据会自动被发送出去,一般不需要手动调用flush()。但如果你在一个没有运行事件循环的线程里使用QSerialPort,或者需要立即将关键指令发出(比如急停命令),那么调用flush()可以尝试立即将缓冲区的数据写入硬件。waitForBytesWritten()则会阻塞当前线程,直到数据确实被写入串口设备,这在命令行工具或后台服务中可能有用,但绝对不要在主线程(GUI线程)中调用它,否则会导致界面卡死。

4.3 处理编码问题:中文不乱码的秘诀

很多新手都会遇到收发中文乱码的问题。这通常是因为设备端(比如单片机)使用的编码是GBK或GB2312,而Qt内部默认使用UTF-8。解决思路就是在接收和发送时进行编码转换。

// 假设设备端使用GBK编码 QString receivedDataInGBK(const QByteArray &rawData) { QTextCodec *codec = QTextCodec::codecForName("GBK"); if (!codec) { qWarning() << "系统不支持GBK编码!"; return QString::fromLocal8Bit(rawData); // 回退方案 } return codec->toUnicode(rawData); } QByteArray sendDataToGBK(const QString &text) { QTextCodec *codec = QTextCodec::codecForName("GBK"); if (!codec) { qWarning() << "系统不支持GBK编码!"; return text.toLocal8Bit(); } return codec->fromUnicode(text); } // 在接收槽函数中使用 void MyClass::handleReadyRead() { QByteArray raw = m_serial->readAll(); QString text = receivedDataInGBK(raw); // 转换为正确的字符串 // ... 显示或处理 text } // 在发送函数中使用 void MyClass::onSendButtonClicked() { QString input = ui->textEdit->toPlainText(); QByteArray dataToSend = sendDataToGBK(input); m_serial->write(dataToSend); }

记住,编码转换的关键是知道设备端用什么编码。如果不确定,可以先用十六进制模式查看设备发来的数据,然后对照编码表去分析。常见的单片机调试输出如果是中文,很大概率是GBK。

5. 错误处理与资源管理:写出健壮的代码

串口通信处于软件和硬件的交界处,异常情况特别多:线被拔了、设备断电了、参数设错了……一个健壮的程序必须能妥善处理这些错误。

5.1 错误信号与错误码

QSerialPort提供了errorOccurred(QSerialPort::SerialPortError error)信号。任何错误发生时,这个信号都会被发射。

private slots: void handleError(QSerialPort::SerialPortError error); void MyClass::handleError(QSerialPort::SerialPortError error) { QString errorString; switch (error) { case QSerialPort::NoError: return; // 没错误,直接返回 case QSerialPort::DeviceNotFoundError: errorString = "串口设备不存在,可能已被拔出。"; break; case QSerialPort::PermissionError: errorString = "没有权限打开串口,可能已被其他程序占用。"; break; case QSerialPort::OpenError: errorString = "串口打开失败(可能已经打开了?)。"; break; case QSerialPort::NotOpenError: errorString = "尝试操作一个未打开的串口。"; break; case QSerialPort::WriteError: case QSerialPort::ReadError: errorString = "数据读写错误,连接可能已中断。"; break; case QSerialPort::ResourceError: // 这是最常见也最关键的硬件错误,比如USB被拔出 errorString = "资源错误,串口连接突然断开。"; // 通常在这里执行清理工作,比如关闭串口,更新UI状态为“断开” if (m_serial->isOpen()) { m_serial->close(); emit serialPortDisconnected(); // 通知UI更新 } break; case QSerialPort::UnsupportedOperationError: errorString = "不支持的操作(例如在硬件流控下尝试手动设置RTS)。"; break; case QSerialPort::TimeoutError: errorString = "操作超时。"; break; case QSerialPort::UnknownError: default: errorString = "发生未知错误。"; break; } qDebug() << "串口错误:" << errorString << "(" << error << ")"; // 在GUI程序中,可以弹出一个非阻塞的消息框提示用户 QMessageBox::warning(nullptr, "串口通信错误", errorString); }

特别要注意ResourceError,它通常意味着物理连接断开。我强烈建议在这个错误处理中,主动关闭串口对象,并将UI状态重置为“断开连接”,同时清除可能存在的接收缓冲区。这样可以避免程序处于一个不一致的状态。

5.2 超时控制

除了错误信号,超时也是通信中必须考虑的。QSerialPort提供了waitForReadyRead(int msecs)和waitForBytesWritten(int msecs)这两个阻塞函数,它们都带有一个超时参数。虽然我们不推荐在GUI线程中使用它们,但在专用的工作线程中,它们配合超时判断,是实现同步通信或协议超时重试的好工具。

// 在工作线程中发送并等待回应的示例 bool sendAndWaitForResponse(const QByteArray &command, QByteArray &response, int timeoutMs) { if (!m_serial->isOpen()) return false; m_serial->write(command); if (!m_serial->waitForBytesWritten(timeoutMs)) { qDebug() << "发送命令超时。"; return false; } // 清空可能存在的旧数据(根据协议决定是否需要) // m_serial->clear(QSerialPort::Input); // 等待并读取回应 response.clear(); while (m_serial->waitForReadyRead(timeoutMs)) { response.append(m_serial->readAll()); // 这里可以加入判断:如果response已经包含一个完整的协议帧,就提前退出循环 // if (isResponseComplete(response)) break; } if (response.isEmpty()) { qDebug() << "等待回应超时。"; return false; } return true; }

5.3 资源清理:关闭与析构

良好的资源管理习惯很重要。在关闭串口或程序退出时,确保串口被正确关闭。

void MyClass::closeSerialPort() { if (m_serial && m_serial->isOpen()) { // 断开信号连接,避免在关闭过程中触发不必要的槽函数 disconnect(m_serial, nullptr, this, nullptr); // 清空缓冲区 m_serial->clear(QSerialPort::Input | QSerialPort::Output); // 关闭串口 m_serial->close(); qDebug() << "串口已关闭。"; } // 注意:不需要delete m_serial,如果它是父对象的子对象,会随父对象自动销毁。 } // 在类的析构函数中也应调用关闭逻辑 MyClass::~MyClass() { closeSerialPort(); }

把closeSerialPort函数放在析构函数里是一个好习惯,可以防止程序意外退出时资源泄露。另外,在Qt中,如果QSerialPort对象指定了父对象(比如this),通常不需要手动delete,Qt的对象树机制会管理它的生命周期。

6. 高级话题:阻塞操作、线程与性能

虽然信号槽模型是主流,但有些场景下你可能需要考虑其他方式。

6.1 何时使用阻塞模式?

阻塞模式(使用waitForReadyRead等)代码逻辑更线性,更容易理解,特别适合实现严格的请求-响应式协议,比如Modbus RTU。在这种协议下,主站发出一条指令后,必须等待从站的回应才能进行下一步。使用异步信号,你需要维护一个状态机来跟踪当前是等待还是发送,代码会复杂一些。而在一个专用的工作线程中使用阻塞调用,代码会简洁很多。

重要原则:阻塞操作必须放在非GUI线程中!绝对不要在界面线程里调用waitFor...函数,否则用户界面会完全卡住,直到函数返回。

6.2 将QSerialPort移到线程中

Qt中移动对象到线程有一套固定的模式:

// 1. 创建线程和串口对象(注意:串口对象不能有父对象,否则无法移动) QThread *serialThread = new QThread; QSerialPort *serial = new QSerialPort; // 无父对象 // 2. 将串口对象移动到新线程 serial->moveToThread(serialThread); // 3. 连接信号槽(注意:跨线程连接默认是队列连接) // 启动线程后,在线程的上下文中打开串口 connect(serialThread, &QThread::started, [serial, portName]() { // 这个lambda将在serialThread中执行 serial->setPortName(portName); if (serial->open(QIODevice::ReadWrite)) { // ... 配置参数 qDebug() << "串口在线程中打开成功"; } }); // 4. 连接数据接收信号(readyRead是串口对象发出的,接收槽将在串口所在的线程被调用) // 我们需要一个间接方式将数据传递回主线程,通常通过自定义信号 connect(serial, &QSerialPort::readyRead, this, [this, serial]() { QByteArray data = serial->readAll(); emit dataFromThread(data); // 发射信号,此信号连接到主线程的槽 }); // 5. 发送数据到串口线程,需要通过队列方式调用 // 例如,在主线程中调用一个方法,该方法通过元对象系统invoke到串口线程 void MyClass::sendDataInThread(const QByteArray &data) { QMetaObject::invokeMethod(serial, [serial, data]() { serial->write(data); }, Qt::QueuedConnection); } // 6. 启动线程 serialThread->start(); // 7. 退出时,安全关闭 void MyClass::cleanup() { // 请求线程退出 serialThread->quit(); // 等待线程真正结束 if (!serialThread->wait(2000)) { serialThread->terminate(); // 强制终止(不推荐) serialThread->wait(); } delete serial; delete serialThread; }

将QSerialPort放到独立线程是一个相对高级的话题,它解决了界面卡顿问题,但也引入了跨线程通信的复杂性。对于大多数简单的串口调试工具或通信速率不高的应用,在主线程使用异步信号槽模型完全足够。只有当通信非常频繁、数据处理复杂,或者必须使用阻塞协议时,才需要考虑多线程方案。

6.3 性能调优点滴

  • 调整读取缓冲区大小:setReadBufferSize(qint64 size)。默认是0,表示无限缓冲。如果你知道数据包的最大长度,可以设置为一个合理的值(比如最大包长的两倍),防止异常情况下内存被持续消耗。但大多数情况保持默认即可。
  • 及时读取数据:在readyRead槽函数中,尽量快速地将数据从串口缓冲区读走。如果处理数据(如解析、显示)很耗时,应该将读取和处理的步骤分离:在槽函数里只负责readAll()并存入一个队列,然后通过定时器或另一个线程来处理这个队列。
  • 避免频繁的UI更新:如果数据流量很大(比如高速数据采集),不要每收到几个字节就更新一次UI显示。可以累积一定量的数据或固定时间间隔(例如每100毫秒)更新一次UI。

7. 实战案例:构建一个简易串口调试助手

理论说了这么多,最后我们串起来,勾勒一个极简但功能完整的串口调试助手核心逻辑。这个例子会用到我们前面讲的所有知识点。

// 主要头文件 #include <QMainWindow> #include <QSerialPort> #include <QSerialPortInfo> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void onScanPortsClicked(); // 扫描按钮点击 void onConnectClicked(); // 连接/断开按钮点击 void onSendClicked(); // 发送按钮点击 void onSerialReadyRead(); // 串口数据到达 void onSerialErrorOccurred(QSerialPort::SerialPortError error); // 串口错误 private: void setupUI(); // 初始化界面 void updatePortList(); // 刷新串口列表下拉框 bool openPort(const QString &name); // 打开指定串口 void closePort(); // 关闭串口 Ui::MainWindow *ui; // 假设通过Qt Designer设计了界面 QSerialPort *m_serialPort; QByteArray m_receiveBuffer; // 自定义接收缓冲区,用于协议解析(本例简单,未使用) }; // 在构造函数中初始化 MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_serialPort(new QSerialPort(this)) // 指定父对象,自动管理内存 { ui->setupUi(this); setupUI(); // 初始化串口指针,连接错误信号(readyRead信号在打开时连接) connect(m_serialPort, &QSerialPort::errorOccurred, this, &MainWindow::onSerialErrorOccurred); // 初始扫描一次端口 updatePortList(); } // 扫描端口 void MainWindow::onScanPortsClicked() { updatePortList(); } void MainWindow::updatePortList() { ui->comboBoxPort->clear(); foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) { QString displayName = info.portName() + " (" + info.description() + ")"; ui->comboBoxPort->addItem(displayName, info.portName()); // 显示名,实际值 } } // 连接/断开 void MainWindow::onConnectClicked() { if (m_serialPort->isOpen()) { closePort(); ui->pushButtonConnect->setText("连接"); ui->textEditReceive->append(">>> 串口已断开。"); } else { QString portName = ui->comboBoxPort->currentData().toString(); if (openPort(portName)) { ui->pushButtonConnect->setText("断开"); ui->textEditReceive->append(">>> 串口已连接。"); } } } bool MainWindow::openPort(const QString &name) { m_serialPort->setPortName(name); if (!m_serialPort->open(QIODevice::ReadWrite)) { QMessageBox::critical(this, "错误", "打开串口失败:" + m_serialPort->errorString()); return false; } // 配置参数(从UI获取) m_serialPort->setBaudRate(ui->comboBoxBaud->currentText().toInt()); m_serialPort->setDataBits(QSerialPort::Data8); m_serialPort->setParity(QSerialPort::NoParity); m_serialPort->setStopBits(QSerialPort::OneStop); m_serialPort->setFlowControl(QSerialPort::NoFlowControl); // 连接数据接收信号 connect(m_serialPort, &QSerialPort::readyRead, this, &MainWindow::onSerialReadyRead); return true; } void MainWindow::closePort() { if (m_serialPort->isOpen()) { disconnect(m_serialPort, &QSerialPort::readyRead, this, &MainWindow::onSerialReadyRead); m_serialPort->close(); } } // 接收数据 void MainWindow::onSerialReadyRead() { QByteArray data = m_serialPort->readAll(); // 简单处理:直接转换为字符串显示(假设是文本数据) QString text = QString::fromLocal8Bit(data); // 根据实际情况可能需要编码转换 ui->textEditReceive->insertPlainText(text); // 使用insertPlainText避免追加额外的换行 // 自动滚动到底部 QTextCursor cursor = ui->textEditReceive->textCursor(); cursor.movePosition(QTextCursor::End); ui->textEditReceive->setTextCursor(cursor); } // 发送数据 void MainWindow::onSendClicked() { if (!m_serialPort->isOpen()) { QMessageBox::warning(this, "警告", "请先打开串口。"); return; } QString textToSend = ui->lineEditSend->text(); if (textToSend.isEmpty()) return; // 处理换行符:如果勾选了“发送新行”,则自动添加 if (ui->checkBoxNewLine->isChecked()) { textToSend += "\r\n"; // 根据设备要求,可能是"\n"或"\r\n" } QByteArray data = textToSend.toLocal8Bit(); // 编码转换 m_serialPort->write(data); ui->lineEditSend->clear(); } // 错误处理 void MainWindow::onSerialErrorOccurred(QSerialPort::SerialPortError error) { if (error == QSerialPort::ResourceError) { QMessageBox::critical(this, "连接错误", "串口连接已断开。"); closePort(); ui->pushButtonConnect->setText("连接"); ui->textEditReceive->append(">>> [错误] 连接断开。"); } // 可以处理其他错误... }

这个案例麻雀虽小,五脏俱全。它涵盖了设备扫描、参数配置、打开关闭、数据收发和错误处理等基本流程。你可以以此为基础,添加更多功能,比如十六进制显示、发送、定时发送、数据统计、协议解析等。在实际开发中,我建议你直接参考Qt官方提供的“Terminal Example”示例,那个例子结构更清晰,考虑了更多的细节,是学习QSerialPort的最佳范本之一。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 1:39:12

3个维度解析:unrpyc反编译工具的核心价值与实战应用

3个维度解析&#xff1a;unrpyc反编译工具的核心价值与实战应用 【免费下载链接】unrpyc A renpy script decompiler 项目地址: https://gitcode.com/gh_mirrors/un/unrpyc 问题导入&#xff1a;当我们面对.rpyc文件时&#xff0c;究竟在挑战什么&#xff1f; 在RenPy游…

作者头像 李华
网站建设 2026/10/17 12:14:49

Ubuntu系统下Mamba与CUDA环境精准配置指南

1. 为什么你的Mamba环境总是安装失败&#xff1f;版本不匹配是罪魁祸首 很多朋友在Ubuntu上配置Mamba环境时&#xff0c;第一步就卡住了&#xff0c;要么是causal-conv1d编译失败&#xff0c;要么是mamba-ssm导入出错&#xff0c;屏幕上堆满了红色错误信息&#xff0c;让人头大…

作者头像 李华
网站建设 2026/10/17 12:29:36

SMUDebugTool:AMD Ryzen平台硬件调试利器

SMUDebugTool&#xff1a;AMD Ryzen平台硬件调试利器 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https://gitcode.com/gh…

作者头像 李华
网站建设 2026/10/17 11:51:17

5步掌握Git-RSCLIP:遥感图文检索全攻略

5步掌握Git-RSCLIP&#xff1a;遥感图文检索全攻略 1. 什么是Git-RSCLIP&#xff1f; Git-RSCLIP是北京航空航天大学团队基于SigLIP架构专门开发的遥感图像-文本检索模型。这个模型在Git-10M数据集上进行了预训练&#xff0c;这个数据集包含了1000万对遥感图像和文本描述&…

作者头像 李华
网站建设 2026/10/17 18:31:11

SeqGPT-560M效果可视化演示:左侧输入→右侧JSON输出实时对比截图

SeqGPT-560M效果可视化演示&#xff1a;左侧输入→右侧JSON输出实时对比截图 1. 项目简介 SeqGPT-560M是一个专门为企业级信息处理需求打造的高效文本分析系统。与大家熟悉的聊天机器人不同&#xff0c;这个系统专注于一件事&#xff1a;从各种非结构化文本中精准提取关键信息…

作者头像 李华
网站建设 2026/10/17 19:07:22

Qwen2.5-7B-Instruct MATLAB接口开发:科学计算与数据分析

Qwen2.5-7B-Instruct MATLAB接口开发&#xff1a;科学计算与数据分析 1. 为什么MATLAB用户需要大模型能力 在工程和科研领域&#xff0c;MATLAB一直是科学计算、信号处理、控制系统设计和数据分析的首选工具。但传统MATLAB工作流存在几个长期痛点&#xff1a;数据预处理需要反…

作者头像 李华