Modbus是一種常見的工業(yè)系統(tǒng)通訊協(xié)議。在我們的設計開發(fā)工作中經(jīng)常使用到它。在這一篇中我們將簡單實現(xiàn)一個基于QT的Modbus RTU主站上位工具。
1、概述
??Modbus RTU主站應用很常見,有一些是通用的,有一些是專用的。而這里我們希望實現(xiàn)一個主要針對我們的產(chǎn)品調(diào)試的Modbus RTU主站工具。
??在開始軟件設計之前,我們先來簡略地分析一下,實現(xiàn)這樣一個Modbus RTU主站工具包含的主要內(nèi)容有哪些。我們認為軟件需要如下幾個方面的內(nèi)容:
(1)、串口參數(shù)的配置
??Modbus RTU通過串口來實現(xiàn)通訊,所以我們需要對串口相關的參數(shù)進行配置。對串口的配置主要是串口名、波特率、校驗位、數(shù)據(jù)位和停止位等。對于這些參數(shù)我們讓使用者可以根據(jù)需要選擇。
??而串口號,我們希望軟件可以自動搜索當前可用的串口列表。而且我們可以通過操作更新可用的串口列表。對串口的操作主要是串口的打開與關閉。
(2)、從站信息的配置
??我們實現(xiàn)Modbus RTU主站應用就是訪問從站的數(shù)據(jù),所以我們需要在主站應用中配置從站的信息。主要有站地址、數(shù)據(jù)類型、數(shù)據(jù)格式等,我們將其設置為可以選擇。
讀取從站的參數(shù)配置,主要是起始地址、讀取的數(shù)量。寫從站參數(shù)的配置,主要是起始地址、寫入的數(shù)量以及寫入的數(shù)值。
(3)、對從站的操作
??Modbus RTU主站對從站的操作無非是讀從站數(shù)據(jù)和寫從站數(shù)據(jù),我們通過制定讀寫的寄存器類型、起始地址、數(shù)量等通過按鈕操作來實現(xiàn)讀寫命令的發(fā)送。
??除了手動操作讀寫外,很多時候我們可能需要Modbus RTU主站自動周期性的讀取從站的數(shù)據(jù)。所以我們讓其可以選擇以多長的周期自動循環(huán)讀取。
(4)、對信息的顯示
??接收信息的顯示,作為一款工具軟件, 我們當然希望看到我們發(fā)給從站的命令究竟有沒有成功,最簡單的和直觀的辦法就是將接收到的信息顯示出來。對于Modbus RTU主站當然是顯示對應的地址的值。
??同樣的,我們有時候想要看到發(fā)送和接收到的原始報文,所以我們對發(fā)送和接收到的報文也作相應的顯示。
??對于個別數(shù)據(jù)有時候我們還希望看到他的變化趨勢,所以我們可以添加一個圖形顯示,用以顯示我們制定的數(shù)據(jù)的變化趨勢。
??運行狀態(tài)的顯示, 我們希望對操作的狀態(tài)進行反饋以指示操作的動作是否執(zhí)行,所以我們需要狀態(tài)欄來實現(xiàn)這一需求。
2、界面設計
??根據(jù)上一節(jié)中分析的需求,我們先來設計軟件的界面。我們在QT中基于QMainWindow類生成一個操作界面,包括菜單欄、工具欄和狀態(tài)欄以滿足需求中對狀態(tài)顯示及操作命令的要求。
??而在中間顯示區(qū)域,我們將其劃分為2列。在左邊的一列從上到下設置:串口配置操作區(qū)域和讀寫從站的交互配置區(qū)域。在右側(cè)的一列從上到下設置:動態(tài)曲線顯示區(qū)域、收發(fā)消息顯示區(qū)域以及直接輸入報文發(fā)送命令的輸入?yún)^(qū)域。具體的界面設置如下圖所示:
??完成如上圖的布局后,我們可以選擇在屬性中配置控件的參數(shù),也可以在代碼中添加相關的參數(shù)。在這里在代碼中通過初始化形式完成參數(shù)的設置。完成整個布局后我們先試著運行程序,正常運行則出現(xiàn)如下的界面:
??上圖就是完成布局后的運行界面,不過我們還沒有實現(xiàn)相應的編碼,所以目前尚不能實現(xiàn)我們第一節(jié)中所預想的功能。
3、編碼實現(xiàn)
??接下來這一小節(jié),我們將來編碼實現(xiàn)相應的功能。我們主要將功能分為串口操作功能、從站操作功能以及信息顯示功能三個部分來實現(xiàn)。
3.1、串口操作功能
??對串口的操作首先就是對串口參數(shù)的設置。我們在代碼中對界面上的串口號、波特率、數(shù)據(jù)位、校驗位和停止位的ComboBox控件進行初始化。其中串口號通過自動搜索當前可用的串口來實現(xiàn)。具體的實現(xiàn)方式如下:
//搜索串口
void MainWindow::SearchSerialPorts()
{
ui->comboBoxPort->clear();
foreach(const QSerialPortInfo &info,QSerialPortInfo::availablePorts())
{
ui->comboBoxPort->addItem(info.portName());
}
}
??對串口的操作主要是串口的打開和關閉,在這里因為是Modbus RTU主站應用,我們稱之為連接和斷開。建立或斷開與從站的連接實際就是對串口的配置與操作,只是針對Modbus RTU作了一些封裝,具體實現(xiàn)如下:
//串口連接
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()) //設置數(shù)據(jù)位數(shù)
{
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();
}
//連接槽函數(shù)
//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、從站操作功能
??在前面一節(jié)中我們已經(jīng)設計過,對從站的操作包括手動按鈕讀取從站數(shù)據(jù)、手動按鈕寫入從站數(shù)據(jù)以及自動周期讀取從站數(shù)據(jù)。
??手動讀取從站數(shù)據(jù)是指點擊按鈕時觸發(fā)一次讀從站的操作,而從站的地址、讀取的寄存器類型、讀取的寄存器起始地址和寄存器的數(shù)量均根據(jù)界面上相應的設置確定。具體的實現(xiàn)如下:
//讀數(shù)據(jù)請求
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);
}
}
??手動寫從站操作是指點擊按鈕觸發(fā)一次寫從站操作,而從站的地址、寫入的寄存器類型、寫入的寄存器起始地址、寫入的寄存器的數(shù)量以及寫入的值均根據(jù)界面上相應的設置確定。而寄存器的值得輸入以“,”分割,具體的實現(xiàn)如下:
//寫數(shù)據(jù)請求
void MainWindow::WriteRequest(QList 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; isize(); i++)
{
writeUnit.setValue(i, values.at(i));
}
//serverEdit 發(fā)生給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());
}
}
??對于自動周期性讀取從站數(shù)據(jù)我們通過一個計時器周期性操作,而從站的地址、讀取的寄存器類型、讀取的寄存器起始地址、寄存器的數(shù)量以及間隔時間通過界面設置。而其操作與手動按鈕觸發(fā)一樣。
3.3、信息顯示功能
??對于信息的顯示我們主要考慮3個方面的內(nèi)容。一是讀取回來的從站數(shù)據(jù)結(jié)果顯示;二是上下行報文的監(jiān)視;三是操作過程及狀態(tài)的顯示。
??首先是對讀取回來的從站數(shù)據(jù)進行顯示,在這里我們將讀取的寄存器地址及其對應的數(shù)據(jù)顯示在消息框中。同時我們將部分數(shù)據(jù)在圖形顯示中以曲線的形式展示出來。
//曲線顯示
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 list[8];
QVector newlist[8];
for(int j=0;j<8;j++)
{
list[j] = lineSeries[j]->pointsVector();//獲取現(xiàn)在圖中列表
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]));//最后補上新的數(shù)據(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協(xié)議進行封裝后,我們沒有辦法直接獲取上下行的報文,我們可以開啟日志答應功能,再從其中截取相應的報文。
QLoggingCategory::setFilterRules(QStringLiteral("qt.modbus* = true"));
??而操作過程及狀態(tài)顯示則比較簡單,我們在狀態(tài)欄顯示相應的操作過程和操作的狀態(tài)。
4、小結(jié)
??完成了編碼調(diào)試后,我們尚需要對這一工具進行一些測試。首先我們安裝一個虛擬串口軟件用以虛擬我們用于測試的串口,并找到一款Modbus RTU的從站模擬軟件。當然有實際的從站和硬件的串行端口更好,在這里我們先用軟件模擬。具體的配置如下圖所示:
??而Modbus RTU從站我們使用MThings來模擬,當然也可以使用其它Modbus RTU從站模擬軟件。我們模擬10個保持寄存器和10個線圈,之所以這么設置是因為這兩種數(shù)據(jù)類型支持讀寫,方便我們測試。具體的配置如下圖所示:
??現(xiàn)在將我們設計的Modbus RTU主站運行起來,并使用它去訪問我們剛才配置的Modbus RTU從站。首先我們實驗讀從站數(shù)據(jù)操作。測試的結(jié)果如下圖所示:
??這里我們讀取從站從地址0開始的10個保持寄存器,并將值顯示在消息框和圖形中。我們模擬了2路正弦信號、1路拋物線信號和5路固定值信號。接下來我們測試一下寫操作。測試的結(jié)果如下圖所示:
??這里對從站的從地址3開始的3個保持寄存器的值進行修改。設定的值分別是123、456和789,操作完成后我們查看從站的結(jié)果如下:
??上圖中與我們設定值的完全符合,說明我們的寫從站操作時正確的。到這里我們基于QT的Modbus RTU主站就基本實現(xiàn)了。當然,我們還可以根據(jù)需要修改或添加一些功能以適應不同的應用需求。我們已經(jīng)將代碼發(fā)布到Gitee,歡迎下載和交流。
下載地址:https://gitee.com/ErichMoonan/ModbusMaster
-
MODBUS
+關注
關注
28文章
1718瀏覽量
76429 -
通訊協(xié)議
+關注
關注
10文章
263瀏覽量
20271 -
Qt
+關注
關注
1文章
300瀏覽量
37606 -
RTU
+關注
關注
0文章
385瀏覽量
28547
發(fā)布評論請先 登錄
相關推薦
評論