QT QSerialPort 串口不稳定,串口是否使用独立线程

Qt 的 QSerialPort 默认 不是独立线程

它默认运行在 创建它的线程 中,也就是你一般在 MainWindow 里 new,它就运行在 UI 主线程里。


那 QSerialPort 会不会卡 UI?

不会卡 UI,因为:

  • 读取数据是异步信号(readyRead)
  • Qt 自己在内部做了事件驱动
  • 接收数据不需要循环、sleep,不需要你手动开线程

正常情况下,串口即使每秒几千条数据,也不会阻塞 UI。


那什么时候需要独立线程?

只有当你自己写了耗时操作(while 死循环、解析巨量数据、频繁阻塞操作)才需要线程。

典型需要线程的情况:

  1. 你自己写一个死循环读串口

    cpp 复制代码
    while (true) {
        port.read(...);
    }

    (这个会卡死 UI)

  2. 协议解析非常复杂、会长时间阻塞

  3. 收/发速率非常大,接收回调里做大量计算

  4. 设备每秒发送几千、几万个字节,需要后台处理


那 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 分发
  • 多协议并行处理

这已经接近专业串口调试助手的功能水平。


相关推荐
IOT-Power2 小时前
QT的解耦方式
qt
定义小花3 小时前
c++ cmake qt
开发语言·c++·qt
cn_newer3 小时前
Qt的applicationName的使用TARGET作为默认值会截断
qt
疋瓞17 小时前
C++_win_QT6学习《3》_结合qt项目开发学习git仓库相关知识
c++·qt·学习
锡兰_CC19 小时前
无缝触达,卓越体验:开启openEuler世界的任意门
服务器·网络·数据库·c++·图像处理·qt·nginx
Source.Liu19 小时前
【LibreCAD】RS_Pen 类详解
qt·cad
油炸自行车21 小时前
【Qt】Qt Creator Debug模式提示“缺少 Windows CDB 调试器配套的扩展组件“”
开发语言·windows·qt
我要升天!21 小时前
QT -- 网络编程
c语言·开发语言·网络·c++·qt
墨月白21 小时前
[QT]QList 相关接口
qt