QT 串口 源码结构框架

下面假设用 Qt Widgets,工程名叫 SerialTool

(用 qmake 或 CMake 都行,这里用 qmake 举例。)


1. 工程目录结构

text 复制代码
SerialTool/
  SerialTool.pro
  main.cpp

  MainWindow.h
  MainWindow.cpp
  MainWindow.ui

  SerialService.h      // 串口收发层
  SerialService.cpp

  ProtocolParser.h     // 协议组帧 + 校验层
  ProtocolParser.cpp

  CommandDispatcher.h  // 按 CMD 分发
  CommandDispatcher.cpp

  Utils.h              // 通用工具函数(时间戳等)
  Utils.cpp

SerialTool.pro 示例

pro 复制代码
QT       += core gui serialport

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = SerialTool
TEMPLATE = app

SOURCES += \
    main.cpp \
    MainWindow.cpp \
    SerialService.cpp \
    ProtocolParser.cpp \
    CommandDispatcher.cpp \
    Utils.cpp

HEADERS += \
    MainWindow.h \
    SerialService.h \
    ProtocolParser.h \
    CommandDispatcher.h \
    Utils.h

FORMS += \
    MainWindow.ui

2. main.cpp

cpp 复制代码
#include <QApplication>
#include "MainWindow.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

没啥好说的,标准 Qt 入口。


3. MainWindow.ui 控件约定

在 Qt Designer 里放这些控件,并设置 objectName:

  • 串口区域

    • QComboBox 名为 comboPort (串口号)
    • QComboBox 名为 comboBaud (波特率)
    • QPushButton 名为 btnOpen
    • QPushButton 名为 btnClose
  • 发送区域

    • QLineEdit 名为 lineEditSend
    • QCheckBox 名为 chkHexSend (选中时按 hex 解析)
    • QCheckBox 名为 chkAutoSend (自动发送)
    • QSpinBox 名为 spinInterval (自动发送间隔 ms)
    • QPushButton 名为 btnSend
  • 接收区域

    • QTextEdit 名为 textEditRecv
    • QCheckBox 名为 chkHexRecv (以 hex 显示)
    • QPushButton 名为 btnClearRecv
  • 日志区域

    • QTextEdit 名为 textEditLog
    • QPushButton 名为 btnClearLog
  • 统计区域

    • QLabel 名为 labelTxCount
    • QLabel 名为 labelRxCount

4. Utils.h / Utils.cpp(时间戳等)

cpp 复制代码
// Utils.h
#pragma once
#include <QString>

QString withTimestamp(const QString &msg);
cpp 复制代码
// Utils.cpp
#include "Utils.h"
#include <QDateTime>

QString withTimestamp(const QString &msg)
{
    return QDateTime::currentDateTime()
            .toString("[yyyy-MM-dd HH:mm:ss.zzz] ") + msg;
}

评价:

公共小工具集中放在一个文件里,避免到处复制时间戳代码。独立出来之后也方便以后加别的工具函数。


5. 串口层 SerialService

cpp 复制代码
// SerialService.h
#pragma once
#include <QObject>
#include <QSerialPort>
#include <QTimer>

class SerialService : public QObject
{
    Q_OBJECT
public:
    explicit SerialService(QObject *parent = nullptr);

    bool open(const QString &portName, int baudRate);
    void close();
    bool isOpen() const { return port_.isOpen(); }

public slots:
    void send(const QByteArray &data);

signals:
    void received(const QByteArray &data);
    void log(const QString &msg);

    void opened();
    void closed();

    void sendCountUpdated(quint64 bytes);
    void recvCountUpdated(quint64 bytes);

private slots:
    void onReadyRead();
    void onPortError(QSerialPort::SerialPortError error);
    void tryReconnect();

private:
    QSerialPort port_;
    QTimer reconnectTimer_;

    QString lastPortName_;
    int lastBaudRate_ = 0;

    quint64 sentBytes_ = 0;
    quint64 recvBytes_ = 0;
};
cpp 复制代码
// SerialService.cpp
#include "SerialService.h"
#include "Utils.h"

