使用Qt Create18基于 Qt 6 + QtSerialBus ,运行于 WSL2 (Ubuntu) 环境,记录下最初调通了的的测试版,使用tcp工具运行在windows端,可与运行在WSL2下的程序进行modbus tcp通讯。先记录下初版,后期有机会加了应用层调通的话再记录。备注下:代码基本上都是基础普通的AI助手生成的。

之前两个文档介绍了前期准备工作
Windows Qt Creator + WSL Ubuntu 编译 x86_64 Linux 程序环境搭建说明-CSDN博客
MobaXterm测试运行Qt Create+wsl编译的执行文件-CSDN博客

一:小程序整体结构

EmsDiagnoseModbus
├── main.cpp                    // 程序入口、信号处理
├── mainwindow.h/cpp     // 主窗口、定时器调度、模块整合
├── modbusserver/          // Qt Modbus TCP 服务端模块
│   ├── modbusserver.h
│   └── modbusserver.cpp
└── diagnosedata/           // 诊断数据管理层(点表、数据交互)
    ├── diagnosedatamanager.h
    └── diagnosedatamanager.cpp

服务接口 → diagnosedatamanager(统一维护点表+缓存数据)
                ↓(定时器定时拉取/更新)
        MainWindow 定时同步任务
                ↓
        ModbusServer::syncDataFromDiagnoseData()
                ↓(setData写入寄存器)
        Qt Modbus 寄存器映射
                ↓
Windows上位机(Modbus TCP客户端)读写数据

  • 数据管理层(diagnosedatamanager) 采用单例模式实现全局数据中心,统一维护全部诊断点表、寄存器地址定义、测点缓存。封装线程安全的读写接口与结构体式批量数据更新接口,解决多参数传参臃肿问题,同时区分保持寄存器、输入寄存器、线圈、离散输入四类 Modbus 标准数据区,适配工业点表规范。
  • Modbus 通讯层(modbusserver) 基于 Qt 原生 QModbusTcpServer 实现标准 Modbus TCP 服务端,监听全网网卡(0.0.0.0)+ 标准 502 端口,无需端口转发即可被局域网内 Windows 上位机直连访问。规避 QtSerialBus 框架死循环问题,仅保留读请求回调,配置固定从站地址为 1,兼容主流 Modbus 客户端工具。
  • 主逻辑调度层(MainWindow) 作为程序主载体,通过多定时器分工完成业务调度:定时采集 EMS 诊断数据、定时将缓存数据同步至 Modbus 寄存器、定时刷新本地 UI 界面展示实时数据。将数据采集逻辑封装为独立槽函数,替代 Lambda 表达式,代码规范易迭代。
  • 程序入口层(main.cpp) 实现 Linux/WSL 环境下 SIGINT(Ctrl+C) 信号捕获,保障终端快捷键退出、窗口关闭两种方式均可释放端口与资源,彻底解决端口残留占用问题。
二:具体代码
2.1:main.cpp
#include <signal.h>
#include <QCoreApplication>
#include <QDebug>
#include "mainwindow.h"

// 信号处理函数
void sigintHandler(int sig)
{
    Q_UNUSED(sig);
    qDebug() << "\n收到Ctrl+C信号,退出...";
    QCoreApplication::quit();
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 注册SIGINT信号处理
    signal(SIGINT, sigintHandler);

    MainWindow w;
    w.show();

    return a.exec();
}
2.2:mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "modbusserver/modbusserver.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    void onRefreshUiData();   // UI刷新槽函数
    void onCollectData();     // 数据采集槽函数

private:
    Ui::MainWindow *ui;
    ModbusServer *m_modbusServer;
    QTimer *m_uiRefreshTimer;
    QTimer *m_collectTimer;
    QTimer *m_syncTimer;
};

