天天看點

軟體設計開發筆記3:基于QT的Modbus RTU主站

  Modbus是一種常見的工業系統通訊協定。在我們的設計開發工作中經常使用到它。在這一篇中我們将簡單實作一個基于QT的Modbus RTU主站上位工具。

1、概述

  Modbus RTU主站應用很常見,有一些是通用的,有一些是專用的。而這裡我們希望實作一個主要針對我們的産品調試的Modbus RTU主站工具。

  在開始軟體設計之前,我們先來簡略地分析一下,實作這樣一個Modbus RTU主站工具包含的主要内容有哪些。我們認為軟體需要如下幾個方面的内容:

(1)、序列槽參數的配置

  Modbus RTU通過序列槽來實作通訊,是以我們需要對序列槽相關的參數進行配置。對序列槽的配置主要是序列槽名、波特率、校驗位、資料位和停止位等。對于這些參數我們讓使用者可以根據需要選擇。

  而序列槽号,我們希望軟體可以自動搜尋目前可用的序列槽清單。而且我們可以通過操作更新可用的序列槽清單。對序列槽的操作主要是序列槽的打開與關閉。

(2)、從站資訊的配置

  我們實作Modbus RTU主站應用就是通路從站的資料,是以我們需要在主站應用中配置從站的資訊。主要有站位址、資料類型、資料格式等,我們将其設定為可以選擇。

讀取從站的參數配置,主要是起始位址、讀取的數量。寫從站參數的配置,主要是起始位址、寫入的數量以及寫入的數值。

(3)、對從站的操作

  Modbus RTU主站對從站的操作無非是讀從站資料和寫從站資料,我們通過制定讀寫的寄存器類型、起始位址、數量等通過按鈕操作來實作讀寫指令的發送。

  除了手動操作讀寫外,很多時候我們可能需要Modbus RTU主站自動周期性的讀取從站的資料。是以我們讓其可以選擇以多長的周期自動循環讀取。

(4)、對資訊的顯示

  接收資訊的顯示,作為一款工具軟體, 我們當然希望看到我們發給從站的指令究竟有沒有成功,最簡單的和直覺的辦法就是将接收到的資訊顯示出來。對于Modbus RTU主站當然是顯示對應的位址的值。

  同樣的,我們有時候想要看到發送和接收到的原始封包,是以我們對發送和接收到的封包也作相應的顯示。

  對于個别資料有時候我們還希望看到他的變化趨勢,是以我們可以添加一個圖形顯示,用以顯示我們制定的資料的變化趨勢。

  運作狀态的顯示, 我們希望對操作的狀态進行回報以訓示操作的動作是否執行,是以我們需要狀态欄來實作這一需求。

2、界面設計

  根據上一節中分析的需求,我們先來設計軟體的界面。我們在QT中基于QMainWindow類生成一個操作界面,包括菜單欄、工具欄和狀态欄以滿足需求中對狀态顯示及操作指令的要求。

  而在中間顯示區域,我們将其劃分為2列。在左邊的一列從上到下設定:序列槽配置操作區域和讀寫從站的互動配置區域。在右側的一列從上到下設定:動态曲線顯示區域、收發消息顯示區域以及直接輸入封包發送指令的輸入區域。具體的界面設定如下圖所示:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  完成如上圖的布局後,我們可以選擇在屬性中配置控件的參數,也可以在代碼中添加相關的參數。在這裡在代碼中通過初始化形式完成參數的設定。完成整個布局後我們先試着運作程式,正常運作則出現如下的界面:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  上圖就是完成布局後的運作界面,不過我們還沒有實作相應的編碼,是以目前尚不能實作我們第一節中所預想的功能。

3、編碼實作

  接下來這一小節,我們将來編碼實作相應的功能。我們主要将功能分為序列槽操作功能、從站操作功能以及資訊顯示功能三個部分來實作。

3.1、序列槽操作功能

  對序列槽的操作首先就是對序列槽參數的設定。我們在代碼中對界面上的序列槽号、波特率、資料位、校驗位和停止位的ComboBox控件進行初始化。其中序列槽号通過自動搜尋目前可用的序列槽來實作。具體的實作方式如下:

//搜尋序列槽
void MainWindow::SearchSerialPorts()
{
    ui->comboBoxPort->clear();

    foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
    {
        ui->comboBoxPort->addItem(info.portName());
    }
}      

  對序列槽的操作主要是序列槽的打開和關閉,在這裡因為是Modbus RTU主站應用,我們稱之為連接配接和斷開。建立或斷開與從站的連接配接實際就是對序列槽的配置與操作,隻是針對Modbus RTU作了一些封裝,具體實作如下:

//序列槽連接配接
void MainWindow::on_actionConnect_triggered()
{
    if (!modbusDevice)
        return;

    modbusDevice->setConnectionParameter(QModbusDevice::SerialPortNameParameter,ui->comboBoxPort->currentText());
    modbusDevice->setConnectionParameter(QModbusDevice::SerialBaudRateParameter,ui->comboBoxBaud->currentText().toInt());

    switch(ui->comboBoxParity->currentIndex())                   //設定奇偶校驗
    {
        case 0: modbusDevice->setConnectionParameter(QModbusDevice::SerialParityParameter,QSerialPort::NoParity);break;
        default: break;
    }

    switch(ui->comboBoxData->currentIndex())                   //設定資料位數
    {
        case 1:modbusDevice->setConnectionParameter(QModbusDevice::SerialDataBitsParameter,QSerialPort::Data8);break;
        default: break;
    }

    switch(ui->comboBoxStop->currentIndex())                     //設定停止位
    {
        case 1: modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::OneStop);break;
        case 2: modbusDevice->setConnectionParameter(QModbusDevice::SerialStopBitsParameter,QSerialPort::TwoStop);break;
        default: break;
    }

    modbusDevice->setTimeout(1000);
    modbusDevice->setNumberOfRetries(3);

    if (modbusDevice->connectDevice())
    {
        //開啟自動讀取
        if(ui->checkBoxAuto->isChecked())
        {
            connect(pollTimer,&QTimer::timeout, this, &MainWindow::ReadRequest);
            pollTimer->setInterval(ui->spinBoxInterval->value());
            pollTimer->start();
        }

        //連接配接槽函數
        //QObject::connect(serialPort, &QSerialPort::readyRead, this, &MainWindow::ReadSerialData);

        // 設定控件可否使用
        ui->actionConnect->setEnabled(false);
        ui->actionDisconnect->setEnabled(true);
        ui->actionRefresh->setEnabled(false);
    }
    else    //打開失敗提示
    {

        QMessageBox::information(this,tr("錯誤"),tr("連接配接從站失敗!"),QMessageBox::Ok);
    }
}      

3.2、從站操作功能

  在前面一節中我們已經設計過,對從站的操作包括手動按鈕讀取從站資料、手動按鈕寫入從站資料以及自動周期讀取從站資料。

  手動讀取從站資料是指點選按鈕時觸發一次讀從站的操作,而從站的位址、讀取的寄存器類型、讀取的寄存器起始位址和寄存器的數量均根據界面上相應的設定确定。具體的實作如下:

//讀資料請求
void MainWindow::ReadRequest()
{
    if (!modbusDevice)
    {
        QMessageBox::information(NULL,  "Title",  "尚未連接配接從站裝置");
        return;
    }

    QModbusDataUnit::RegisterType type;

    switch(ui->comboBoxDataType->currentIndex())
    {
        case 0:type=QModbusDataUnit::Coils;break;
        case 1:type=QModbusDataUnit::DiscreteInputs;break;
        case 2:type=QModbusDataUnit::InputRegisters;break;
        case 3:type=QModbusDataUnit::HoldingRegisters;break;
        default:type=QModbusDataUnit::Invalid;
    }

    int startAddress = ui->spinBoxStartRead->value();
    Q_ASSERT(startAddress >= 0 && startAddress < 10);

    // do not go beyond 10 entries
    quint16 numberOfEntries = qMin(quint16(ui->spinBoxNumberRead->value()), quint16(10 - startAddress));
    QModbusDataUnit readUnit=QModbusDataUnit(type, startAddress, numberOfEntries);

    statusBar()->clearMessage();

    if (auto *reply = modbusDevice->sendReadRequest(readUnit, ui->spinBoxStation->value()))
    {
        if (!reply->isFinished())
            connect(reply, &QModbusReply::finished, this, &MainWindow::ReadSerialData);
        else
            delete reply; // broadcast replies return immediately
    }
    else
    {
        statusBar()->showMessage(tr("Read error: ") + modbusDevice->errorString(), 5000);
    }

}      

  手動寫從站操作是指點選按鈕觸發一次寫從站操作,而從站的位址、寫入的寄存器類型、寫入的寄存器起始位址、寫入的寄存器的數量以及寫入的值均根據界面上相應的設定确定。而寄存器的值得輸入以“,”分割,具體的實作如下:

//寫資料請求
void MainWindow::WriteRequest(QList<quint16> values)
{
    if (!modbusDevice)
    {
        QMessageBox::information(NULL,  "Title",  "尚未連接配接從站裝置");
        return;
    }

    QModbusDataUnit::RegisterType type;

    switch(ui->comboBoxDataType->currentIndex())
    {
        case 0:type=QModbusDataUnit::Coils;break;
        case 1:type=QModbusDataUnit::DiscreteInputs;break;
        case 2:type=QModbusDataUnit::InputRegisters;break;
        case 3:type=QModbusDataUnit::HoldingRegisters;break;
        default:type=QModbusDataUnit::Invalid;
    }

    int startAddress = ui->spinBoxStartWrite->value();
    Q_ASSERT(startAddress >= 0 && startAddress < 10);

    QModbusDataUnit writeUnit = QModbusDataUnit(type,startAddress, values.size());
    for(int i=0; i<values.size(); i++)
    {
        writeUnit.setValue(i, values.at(i));
    }

    //serverEdit 發生給slave的ID
    if (auto *reply = modbusDevice->sendWriteRequest(writeUnit,ui->spinBoxStation->value()))
    {
        if (!reply->isFinished())
        {
            connect(reply, &QModbusReply::finished, this, [this, reply]() {
                if (reply->error() == QModbusDevice::ProtocolError) {
                    qDebug() << QString("Write response error: %1 (Mobus exception: 0x%2)")
                                .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16);
                } else if (reply->error() != QModbusDevice::NoError) {
                    qDebug() << QString("Write response error: %1 (code: 0x%2)").
                                arg(reply->errorString()).arg(reply->error(), -1, 16);
                }
                reply->deleteLater();
            });
        }
        else
        {
            reply->deleteLater();
        }
    }
    else
    {
        qDebug() << QString(("Write error: ") + modbusDevice->errorString());
    }
}      

  對于自動周期性讀取從站資料我們通過一個計時器周期性操作,而從站的位址、讀取的寄存器類型、讀取的寄存器起始位址、寄存器的數量以及間隔時間通過界面設定。而其操作與手動按鈕觸發一樣。