SerialService::SerialService(QObject *parent)
    : QObject(parent)
{
    connect(&port_, &QSerialPort::readyRead,
            this, &SerialService::onReadyRead);

    connect(&port_, &QSerialPort::errorOccurred,
            this, &SerialService::onPortError);

    reconnectTimer_.setInterval(2000); // 2 秒尝试一次
    reconnectTimer_.setSingleShot(false);
    connect(&reconnectTimer_, &QTimer::timeout,
            this, &SerialService::tryReconnect);
}

bool SerialService::open(const QString &portName, int baudRate)
{
    lastPortName_ = portName;
    lastBaudRate_ = baudRate;

    if (port_.isOpen())
        port_.close();

    port_.setPortName(portName);
    port_.setBaudRate(baudRate);

    if (!port_.open(QIODevice::ReadWrite)) {
        emit log(withTimestamp("串口打开失败: " + port_.errorString()));
        // 可以启动自动重连
        reconnectTimer_.start();
        return false;
    }

    reconnectTimer_.stop();
    emit log(withTimestamp(QString("串口已打开: %1 @ %2")
                           .arg(portName).arg(baudRate)));
    emit opened();
    return true;
}

void SerialService::close()
{
    reconnectTimer_.stop();
    if (port_.isOpen()) {
        port_.close();
        emit log(withTimestamp("串口已关闭"));
        emit closed();
    }
}

void SerialService::send(const QByteArray &data)
{
    if (!port_.isOpen()) {
        emit log(withTimestamp("串口未打开, 发送失败"));
        return;
    }
    qint64 n = port_.write(data);
    if (n > 0) {
        sentBytes_ += static_cast<quint64>(n);
        emit sendCountUpdated(sentBytes_);
        emit log(withTimestamp("发送: " + data.toHex(' ')));
    }
}

void SerialService::onReadyRead()
{
    QByteArray data = port_.readAll();
    if (!data.isEmpty()) {
        recvBytes_ += static_cast<quint64>(data.size());
        emit recvCountUpdated(recvBytes_);
        emit log(withTimestamp("接收: " + data.toHex(' ')));
        emit received(data);
    }
}

void SerialService::onPortError(QSerialPort::SerialPortError error)
{
    if (error == QSerialPort::ResourceError) {
        emit log(withTimestamp("串口错误: " + port_.errorString()));
        port_.close();
        emit closed();
        reconnectTimer_.start();
    }
}

void SerialService::tryReconnect()
{
    if (port_.isOpen()) {
        reconnectTimer_.stop();
        return;
    }
    if (lastPortName_.isEmpty() || lastBaudRate_ == 0)
        return;

    emit log(withTimestamp("尝试自动重连..."));
    open(lastPortName_, lastBaudRate_);
}

分析评价:

  • 串口读写全部在同一个线程(默认就是 UI 线程),用的是 readyRead 事件回调,不需要自己开线程。
  • 自动重连用 QTimer,逻辑简单清晰。错误类型选择 ResourceError,通常是设备断开。
  • 使用 sentBytes_ / recvBytes_ 计数,单独用信号发给 UI。这样 UI 层不关心细节,只负责展示。
  • SerialService 不依赖任何 UI 控件,只通过信号沟通,这点很重要,后面可以在别的工程复用。

6. 协议解析层 ProtocolParser

假设协议:55 AA LEN CMD PAYLOAD... CHECKSUM

LEN 不包含头和校验,CHECKSUM 是 CMD+PAYLOAD 的异或。

cpp 复制代码
// ProtocolParser.h
#pragma once
#include <QObject>

class ProtocolParser : public QObject
{
    Q_OBJECT
public:
    explicit ProtocolParser(QObject *parent = nullptr);

public slots:
    void feed(const QByteArray &data);

signals:
    void frameParsed(quint8 cmd, const QByteArray &payload);
    void log(const QString &msg);

private:
    QByteArray buffer_;
    void process();
};
cpp 复制代码
// ProtocolParser.cpp
#include "ProtocolParser.h"
#include "Utils.h"