#endif // MAINWINDOW_H
2.3:mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "diagnosedata/diagnosedatamanager.h"
#include <QDebug>
#include <QTimer>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->setWindowTitle("诊断数据 + Modbus TCP 服务端");

    // ====================== Modbus服务启动 ======================
    m_modbusServer = new ModbusServer(this);
    bool startResult = m_modbusServer->startServer();
    if (!startResult)
    {
        qDebug() << "[ERROR] Modbus服务启动失败";
    }

    // ====================== UI刷新定时器(1秒刷新一次界面) ======================
    m_uiRefreshTimer = new QTimer(this);
    m_uiRefreshTimer->setInterval(1000);
    connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::onRefreshUiData);
    m_uiRefreshTimer->start();

    // ====================== 数据采集定时器(2秒更新一次诊断数据) ======================
    m_collectTimer = new QTimer(this);
    m_collectTimer->setInterval(2000);
    // 【修改点】连接到独立槽函数,替代lambda表达式
    connect(m_collectTimer, &QTimer::timeout, this, &MainWindow::onCollectData);
    m_collectTimer->start();

    // ====================== 寄存器同步定时器(1秒同步一次数据到Modbus寄存器) ======================
    m_syncTimer = new QTimer(this);
    m_syncTimer->setInterval(1000);
    connect(m_syncTimer, &QTimer::timeout, m_modbusServer, &ModbusServer::syncDataFromDiagnoseData);
    m_syncTimer->start();
}

MainWindow::~MainWindow()
{
    if (m_modbusServer) {
        m_modbusServer->stopServer();
        delete m_modbusServer;
        m_modbusServer = nullptr;
        qDebug() << "[INFO] Modbus服务已停止,端口已释放";
    }
    DiagnoseDataManager::destroyInstance();
    delete ui;
}

// ====================== UI刷新函数 ======================
void MainWindow::onRefreshUiData()
{
    auto* dataMgr = DiagnoseDataManager::getInstance();

    double volt  = dataMgr->getHoldingData(DiagnoseDataManager::REG_VOLTAGE) / 10.0;
    double curr  = dataMgr->getHoldingData(DiagnoseDataManager::REG_CURRENT) / 10.0;
    double pAct  = dataMgr->getHoldingData(DiagnoseDataManager::REG_POWER_ACTIVE) / 10.0;
    int temp     = dataMgr->getHoldingData(DiagnoseDataManager::REG_TEMP);
    int status   = dataMgr->getInputData(DiagnoseDataManager::REG_RUN_STATUS);

    ui->lab_volt->setText(QString("电压:%1 V").arg(volt));
    ui->lab_curr->setText(QString("电流:%1 A").arg(curr));
    ui->lab_power->setText(QString("有功功率:%1 kW").arg(pAct));
    ui->lab_temp->setText(QString("设备温度:%2 ℃").arg(temp));
    ui->lab_status->setText(QString("运行状态:%1").arg(status == 1 ? "正常运行" : "停止"));
}

// ====================== 【新增】数据采集函数(原lambda逻辑迁移到此处) ======================
void MainWindow::onCollectData()
{
    auto* dataMgr = DiagnoseDataManager::getInstance();

    // 构造业务数据结构体
    DiagnoseDataManager::DiagnoseBizData bizData;
    bizData.voltage        = 2203;   // 电压 220.3V(放大10倍)
    bizData.current        = 142;    // 电流 14.2A(放大10倍)
    bizData.powerActive    = 2860;   // 有功功率 286.0kW(放大10倍)
    bizData.powerReactive  = 175;    // 无功功率 17.5kvar(放大10倍)
    bizData.powerFactor    = 930;    // 功率因数 0.93(放大1000倍)
    bizData.temperature    = 36;     // 设备温度 36℃

    // 批量更新所有诊断数据
    dataMgr->batchUpdateDiagnoseData(bizData);

    // 更新输入寄存器(状态、告警码)
    dataMgr->setInputData(DiagnoseDataManager::REG_RUN_STATUS, 1);
    dataMgr->setInputData(DiagnoseDataManager::REG_ALARM_CODE, 0);
}
2.4:diagnosedatamanager.h
#ifndef DIAGNOSEDATAMANAGER_H
#define DIAGNOSEDATAMANAGER_H

#include <QObject>
#include <QMutex>
#include <QMutexLocker>

class DiagnoseDataManager : public QObject
{
    Q_OBJECT
public:
    static DiagnoseDataManager* getInstance();
    static void destroyInstance();

    // ====================== 寄存器地址枚举(点表定义) ======================
    enum HoldingRegAddr {
        REG_VOLTAGE        = 0,
        REG_CURRENT        = 1,
        REG_POWER_ACTIVE   = 2,
        REG_POWER_REACTIVE = 3,
        REG_POWER_FACTOR   = 4,
        REG_TEMP           = 5,
        HOLD_REG_TOTAL     = 20  // 总长度预留,后续新增测点直接用
    };

    enum InputRegAddr {
        REG_RUN_STATUS = 0,
        REG_ALARM_CODE = 1,
        INPUT_REG_TOTAL = 10
    };

