Qt 的 QSerialPort 默认 不是独立线程
它默认运行在 创建它的线程 中,也就是你一般在 MainWindow 里 new,它就运行在 UI 主线程里。
那 QSerialPort 会不会卡 UI?
不会卡 UI,因为:
- 读取数据是异步信号(readyRead)
- Qt 自己在内部做了事件驱动
- 接收数据不需要循环、sleep,不需要你手动开线程
正常情况下,串口即使每秒几千条数据,也不会阻塞 UI。
那什么时候需要独立线程?
只有当你自己写了耗时操作(while 死循环、解析巨量数据、频繁阻塞操作)才需要线程。
典型需要线程的情况:
-
你自己写一个死循环读串口
cppwhile (true) { port.read(...); }(这个会卡死 UI)
-
协议解析非常复杂、会长时间阻塞
-
收/发速率非常大,接收回调里做大量计算
-
设备每秒发送几千、几万个字节,需要后台处理
那 QSerialPort 放到线程里需要什么?
如果你需要线程版本,可以把 SerialService 移到 QThread:
cpp
QThread* thread = new QThread;
SerialService* serial = new SerialService();
serial->moveToThread(thread);
thread->start();
注意:
- QSerialPort 必须在工作线程内创建和使用
- 打开串口、read/write 都要在那个线程里进行
- UI 与串口线程只能通过信号槽通信
推荐原则
| 情况 | 建议 |
|---|---|
| 普通项目,普通收发量(绝大多数应用) | 不要线程,QSerialPort 直接用 |
| 协议解析简单 | 仍然不用线程 |
| 收发量非常大,解析复杂 | 解析放线程,串口仍可主线程 |
| 非常高频收发 | 串口 + 解析 都可以放线程 |
总结一句话
QSerialPort 默认不在独立线程,也不需要线程就能稳定工作。只有你自己写了耗时操作时才需要线程。
综上如果没有使用独立线程,无法稳定工作是因为代码写的烂!!
下面我给你一份 完整的增强版串口工具设计方案,涵盖你提到的全部功能:
✔ 自动重连
✔ 时间戳显示
✔ 自动换行、清空
✔ 统计发送/接收字节数
✔ 发送定时器(循环发送)
✔ 协议命令管理器(按 CMD 分发)
✔ JSON 协议解析
✔ 多协议同时支持
所有功能将给出:
- 设计结构
- 建议写法
- 关键代码示例
你可以按需选用,也可以把它们整合成一个完整的串口调试器项目。
⭐ 完整结构示例(推荐架构)
UI 层(MainWindow)
|
|-- SerialService(串口通信层)
|
|-- ProtocolParser(协议解析层)
| |
| |-- CommandDispatcher(命令分发器)
| |
| |-- JsonProtocol / BinaryProtocol / CustomProtocol
|
|-- Utils(计数器、日志格式化、定时器等)
1)自动重连 AutoReconnect
适用场景:
串口拔掉再插入、设备重启、COM 号变动等。
典型做法:使用 QTimer 周期检测串口状态。
cpp
// 在 SerialService 构造中
reconnectTimer_ = new QTimer(this);
reconnectTimer_->setInterval(2000); // 2 秒
connect(reconnectTimer_, &QTimer::timeout, this, &SerialService::tryReconnect);
tryReconnect() 实现:
cpp
void SerialService::tryReconnect()
{
if (port_.isOpen()) return;
if (port_.open(QIODevice::ReadWrite)) {
emit log("自动重连成功");
emit opened();
}
}
当串口意外断开时触发:
cpp
connect(&port_, &QSerialPort::errorOccurred, this,
[this](QSerialPort::SerialPortError err){
if (err == QSerialPort::ResourceError) {
emit log("串口断开,开始自动重连");
port_.close();
reconnectTimer_->start();
}
});
2)时间戳显示 Timestamp
每条日志前面加:
[2025-01-01 12:00:00.123]
示例函数:
cpp
QString addTimestamp(const QString &msg)
{
return QDateTime::currentDateTime()
.toString("[yyyy-MM-dd HH:mm:ss.zzz] ") + msg;
}
使用:
cpp
emit log(addTimestamp("收到数据:" + data.toHex(' ')));
UI 显示效果更清晰。
3)自动换行 / 清空
自动换行(接收区)
cpp
ui->textEditRecv->append(addTimestamp(data.toHex(' ')));
清空接收区
cpp
ui->textEditRecv->clear();
清空发送区
cpp
ui->lineEditSend->clear();
4)统计发送 / 接收字节数
在 SerialService 中增加:
cpp
quint64 sentBytes_ = 0;
quint64 recvBytes_ = 0;
发送计数:
cpp
port_.write(data);
sentBytes_ += data.size();
emit sendCountUpdated(sentBytes_);
接收计数:
cpp
recvBytes_ += data.size();
emit recvCountUpdated(recvBytes_);
UI 显示:
发送:12345 字节
接收:56789 字节
5)发送定时器(循环发送)
UI 上加一个 checkbox 和 spinbox(间隔 ms)。
核心代码:
cpp
sendTimer_ = new QTimer(this);
connect(ui->chkAutoSend, &QCheckBox::toggled, this, [this](bool on){
if (on) {
sendTimer_->start(ui->spinInterval->value());
} else {
sendTimer_->stop();
}
});
connect(sendTimer_, &QTimer::timeout, this, &MainWindow::onSendClicked);
这样即可周期自动发送。
6)协议命令管理器(按 CMD 分发)
协议常见格式:
55 AA LEN CMD PAYLOAD CHECKSUM
解析器解析出 payload 后:
payload[0] = CMD 字节。
创建分发器:
cpp
class CommandDispatcher : public QObject
{
Q_OBJECT
public:
void dispatch(const QByteArray &payload)
{
quint8 cmd = (quint8)payload[0];
if (handlers_.contains(cmd))
handlers_[cmd](payload);
else
emit log(QString("未识别 CMD:%1").arg(cmd));
}
void registerHandler(quint8 cmd, std::function<void(const QByteArray&)> func)
{
handlers_[cmd] = func;
}
signals:
void log(const QString &msg);
private:
QMap<quint8, std::function<void(const QByteArray&)>> handlers_;
};
注册处理函数:
cpp
dispatcher_->registerHandler(0x01, [this](const QByteArray &p){
ui->textEditRecv->append("收到心跳包");
});
dispatcher_->registerHandler(0x02, [this](const QByteArray &p){
ui->textEditRecv->append("收到设备信息:" + p.toHex(' '));
});
解析器连接:
cpp
connect(parser_, &ProtocolParser::frameParsed,
dispatcher_, &CommandDispatcher::dispatch);
7)JSON 协议解析
如果设备发 JSON:
{"cmd":1, "temp":25.5}
解析方法:
cpp
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(payload, &err);
if (err.error != QJsonParseError::NoError) {
emit log("JSON 解析失败:" + err.errorString());
return;
}
QJsonObject obj = doc.object();
int cmd = obj["cmd"].toInt();
double temp = obj["temp"].toDouble();
可与 CommandDispatcher 一起使用:
cpp
dispatcher_->registerHandler(1, [this](const QByteArray &p){
QJsonDocument doc = QJsonDocument::fromJson(p);
auto obj = doc.object();
double temp = obj["temp"].toDouble();
ui->textEditRecv->append(QString("当前温度:%1").arg(temp));
});
8)多协议同时支持(JSON + 二进制 + 自定义)
可以为每种协议建独立解析器:
JsonProtocolParser
BinaryProtocolParser
CustomProtocolParser
或者统一管理:
cpp
class MultiProtocolProcessor : public QObject
{
Q_OBJECT
public:
enum ProtocolType { Json, Binary, Custom };
void process(const QByteArray &bytes)
{
if (isJson(bytes)) {
emit jsonParsed(bytes);
} else if (isBinary(bytes)) {
emit binaryParsed(bytes);
} else {
emit customParsed(bytes);
}
}
signals:
void jsonParsed(const QByteArray&);
void binaryParsed(const QByteArray&);
void customParsed(const QByteArray&);
};
判断 JSON 很简单:
cpp
bool isJson(const QByteArray &bytes)
{
return bytes.startsWith('{') && bytes.endsWith('}');
}
📌 最终效果(高级串口调试工具)
你现在可以轻松做出一个功能齐全的串口工具:
- 自动重连
- 时间戳显示
- 自动换行
- 批量清空
- 发送/接收字节统计
- 自动循环发送
- 完整的协议解析
- 支持 JSON、二进制
- 支持 CMD 分发
- 多协议并行处理
这已经接近专业串口调试助手的功能水平。