ProtocolParser::ProtocolParser(QObject *parent)
    : QObject(parent)
{
}

void ProtocolParser::feed(const QByteArray &data)
{
    buffer_.append(data);
    process();
}

void ProtocolParser::process()
{
    while (true) {
        if (buffer_.size() < 4)  // 至少 55 AA LEN CMD
            return;

        // 找头
        int headerIndex = buffer_.indexOf("\x55\xAA", 0);
        if (headerIndex < 0) {
            emit log(withTimestamp("未找到帧头, 清空缓存"));
            buffer_.clear();
            return;
        }
        if (headerIndex > 0) {
            buffer_.remove(0, headerIndex);
            if (buffer_.size() < 4)
                return;
        }

        quint8 len = static_cast<quint8>(buffer_[2]);
        int frameSize = 2 /*55AA*/ + 1 /*LEN*/ + 1 /*CMD*/ + len /*payload*/ + 1 /*CHK*/;

        if (buffer_.size() < frameSize)
            return; // 数据不完整,等下次

        QByteArray frame = buffer_.mid(0, frameSize);
        buffer_.remove(0, frameSize);

        quint8 cmd = static_cast<quint8>(frame[3]);
        QByteArray payload = frame.mid(4, len);
        quint8 recvChk = static_cast<quint8>(frame.last());

        quint8 calc = cmd;
        for (char c : payload)
            calc ^= static_cast<quint8>(c);

        if (calc != recvChk) {
            emit log(withTimestamp("校验失败, 丢弃一帧"));
            continue;
        }

        emit log(withTimestamp(QString("解析成功一帧, CMD=0x%1")
                               .arg(cmd, 2, 16, QChar('0'))));
        emit frameParsed(cmd, payload);
    }
}

分析评价:

  • 使用内部 buffer 解决粘包、半包问题,这是协议解析必须要有的。
  • 每次 feed 只做追加 + 循环解析,解析出一帧就 emit,一帧不够就等下一次 feed。
  • 解析逻辑不依赖串口,只处理字节,适合以后换成 TCP、UDP 复用。
  • CMD 和 payload 拆出来,方便后面 CommandDispatcher 分发。

7. 命令分发 CommandDispatcher

按 CMD 字节调用不同处理函数。

cpp 复制代码
// CommandDispatcher.h
#pragma once
#include <QObject>
#include <QMap>
#include <functional>

class CommandDispatcher : public QObject
{
    Q_OBJECT
public:
    explicit CommandDispatcher(QObject *parent = nullptr);

    void registerHandler(quint8 cmd, std::function<void(const QByteArray&)> handler);

public slots:
    void dispatch(quint8 cmd, const QByteArray &payload);

signals:
    void log(const QString &msg);

private:
    QMap<quint8, std::function<void(const QByteArray&)>> handlers_;
};
cpp 复制代码
// CommandDispatcher.cpp
#include "CommandDispatcher.h"
#include "Utils.h"

CommandDispatcher::CommandDispatcher(QObject *parent)
    : QObject(parent)
{
}

void CommandDispatcher::registerHandler(quint8 cmd, std::function<void(const QByteArray&)> handler)
{
    handlers_[cmd] = handler;
}

void CommandDispatcher::dispatch(quint8 cmd, const QByteArray &payload)
{
    if (!handlers_.contains(cmd)) {
        emit log(withTimestamp(QString("未注册 CMD: 0x%1")
                               .arg(cmd, 2, 16, QChar('0'))));
        return;
    }
    handlers_[cmd](payload);
}

分析评价:

  • 这是一个非常简单的"命令路由器"。解析层只管发 cmd+payload,具体每个 cmd 做什么由上层业务决定。
  • 使用 std::function,可以在 MainWindow 里用 lambda 直接写业务逻辑,不用再新建很多子类。
  • Dispatcher 不依赖 UI,不依赖串口,只依赖 cmd+payload,通用性很高。

8. MainWindow.h / MainWindow.cpp