    enum CoilsAddr {
        COIL_SWITCH_1 = 0,
        COIL_SWITCH_2 = 1,
        COIL_TOTAL    = 8
    };

    enum DiscreteInputAddr {
        DI_FAULT_1 = 0,
        DI_FAULT_2 = 1,
        DI_TOTAL   = 8
    };

    // ====================== 业务数据结构体(批量更新用) ======================
    struct DiagnoseBizData
    {
        // 保持寄存器对应测点 - 后续新增直接在这里加成员
        quint16 voltage;        // 电压
        quint16 current;        // 电流
        quint16 powerActive;    // 有功功率
        quint16 powerReactive;  // 无功功率
        quint16 powerFactor;    // 功率因数
        quint16 temperature;    // 设备温度
    };

    // ====================== 单个测点读写接口 ======================
    quint16 getHoldingData(int addr);
    void setHoldingData(int addr, quint16 val);

    quint16 getInputData(int addr);
    void setInputData(int addr, quint16 val);

    bool getCoilData(int addr);
    void setCoilData(int addr, bool val);

    bool getDiscreteInputData(int addr);
    void setDiscreteInputData(int addr, bool val);

    // ====================== 批量更新接口(替换原多参数版本) ======================
    void batchUpdateDiagnoseData(const DiagnoseBizData& data);

private:
    explicit DiagnoseDataManager(QObject *parent = nullptr);
    ~DiagnoseDataManager() override;

    static DiagnoseDataManager* m_instance;
    static QMutex m_staticMutex;

    // 寄存器数据缓存
    quint16 m_holdingReg[HOLD_REG_TOTAL] = {0};
    quint16 m_inputReg[INPUT_REG_TOTAL]  = {0};
    bool    m_coils[COIL_TOTAL]          = {false};
    bool    m_discreteInput[DI_TOTAL]    = {false};

    // 数据读写锁
    QMutex m_dataMutex;
};

#endif // DIAGNOSEDATAMANAGER_H
2.5:diagnosedatamanager.cpp
#include "diagnosedatamanager.h"

// 单例初始化
DiagnoseDataManager* DiagnoseDataManager::m_instance = nullptr;
QMutex DiagnoseDataManager::m_staticMutex;

DiagnoseDataManager* DiagnoseDataManager::getInstance()
{
    QMutexLocker locker(&m_staticMutex);
    if (m_instance == nullptr) {
        m_instance = new DiagnoseDataManager();
    }
    return m_instance;
}

void DiagnoseDataManager::destroyInstance()
{
    QMutexLocker locker(&m_staticMutex);
    if (m_instance != nullptr) {
        delete m_instance;
        m_instance = nullptr;
    }
}

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

DiagnoseDataManager::~DiagnoseDataManager()
{
}

// ====================== 保持寄存器读写 ======================
quint16 DiagnoseDataManager::getHoldingData(int addr)
{
    if (addr < 0 || addr >= HOLD_REG_TOTAL)
        return 0;
    QMutexLocker locker(&m_dataMutex);
    return m_holdingReg[addr];
}

void DiagnoseDataManager::setHoldingData(int addr, quint16 val)
{
    if (addr < 0 || addr >= HOLD_REG_TOTAL)
        return;
    QMutexLocker locker(&m_dataMutex);
    m_holdingReg[addr] = val;
}

// ====================== 输入寄存器读写 ======================
quint16 DiagnoseDataManager::getInputData(int addr)
{
    if (addr < 0 || addr >= INPUT_REG_TOTAL)
        return 0;
    QMutexLocker locker(&m_dataMutex);
    return m_inputReg[addr];
}

void DiagnoseDataManager::setInputData(int addr, quint16 val)
{
    if (addr < 0 || addr >= INPUT_REG_TOTAL)
        return;
    QMutexLocker locker(&m_dataMutex);
    m_inputReg[addr] = val;
}

// ====================== 线圈读写 ======================
bool DiagnoseDataManager::getCoilData(int addr)
{
    if (addr < 0 || addr >= COIL_TOTAL)
        return false;
    QMutexLocker locker(&m_dataMutex);
    return m_coils[addr];
}

void DiagnoseDataManager::setCoilData(int addr, bool val)
{
    if (addr < 0 || addr >= COIL_TOTAL)
        return;
    QMutexLocker locker(&m_dataMutex);
    m_coils[addr] = val;
}

