一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

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

 

posted on 2024-03-09 15:32  一杯清酒邀明月  阅读(2008)  评论(0编辑  收藏  举报