cpp 复制代码
// MainWindow.h
#pragma once
#include <QMainWindow>
#include <QTimer>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class SerialService;
class ProtocolParser;
class CommandDispatcher;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;

    SerialService *serial_;
    ProtocolParser *parser_;
    CommandDispatcher *dispatcher_;
    QTimer autoSendTimer_;

    void initUi();
    void initSignals();
    void initDispatcher();

private slots:
    void onOpenClicked();
    void onCloseClicked();
    void onSendClicked();
    void onClearRecv();
    void onClearLog();
};
cpp 复制代码
// MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"

#include "SerialService.h"
#include "ProtocolParser.h"
#include "CommandDispatcher.h"
#include "Utils.h"

#include <QSerialPortInfo>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    serial_ = new SerialService(this);
    parser_ = new ProtocolParser(this);
    dispatcher_ = new CommandDispatcher(this);

    autoSendTimer_.setSingleShot(false);

    initUi();
    initSignals();
    initDispatcher();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::initUi()
{
    // 列出串口
    ui->comboPort->clear();
    const auto ports = QSerialPortInfo::availablePorts();
    for (const auto &info : ports) {
        ui->comboPort->addItem(info.portName());
    }

    // 波特率
    ui->comboBaud->clear();
    ui->comboBaud->addItems({"9600", "19200", "38400", "57600", "115200"});
    ui->comboBaud->setCurrentText("115200");

    ui->labelTxCount->setText("TX: 0");
    ui->labelRxCount->setText("RX: 0");
    ui->spinInterval->setValue(1000);
}

void MainWindow::initSignals()
{
    connect(ui->btnOpen, &QPushButton::clicked,
            this, &MainWindow::onOpenClicked);
    connect(ui->btnClose, &QPushButton::clicked,
            this, &MainWindow::onCloseClicked);
    connect(ui->btnSend, &QPushButton::clicked,
            this, &MainWindow::onSendClicked);

    connect(ui->btnClearRecv, &QPushButton::clicked,
            this, &MainWindow::onClearRecv);
    connect(ui->btnClearLog, &QPushButton::clicked,
            this, &MainWindow::onClearLog);

    // 串口 → 解析
    connect(serial_, &SerialService::received,
            parser_, &ProtocolParser::feed);

    // 解析 → 分发
    connect(parser_, &ProtocolParser::frameParsed,
            dispatcher_, &CommandDispatcher::dispatch);

    // 日志合流
    auto logToUi = [this](const QString &msg){
        ui->textEditLog->append(msg);
    };
    connect(serial_, &SerialService::log, logToUi);
    connect(parser_, &ProtocolParser::log, logToUi);
    connect(dispatcher_, &CommandDispatcher::log, logToUi);

    // 统计
    connect(serial_, &SerialService::sendCountUpdated,
            this, [this](quint64 bytes){
                ui->labelTxCount->setText(QString("TX: %1").arg(bytes));
            });
    connect(serial_, &SerialService::recvCountUpdated,
            this, [this](quint64 bytes){
                ui->labelRxCount->setText(QString("RX: %1").arg(bytes));
            });

    // 自动发送
    connect(ui->chkAutoSend, &QCheckBox::toggled,
            this, [this](bool on){
                if (on) {
                    autoSendTimer_.start(ui->spinInterval->value());
                } else {
                    autoSendTimer_.stop();
                }
            });

    connect(&autoSendTimer_, &QTimer::timeout,
            this, &MainWindow::onSendClicked);

    // 改变间隔时实时更新
    connect(ui->spinInterval, QOverload<int>::of(&QSpinBox::valueChanged),
            this, [this](int v){
                if (autoSendTimer_.isActive())
                    autoSendTimer_.start(v);
            });
}

void MainWindow::initDispatcher()
{
    // 示例: CMD = 0x01, 认为是 JSON 文本
    dispatcher_->registerHandler(0x01, [this](const QByteArray &payload){
        QString s = QString::fromUtf8(payload);
        ui->textEditRecv->append(withTimestamp("CMD 0x01(JSON): " + s));
    });

    // 示例: CMD = 0x02, 以二进制十六进制显示
    dispatcher_->registerHandler(0x02, [this](const QByteArray &payload){
        ui->textEditRecv->append(withTimestamp(
            "CMD 0x02(BIN): " + payload.toHex(' ')
        ));
    });
}