// ====================== 离散输入读写 ======================
bool DiagnoseDataManager::getDiscreteInputData(int addr)
{
    if (addr < 0 || addr >= DI_TOTAL)
        return false;
    QMutexLocker locker(&m_dataMutex);
    return m_discreteInput[addr];
}

void DiagnoseDataManager::setDiscreteInputData(int addr, bool val)
{
    if (addr < 0 || addr >= DI_TOTAL)
        return;
    QMutexLocker locker(&m_dataMutex);
    m_discreteInput[addr] = val;
}

// ====================== 批量更新业务数据 ======================
void DiagnoseDataManager::batchUpdateDiagnoseData(const DiagnoseBizData& data)
{
    QMutexLocker locker(&m_dataMutex);
    // 一次性赋值所有测点,保证数据一致性
    m_holdingReg[REG_VOLTAGE]        = data.voltage;
    m_holdingReg[REG_CURRENT]        = data.current;
    m_holdingReg[REG_POWER_ACTIVE]   = data.powerActive;
    m_holdingReg[REG_POWER_REACTIVE] = data.powerReactive;
    m_holdingReg[REG_POWER_FACTOR]   = data.powerFactor;
    m_holdingReg[REG_TEMP]           = data.temperature;
}
2.6:modbusserver.h
#ifndef MODBUSSERVER_H
#define MODBUSSERVER_H

#include <QObject>
#include <QtSerialBus/QModbusTcpServer>
#include "../diagnosedata/diagnosedatamanager.h"

class ModbusServer : public QModbusTcpServer
{
    Q_OBJECT
public:
    explicit ModbusServer(QObject *parent = nullptr);
    ~ModbusServer() override;

    /**
     * @brief 启动Modbus服务
     * @param port 监听端口,默认502(标准Modbus端口)
     * @param listenAddress 监听地址,默认0.0.0.0(所有网卡)
     * @return 启动成功返回true
     */
    bool startServer(quint16 port = 502, const QString& listenAddress = "0.0.0.0");

    // 停止服务,释放端口
    void stopServer();

    // 同步诊断数据到Modbus寄存器
    void syncDataFromDiagnoseData();

protected:
    // 重写读寄存器回调,保留调试日志
    bool readData(QModbusDataUnit *newData) const override;

private slots:
    // 捕获服务错误信息
    void onErrorOccurred(QModbusDevice::Error error);

private:
    // 初始化寄存器映射表
    void initRegisterMap();
};

#endif // MODBUSSERVER_H
2.7:modbusserver.cpp
#include "modbusserver.h"
#include <QDebug>
#include <QModbusPdu>

ModbusServer::ModbusServer(QObject *parent)
    : QModbusTcpServer(parent)
{
    // 绑定错误信号
    connect(this, &QModbusServer::errorOccurred, this, &ModbusServer::onErrorOccurred);

    // 绑定状态变化信号(调试用)
    connect(this, &QModbusServer::stateChanged, this, [](QModbusDevice::State state){
        qDebug() << "[DEBUG] Modbus服务状态变化:" << state;
    });

    // 初始化寄存器映射
    initRegisterMap();

    // 强制设置从站地址为1,和上位机指令匹配
    setServerAddress(1);
    qDebug() << "[DEBUG] 服务端从站地址设置为:" << serverAddress();
}

ModbusServer::~ModbusServer()
{
    stopServer();
}

void ModbusServer::initRegisterMap()
{
    QModbusDataUnitMap regMap;

    // 保持寄存器映射
    regMap.insert(QModbusDataUnit::HoldingRegisters,
                   QModbusDataUnit(QModbusDataUnit::HoldingRegisters, 0, DiagnoseDataManager::HOLD_REG_TOTAL));

    // 输入寄存器映射
    regMap.insert(QModbusDataUnit::InputRegisters,
                   QModbusDataUnit(QModbusDataUnit::InputRegisters, 0, DiagnoseDataManager::INPUT_REG_TOTAL));

    // 线圈映射
    regMap.insert(QModbusDataUnit::Coils,
                   QModbusDataUnit(QModbusDataUnit::Coils, 0, DiagnoseDataManager::COIL_TOTAL));

    // 离散输入映射
    regMap.insert(QModbusDataUnit::DiscreteInputs,
                   QModbusDataUnit(QModbusDataUnit::DiscreteInputs, 0, DiagnoseDataManager::DI_TOTAL));

    // 设置映射表
    bool mapOk = setMap(regMap);
    if (!mapOk) {
        qDebug() << "[ERROR] 寄存器映射设置失败:" << errorString();
    } else {
        qDebug() << "[INFO] 寄存器映射初始化成功";
    }
}

