前言
Modbus在工業控制中的應用非常多,由於其免費使用加上一定的歷史環境,Modbus在PLC上的通訊應用非常多,本文主要介紹Mosbus TCP master(主站)的實現。
一、關於Modbus
Modbus是由MODICON公司開發的一種工業現場匯流排協議標準,隨後施耐德推出了基於TCP/IP的MOdbus協議:Modbus tcp;
Modbus協議是一項應用層報文傳輸協議,包括ASCII、RTU、TCP三種報文型別。
標準的Modbus協議物理層介面有RS232、RS422、RS485和乙太網介面,採用master/slave方式通訊。
Modbus有4種操作物件:線圈、離散輸入、輸入暫存器、保持暫存器
Coils、DiscreteInputs、InputRegisters、HoldingRegisters
- 線圈:PLC的輸出位,開關量,在MOdbus中可讀可寫;
- 離散輸入:PLC的輸入位,開關量,在Modbus中只讀;
- 輸入暫存器:PLC中只能從模擬量輸入端改變的暫存器,在MODBUS中只讀
- 保持暫存器:PLC中用於輸出模擬量訊號的暫存器,在MODBUS中可讀可寫
由於是基於QT去實現的ModbusTCP通訊,所以對Modbus的功能碼不需要做過多的掌握,瞭解即可。
二、Modbus TCP Master的實現
//主站的實現,一般都是上位機做主站,PLC做從站
1.封裝自己的Modbus類
QT.pro檔案中新增 serialbus 模組:
QT += core gui sql serialbus
讓自定義類繼承QObject,在標頭檔案中新增相應的標頭檔案
QModbusTcpClient 類 和 QModbusDataUnit 類
在自定義類中建立modbus TCP client 物件指標。
QModbusTcpClient *My_client;
2.Modbus 透過TCP/IP進行連線
自定義類的建構函式中例項化Modbus tcp物件:
My_client = new QModbusTcpClient();
Modbus TCP/IP協議進行連線的時候需要透過IP + Port ;
//埠號一般用502
1 /********************************************
2 * 函式名稱:Connect_to_modbus(QString IP_address,int Port)
3 * 功能:連線到modbus裝置
4 * 工作方式:
5 * 引數:
6 引數1:modbus裝置的IP地址 QString 型別
7 引數2:modbus裝置的埠號(一般用502) int 型別
8 * 返回值:成功返回true,失敗返回fasle。
9 * 備註:
10 * 修改記錄
11 *********************************************/
12 bool My_modbus_tcp::Connect_to_modbus(QString IP_address,int Port)
13 {
14 if(!My_client){
15 return false;
16 }
17
18 if (My_client->state() != QModbusDevice::ConnectedState) { //判斷當前連線狀態是否為斷開狀態
19
20 //配置modbus tcp的連線引數 IP + Port modbus協議的埠號為502
21 My_client->setConnectionParameter(QModbusDevice::NetworkAddressParameter,IP_address);
22 My_client->setConnectionParameter(QModbusDevice::NetworkPortParameter,Port);
23
24 if (!My_client->connectDevice()) {
25 qDebug()<< "連線modbus裝置失敗";
26 return false;
27 }
28 else {
29 qDebug()<< "成功連線到modbs裝置";
30 return true;
31 }
32 }
33
34 else {
35 My_client->disconnectDevice();
36 return false;
37 }
38
39 }
上述程式碼qDebug()<< "成功連線到modbs裝置"不太合理,判斷連線成功其實應該以Modbus的連線狀態來進行判斷的。
連線狀態參考幫助手冊 enum QModbusDevice::State:
有UnconnectedState、ConnectingState、ConnectedState、ClosingState4種
QModbusClient 類自帶了狀態改變的訊號:QModbusClient::stateChanged
//我在自定義類中新增了兩個訊號,statechange_on()和statechange_off();用於判斷當前是否連線
連線狀態相關的程式碼如下:
先在自定義類的建構函式中繫結訊號(QModbusClient::stateChanged)和槽(自定義槽函式)
1 connect(My_client, &QModbusClient::stateChanged,
2 this, &My_modbus_tcp::onStateChanged);//連線狀態發生改變時處理函式(connect or discennect)
實現狀態改變的槽函式,狀態進行變化時發出相應的訊號,可以使用該訊號進行訊號與槽的繫結實現狀態改變時的功能。
1 /********************************************
2 * 函式名稱:onStateChanged()
3 * 功能:監聽TCP連線的狀態,若狀態發生改變,發出對應的訊號
4 * 工作方式:
5 * 引數:無引數
6 * 返回值:無返回值
7 * 備註:
8 * 修改記錄:
9 *********************************************/
10 void My_modbus_tcp::onStateChanged() //連線狀態改變時的槽函式
11 {
12 if(My_client->state() == QModbusDevice::ConnectedState)
13 {
14 emit statechange_on();
15 }
16
17 else {
18 emit statechange_off();
19 }
20 }
3.Modbus 透過TCP/IP讀取資料
Modbus物件的4種資料都可以進行讀取,線圈和離散輸入都是位資料,結果只能是0/1;輸入暫存器和保持暫存器可以實現0x00~0xFF;
(1)讀取線圈資料
1 /********************************************
2 * 函式名稱:read_modbus_tcp_Coils(int start_add,quint16 numbers ,int Server_ID)
3 * 功能:傳送讀取modbus裝置線圈資料請求
4 * 工作方式:
5 * 引數:
6 * 引數1:int start_add 讀取的起始地址
7 * 引數2:quint16 numbers 讀取的個數
8 * 引數3:int Server_ID Modbus的裝置ID
9 * 返回值:成功返回true,失敗返回fasle。
10 * 備註:
11 * 修改記錄:
12 *********************************************/
13 bool My_modbus_tcp::read_modbus_tcp_Coils(int start_add,quint16 numbers,int Server_ID)
14 {
15 if (!My_client->state() == QModbusDevice::ConnectedState){
16 return false;
17 }
18
19 QModbusDataUnit ReadUnit(QModbusDataUnit::Coils,start_add,numbers);
20 qDebug()<< "配置ReadUnit完成";
21 if (auto *reply = My_client->sendReadRequest(ReadUnit, Server_ID)) //1是Server_ID
22 {
23 if (!reply->isFinished())
24 { qDebug()<< "準備進行訊號與槽連線";
25 QObject::connect(reply, &QModbusReply::finished,this,&My_modbus_tcp::ReadReady_Coils);
26 qDebug()<<"進入讀取的槽函式 ";
27 return true;
28 }
29 else
30 {
31 qDebug()<< "提前delete reply";
32 delete reply;
33 return false;
34 }
35
36 }
37
38 else {
39 qDebug()<< "提前退出";
40 return false;
41 }
42 }
1 /********************************************
2 * 函式名稱:ReadReady_Coils()
3 * 功能:接收到讀取請求後執行的槽函式
4 * 工作方式:
5 * 引數:無引數
6 * 返回值:沒有返回值
7 * 備註:
8 * 修改記錄
9 *********************************************/
10 void My_modbus_tcp::ReadReady_Coils()
11 {
12 qDebug()<< "開始執行槽函式";
13 QModbusReply *reply = qobject_cast<QModbusReply *>(sender());
14 if (!reply){
15 qDebug()<< "提前退出";
16 return ;
17 }
18 if (reply->error() == QModbusDevice::NoError)
19 { qDebug()<< "接收資料";
20 const QModbusDataUnit unit = reply->result();
21 for(uint16_t i=0; i< unit.valueCount(); i++)
22 {
23 /*
24 QByteArray AllData =unit.values(); //一次性讀完
25 */
26
27 uint16_t res=unit.value(i); //一個一個讀
28 Coils_Bufer[i] = static_cast<uint8_t>(res);
29 //讀完將資料儲存起來 Coils_Bufer[i] 自定的陣列 用來存放資料
30
31 }
32
33 }
34 else
35 {
36 }
37
38 reply->deleteLater(); // delete the reply
39 emit my_readC_finished(); //coils讀取完成後emit 讀取完成的訊號;
40
41 }
讀取離散變數時和讀取線圈資料一樣,唯一區別就是配置讀取資料單元時換成
QModbusDataUnit ReadUnit(QModbusDataUnit::DiscreteInputs,start_add,numbers);
離散變數讀取完成時發出自己的訊號(自定義訊號)。
(2)讀取保持暫存器資料
1 /********************************************
2 * 函式名稱:read_modbus_tcp_HoldingRegisters(int start_add,quint16 numbers ,int Server_ID)
3 * 功能:傳送讀取modbus裝置HoldingRegisters資料請求
4 * 工作方式:
5 * 引數
6 * 引數1:讀取資料的起始地址
7 * 引數2:讀取多少個資料
8 * 引數3:SerVer ID號
9 * 返回值:成功返回true,失敗返回fasle。
10 * 備註:
11 * QModbusDataUnit ReadUnit(QModbusDataUnit::HoldingRegisters,引數1,引數2);
12 * 引數1:讀取modbus裝置的起始地址 int 型別
13 引數2:讀取幾個modbus資料 quint16 型別
14 * 修改記錄:
15 *********************************************/
16 bool My_modbus_tcp::read_modbus_tcp_HoldingRegisters(int start_add,quint16 numbers ,int Server_ID)
17 {
18 QModbusDataUnit ReadUnit(QModbusDataUnit::HoldingRegisters,start_add,numbers);
19
20 if (auto *reply = My_client->sendReadRequest(ReadUnit, Server_ID)) //1是Server_ID
21 {
22 if (!reply->isFinished())
23 {
24 QObject::connect(reply, &QModbusReply::finished,this,&My_modbus_tcp::ReadReady_InputRegisters);
25
26 }
27 else
28 {
29 delete reply;
30 }
31
32 }
33
34 }
1 /********************************************
2 * 函式名稱:ReadReady_HoldingRegisters()
3 * 功能:槽函式,傳送請求成功後,接收資料將其儲存在Hold_Bufer[]陣列中
4 * 工作方式:
5 * 引數:無引數
6 * 返回值:沒有返回值
7 * 備註:
8 * 修改記錄
9 *********************************************/
10 void My_modbus_tcp::ReadReady_HoldingRegisters()
11 {
12 QModbusReply *reply = qobject_cast<QModbusReply *>(sender());
13 if (!reply){
14 return ;
15 }
16 if (reply->error() == QModbusDevice::NoError)
17 {
18 const QModbusDataUnit unit = reply->result();
19
20 for(uint16_t i=0; i< unit.valueCount(); )
21 {
22 uint16_t res=unit.value(i);
23 Input_Bufer[i] = static_cast<uint8_t>(res);
24 i++;
25 }
26
27 }
28 else
29 {
30 }
31
32 reply->deleteLater(); // delete the reply
33 emit my_readH_finished(); //自定義的訊號
34 }
輸入暫存器類似,換掉配置單元的資料即可。
(3)給線圈寫入資料
1 /********************************************
2 * 函式名稱: Write_modbus_tcp_Coils(QString str1,int star_add,int number)
3 * 功 能: 將想要修改的資料寫入到modbus裝置某個(某些)地址的Coils中。
4 * 工作方式:
5 * 參 數:
6 * 引數1:要寫入的資料(例:1 0 1 0 1 0) QString 型別
7 * 引數2:寫入資料的起始地址 int 型別
8 * 引數3:寫入資料的個數 quint16
9 * 返回值:沒有返回值
10 * 備註:一次性可以寫入單個或者多個資料,取決於該函式執行時引數。
11 * 修改記錄
12 *********************************************/
13 bool My_modbus_tcp::Write_modbus_tcp_Coils(QString str1,int star_add,int number)
14 {
15 quint16 number1 = static_cast<quint16>(number); //C++中的資料型別轉換
16 QModbusDataUnit writeUnit(QModbusDataUnit::Coils,star_add,number1);
17
18 for (uint i1 = 0; i1 < writeUnit.valueCount(); i1++) {
19 int j1 = 2*i1;
20 QString stt = str1.mid (j1,1);
21 bool ok;
22 quint16 hex1 =stt.toInt(&ok,16);//將textedit中讀取到的資料轉換為16進位制傳送
23 writeUnit.setValue(i1,hex1);//設定傳送資料
24 }
25
26 if (auto *reply = My_client->sendWriteRequest(writeUnit, 1)) {// ui->spinBox_SerAddress->value()是server address sendWriteRequest是向伺服器寫資料
27 if (!reply->isFinished()) { //reply Returns true when the reply has finished or was aborted.
28 connect(reply, &QModbusReply::finished, this, [this, reply]() {
29 if (reply->error() == QModbusDevice::ProtocolError) {
30 qDebug() << (tr("Write response error: %1 (Mobus exception: 0x%2)")
31 .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16),
32 5000);
33 } else if (reply->error() != QModbusDevice::NoError) {
34 qDebug()<< (tr("Write response error: %1 (code: 0x%2)").
35 arg(reply->errorString()).arg(reply->error(), -1, 16), 5000);
36 }
37 reply->deleteLater();
38 });
39 } else {
40 // broadcast replies return immediately
41 reply->deleteLater();
42 }
43 } else {
44 qDebug() << (tr("Write error: ") + My_client->errorString(), 5000);
45 }
46
47 }
!!!寫資料時必須轉化位16進位制寫入
(4)給保持暫存器寫資料
1 /********************************************
2 * 函式名稱: Write_modbus_tcp_HoldingRegisters(QString str1,int star_add,int number)
3 * 功 能: 將想要修改的資料寫入到modbus裝置某個(某些)地址的HoldingRegisters中。
4 * 工作方式:
5 * 參 數:
6 * 引數1:要寫入的資料(例:FF A0 00等) QString 型別
7 * 引數2:寫入資料的起始地址 int 型別
8 * 引數3:寫入資料的個數 quint16
9 * 返回值:沒有返回值
10 * 備註:一次性可以寫入單個或者多個資料,取決於該函式執行時引數。
11 * 修改記錄
12 *********************************************/
13 bool My_modbus_tcp::Write_modbus_tcp_HoldingRegisters(QString str1,int star_add,int number)
14 {
15 qDebug()<< "準備寫holding資料::";
16 QByteArray str2 = QByteArray::fromHex (str1.toLatin1().data());//按十六進位制編碼接入文字
17 QString str3 = str2.toHex().data();//以十六進位制顯示
18
19 quint16 number1 = static_cast<quint16>(number);
20 QModbusDataUnit writeUnit(QModbusDataUnit::HoldingRegisters,star_add,number1);
21 int j1 = 0;
22 for (uint i1 = 0; i1 < writeUnit.valueCount(); i1++) {
23
24 if(i1 == 0){
25 j1 = static_cast<int>(2*i1);
26 }
27 else {
28 j1 = j1+3;
29 }
30 QString stt = str1.mid (j1,2);
31 bool ok;
32 quint16 hex1 =static_cast<quint16>(stt.toInt(&ok,16));//將textedit中讀取到的資料轉換為16進位制傳送
33 writeUnit.setValue(static_cast<int>(i1),hex1);//設定傳送資料
34 }
35
36 if (auto *reply = My_client->sendWriteRequest(writeUnit, 1)) {// ui->spinBox_SerAddress->value()是server address sendWriteRequest是向伺服器寫資料
37 if (!reply->isFinished()) { //reply Returns true when the reply has finished or was aborted.
38 connect(reply, &QModbusReply::finished, this, [this, reply]() {
39 if (reply->error() == QModbusDevice::ProtocolError) {
40 qDebug() << (tr("Write response error: %1 (Mobus exception: 0x%2)")
41 .arg(reply->errorString()).arg(reply->rawResult().exceptionCode(), -1, 16),
42 5000);
43 } else if (reply->error() != QModbusDevice::NoError) {
44 qDebug()<< (tr("Write response error: %1 (code: 0x%2)").
45 arg(reply->errorString()).arg(reply->error(), -1, 16), 5000);
46 }
47 reply->deleteLater();
48 });
49 } else {
50 // broadcast replies return immediately
51 reply->deleteLater();
52 }
53 } else {
54 qDebug() << (tr("Write error: ") + My_client->errorString(), 5000);
55 }
56 }