在传感器、扫码枪、仪器仪表和控制器项目中,上位机经常需要通过串口与设备通信。
Qt 提供的 QSerialPort 已经封装了串口打开、数据读写和错误通知。实际开发时,可以把这些操作集中到一个通信类中,避免串口逻辑散落在各个按钮事件里。
下面使用异步信号槽完成通信,不在界面线程中阻塞等待数据。
一、扫描电脑上的串口
使用 qmake 的项目,需要添加:
QT += serialport
Qt 6 的 CMake 项目可以添加:
find_package(Qt6 REQUIRED COMPONENTS SerialPort)
target_link_libraries(your_app PRIVATE Qt6::SerialPort)
其中 your_app 替换为实际目标名称。包含 Q_OBJECT 的类需要启用 moc,CMake 项目通常设置:
set(CMAKE_AUTOMOC ON)
扫描串口:
#include <QSerialPortInfo>
#include <QDebug>
void scanSerialPorts()
{
const auto ports = QSerialPortInfo::availablePorts();
for (const auto& port : ports) {
qDebug() << "端口名称:" << port.portName()
<< "设备描述:" << port.description()
<< "设备厂商:" << port.manufacturer();
}
}
如果需要填充下拉框,可以把描述作为显示文本,把端口名称存入用户数据:
for (const auto& port : QSerialPortInfo::availablePorts()) {
ui->portComboBox->addItem(
port.portName() + " " + port.description(),
port.portName());
}
连接时读取 currentData(),不要再从显示文本中截取端口号。
二、封装串口管理类
以下示例使用常见的 115200、8N1、无流控 参数。实际连接时必须以设备文档为准。
#include <QObject>
#include <QSerialPort>
#include <QByteArray>
class SerialDevice : public QObject
{
Q_OBJECT
public:
explicit SerialDevice(QObject* parent = nullptr)
: QObject(parent)
{
connect(&port_, &QSerialPort::readyRead,
this, [this] {
const QByteArray data = port_.readAll();
if (!data.isEmpty())
emit dataReceived(data);
});
connect(&port_, &QSerialPort::errorOccurred,
this, [this](QSerialPort::SerialPortError error) {
if (error == QSerialPort::NoError)
return;
const QString message = port_.errorString();
if (error == QSerialPort::ResourceError)
close();
emit errorOccurred(message);
});
}
bool open(const QString& name, qint32 baudRate = 115200)
{
close();
port_.setPortName(name);
const bool configured =
port_.setBaudRate(baudRate) &&
port_.setDataBits(QSerialPort::Data8) &&
port_.setParity(QSerialPort::NoParity) &&
port_.setStopBits(QSerialPort::OneStop) &&
port_.setFlowControl(QSerialPort::NoFlowControl);
if (!configured)
return false;
return port_.open(QIODevice::ReadWrite);
}
void close()
{
if (port_.isOpen())
port_.close();
}
bool send(const QByteArray& data)
{
if (!port_.isOpen())
return false;
return port_.write(data) == data.size();
}
signals:
void dataReceived(QByteArray data);
void errorOccurred(QString message);
private:
QSerialPort port_{this};
};
这个类只负责串口操作,收到数据后直接发出信号,不在内部判断设备命令。
send() 返回成功,表示数据被接受进入发送流程,不代表设备已经执行了指令。执行结果仍然需要通过响应消息确认。
三、发送和接收二进制数据
在窗口中创建通信对象,并在连接前订阅信号:
auto* device = new SerialDevice(this);
connect(device, &SerialDevice::dataReceived,
this, [](const QByteArray& data) {
qDebug().noquote()
<< "收到:" << data.toHex(' ').toUpper();
// 将 data 交给持续存在的协议解析器。
});
connect(device, &SerialDevice::errorOccurred,
this, [](const QString& message) {
qWarning() << "串口错误:" << message;
});
if (!device->open("COM3")) {
qWarning() << "串口打开失败";
return;
}
const QByteArray command =
QByteArray::fromHex("010300000002C40B");
if (!device->send(command))
qWarning() << "指令未完整写入发送缓冲区";
这里实际发送的是:
01 03 00 00 00 02 C4 0B
不要把 "01 03" 这样的显示文本,直接按字符串发送给使用二进制协议的设备。
另外,QByteArray::fromHex() 会忽略非十六进制字符。程序中写死的合法指令可以直接转换,用户输入则应先进行严格检查,包括字符是否合法、有效字符数量是否为偶数。
四、串口通信中容易忽略的问题
一次接收不等于一条消息。
设备发送:
AA 55 01 00 02 10 20 33
程序可能先收到:
AA 55 01 00
再收到剩余部分。因此,readyRead 中应读取原始字节,然后交给协议解析器持续组包。
不要在接收回调中堆积耗时操作。
大量追加文本、逐条写文件和复杂计算,都可能拖慢事件处理。接收频率高时,可以先缓存,再批量保存或刷新界面。
不要混用异步与阻塞等待。
已经通过 readyRead 接收数据时,通常不需要再在按钮事件里调用 waitForReadyRead()。阻塞等待会影响界面响应,也容易让状态处理变得混乱。
重连时清理旧状态。
设备拔出、重新打开串口后,应清理残留半包和未完成的指令等待。否则旧连接的数据可能与新连接的数据拼接在一起。
串口类负责收发,解析器负责识别完整消息,业务层负责处理设备功能。这个边界确定下来后,增加新指令会容易很多。