bool ModbusServer::startServer(quint16 port, const QString& listenAddress)
{
    // 显式设置监听地址 - 0.0.0.0表示监听所有网卡
    setConnectionParameter(QModbusDevice::NetworkAddressParameter, listenAddress);
    // 显式设置监听端口
    setConnectionParameter(QModbusDevice::NetworkPortParameter, port);

    // 启动服务
    if (!connectDevice()) {
        qDebug() << "[ERROR] Modbus服务启动失败:" << errorString();
        return false;
    }

    qDebug() << "[INFO] ========================================";
    qDebug() << "[INFO] Modbus TCP服务启动成功";
    qDebug() << "[INFO] 监听地址:" << listenAddress;
    qDebug() << "[INFO] 监听端口:" << port;
    qDebug() << "[INFO] 从站地址:" << serverAddress();
    qDebug() << "[INFO] ========================================";
    return true;
}

void ModbusServer::stopServer()
{
    if (state() == QModbusDevice::ConnectedState) {
        disconnectDevice();
        qDebug() << "[INFO] Modbus服务已停止,端口已释放";
    }
}

bool ModbusServer::readData(QModbusDataUnit *newData) const
{
    qDebug() << "\n[DEBUG] 收到读寄存器请求";
    qDebug() << "[DEBUG] 寄存器类型:" << newData->registerType();
    qDebug() << "[DEBUG] 起始地址:" << newData->startAddress();
    qDebug() << "[DEBUG] 读取数量:" << newData->valueCount();

    // 调用父类原生实现,从寄存器映射中读取数据
    bool result = QModbusTcpServer::readData(newData);

    qDebug() << "[DEBUG] 读操作结果:" << (result ? "成功" : "失败");
    if (result && newData->valueCount() > 0) {
        qDebug() << "[DEBUG] 首个寄存器值:" << newData->value(0);
    }
    return result;
}

void ModbusServer::onErrorOccurred(QModbusDevice::Error error)
{
    qDebug() << "[ERROR] 服务发生错误,错误码:" << error;
    qDebug() << "[ERROR] 错误详情:" << errorString();
}

void ModbusServer::syncDataFromDiagnoseData()
{
    auto* dataMgr = DiagnoseDataManager::getInstance();

    // 同步保持寄存器
    for (int i = 0; i < DiagnoseDataManager::HOLD_REG_TOTAL; ++i) {
        quint16 val = dataMgr->getHoldingData(i);
        setData(QModbusDataUnit::HoldingRegisters, i, val);
    }

    // 同步输入寄存器
    for (int i = 0; i < DiagnoseDataManager::INPUT_REG_TOTAL; ++i) {
        quint16 val = dataMgr->getInputData(i);
        setData(QModbusDataUnit::InputRegisters, i, val);
    }

    // 同步线圈
    for (int i = 0; i < DiagnoseDataManager::COIL_TOTAL; ++i) {
        bool val = dataMgr->getCoilData(i);
        setData(QModbusDataUnit::Coils, i, val ? 1 : 0);
    }

    // 同步离散输入
    for (int i = 0; i < DiagnoseDataManager::DI_TOTAL; ++i) {
        bool val = dataMgr->getDiscreteInputData(i);
        setData(QModbusDataUnit::DiscreteInputs, i, val ? 1 : 0);
    }
}
三:测试结果

网络调试助手运行在windows
modbus tcp小程序运行在WSL2

注意在WSL2中运行程序时需要使用sudo指令运行(端口号小于1024,如502得使用sudo权限)

使用MobaXterm工具用cd 进入程序所在路径,使用下面的指令可以将小程序窗体显示出来

sudo DISPLAY=:0 ./程序名称xxx -platform xcb

四:注意事项

4.1:使用Qt Create18建立程序时要将程序建立在WSL路径下,(按住shift再点浏览按钮可选择WSL路径)。
4.2:WSL中解决sudo 运行X11问题,保证GUI能弹出来,需要使用下面的命令

# 1. 先给所有用户开放X11访问权限(只需要执行一次)

xhost +

# 2. 以root权限运行程序,绑定502端口

sudo DISPLAY=:0 ./程序名xxx -platform xcb

4.3:实测,使用ubuntu终端也可以直接运行小程序,不确定是不是执行了xhost +指令

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