3.3、資訊顯示功能

  對于資訊的顯示我們主要考慮3個方面的内容。一是讀取回來的從站資料結果顯示;二是上下行封包的監視;三是操作過程及狀态的顯示。

  首先是對讀取回來的從站資料進行顯示,在這裡我們将讀取的寄存器位址及其對應的資料顯示在消息框中。同時我們将部分資料在圖形顯示中以曲線的形式展示出來。

//曲線顯示
void MainWindow::ChartDisplay()
{
    QColor acolor[8]={Qt::red,Qt::blue,Qt::green,Qt::cyan,Qt::yellow,Qt::magenta,Qt::black,Qt::darkRed};
    QStringList name={"抛物線","正弦值","正弦值","固定值","固定值","固定值","固定值","固定值"};
    QVector<QPointF> list[8];
    QVector<QPointF> newlist[8];

    for(int j=0;j<8;j++)
    {
        list[j] = lineSeries[j]->pointsVector();//擷取現在圖中清單

        if (list[j].size() < 200)
        {
            //保持原來
            newlist[j] = list[j];
        }
        else
        {
            //錯位移動
            for(int i =1 ; i< list[j].size();i++)
            {
                newlist[j].append(QPointF(i-1,list[j].at(i).y()));
            }
        }

        newlist[j].append(QPointF(newlist[j].size(),values[j]));//最後補上新的資料

        lineSeries[j]->replace(newlist[j]);//替換更新

        lineSeries[j]->setName(name[j]);//設定曲線名稱
        lineSeries[j]->setPen(acolor[j]);//設定曲線顔色
        lineSeries[j]->setUseOpenGL(true);//openGl 加速

        //mChart->setTitle("Pressure Data");//設定圖示标題
        mChart->removeSeries(lineSeries[j]);
        mChart->addSeries(lineSeries[j]);
        mChart->createDefaultAxes();//設定坐标軸
    }

    ui->graphicsView->setChart(mChart);
}      

  其次對于上下行封包我們也将其顯示到消息顯示框中。在QT對Modbus協定進行封裝後,我們沒有辦法直接擷取上下行的封包,我們可以開啟日志答應功能,再從其中截取相應的封包。

QLoggingCategory::setFilterRules(QStringLiteral("qt.modbus* = true"));      

  而操作過程及狀态顯示則比較簡單,我們在狀态欄顯示相應的操作過程和操作的狀态。

4、小結

  完成了編碼調試後,我們尚需要對這一工具進行一些測試。首先我們安裝一個虛拟序列槽軟體用以虛拟我們用于測試的序列槽,并找到一款Modbus RTU的從站模拟軟體。當然有實際的從站和硬體的串行端口更好,在這裡我們先用軟體模拟。具體的配置如下圖所示:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  而Modbus RTU從站我們使用MThings來模拟,當然也可以使用其它Modbus RTU從站模拟軟體。我們模拟10個保持寄存器和10個線圈,之是以這麼設定是因為這兩種資料類型支援讀寫,友善我們測試。具體的配置如下圖所示:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  現在将我們設計的Modbus RTU主站運作起來,并使用它去通路我們剛才配置的Modbus RTU從站。首先我們實驗讀從站資料操作。測試的結果如下圖所示:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  這裡我們讀取從站從位址0開始的10個保持寄存器,并将值顯示在消息框和圖形中。我們模拟了2路正弦信号、1路抛物線信号和5路固定值信号。接下來我們測試一下寫操作。測試的結果如下圖所示:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  這裡對從站的從位址3開始的3個保持寄存器的值進行修改。設定的值分别是123、456和789,操作完成後我們檢視從站的結果如下:

軟體設計開發筆記3:基于QT的Modbus RTU主站

  上圖中與我們設定值的完全符合,說明我們的寫從站操作時正确的。到這裡我們基于QT的Modbus RTU主站就基本實作了。當然,我們還可以根據需要修改或添加一些功能以适應不同的應用需求。我們已經将代碼釋出到Gitee,歡迎下載下傳和交流。

下載下傳位址:​​​https://gitee.com/ErichMoonan/ModbusMaster​​

歡迎關注:

當然,如果您想及時了解我的部落格更新,不妨點選下方的【關注我】按鈕。