void MainWindow::onOpenClicked()
{
    QString port = ui->comboPort->currentText();
    int baud = ui->comboBaud->currentText().toInt();
    if (port.isEmpty()) {
        QMessageBox::warning(this, "错误", "没有可用串口");
        return;
    }
    serial_->open(port, baud);
}

void MainWindow::onCloseClicked()
{
    serial_->close();
}

void MainWindow::onSendClicked()
{
    QString text = ui->lineEditSend->text().trimmed();
    if (text.isEmpty())
        return;

    QByteArray data;

    if (ui->chkHexSend->isChecked()) {
        // 解析为十六进制字节
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
        QStringList parts = text.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
#else
        QStringList parts = text.split(QRegExp("\\s+"), Qt::SkipEmptyParts);
#endif
        for (const QString &p : parts) {
            bool ok = false;
            int v = p.toInt(&ok, 16);
            if (!ok || v < 0 || v > 255) {
                QMessageBox::warning(this, "错误",
                                     QString("无效字节: '%1'").arg(p));
                return;
            }
            data.append(static_cast<char>(v));
        }
    } else {
        data = text.toUtf8();
    }

    serial_->send(data);
}

void MainWindow::onClearRecv()
{
    ui->textEditRecv->clear();
}

void MainWindow::onClearLog()
{
    ui->textEditLog->clear();
}

分析评价:

  • MainWindow 只做三件事:初始化 UI、连信号、简单业务决策(比如是否 hex 发送)。
    发送、接收、解析、分发全部交给下面的类处理,结构清晰。
  • 自动发送用一个 QTimer 调 onSendClicked(),充分复用已有逻辑,避免重复写发送代码。
  • 解析出来的帧由 CommandDispatcher 按 CMD 处理。Demo 里给了一个 JSON 文本的例子、一个二进制显示的例子,你将来可以替换成自己的业务逻辑。

总体评价与扩展建议

  1. 解耦程度

    • 串口层只负责 IO。
    • 协议层只负责组帧、校验。
    • 分发层只负责按 CMD 区分业务。
    • UI 只负责交互和展示。
      后续不管改协议、换设备、换 UI,都改自己的那一层即可。
  2. 可维护性

    每个类职责单一,文件不长,不会出现一个 MainWindow.cpp 两千行全是逻辑的情况。

  3. 线程

    全部在 UI 线程,不引入多线程复杂度。一般串口调试工具这点已经够用。如果后面数据量巨大,再把 SerialService 或解析层单独 moveToThread 即可。

  4. 扩展点

    • 在 ProtocolParser 里加帧计数、错误计数。
    • CommandDispatcher 增加默认处理策略,比如未知 CMD 也打印 payload。
    • 用 QSettings 保存串口号、波特率、窗口大小等配置。
    • 加一个"导出日志到文件"的功能。

如果你愿意,我可以下一步帮你做一件事:

你告诉我你当前工程里已经有的文件和控件名,我按这个结构帮你"对号入座",改成一份几乎可以直接复制进你项目的版本。

相关推荐
世转神风-7 小时前
winDbg专有名词解释
qt·windbg
2401_8534482311 小时前
项目图片功能制作
linux·数据库·qt·系统移植
深蓝海拓11 小时前
QT中具有重载特性的一些部件
python·qt·pyqt
yangops11 小时前
QT Creator解决: C3861: “_mm_loadu_si64”: 找不到标识符
qt·_mm_loadu_si64·c3861
火山灿火山12 小时前
Qt常用控件(四)
开发语言·qt
蓝天智能15 小时前
Qt实战:Unix_Linux下QTableView Checkbox不显示?问题排查+样式定制全攻略
linux·qt·unix
蓝天智能15 小时前
QT实战:设置按钮背景图标的几种方式及分析
开发语言·qt
小c君tt15 小时前
FFmpeg音视频-库使用2
qt·ffmpeg·音视频
追烽少年x16 小时前
QScrollArea类和QSroller类的简单介绍
qt