Qt ModbusTCP通訊

一杯清酒邀明月發表於2024-03-09

前言
  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 }

相關文章