Modbus TCP 入门学习[通俗易懂]

2022-11-17 10:53:23 浏览数 (2)

大家好,又见面了,我是你们的朋友全栈君。

记录下我入门学习的过程,供日后回看,文字部分多是转载他人blog,有注明来源地址;实验部分为真实测试结果。

1. ModBus通讯协议简介

  (摘抄:来自网络)Modbus协议是一种已广泛应用于当今工业控制领域的通用通讯协议。通过此协议,控制器相互之间、或控制器经由网络(如以太网)可以和其它设备之间进行通信。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控设备方所使用的协议称为Modbus Master,从设备方使用的协议称为Modbus Slave。典型的主设备包括工控机和工业控制器等;典型的从设备如PLC可编程控制器等。Modbus通讯物理接口可以选用串口(包括RS232和RS485),也可以选择以太网口。其通信遵循以下的过程:

  ● 主设备向从设备发送请求

  ● 从设备分析并处理主设备的请求,然后向主设备发送结果

  ● 如果出现任何差错,从设备将返回一个异常功能码

2. Modbus TCP 的数据帧

MBAP 头和PDU 构成, MBAP= Modbus Application Protocol Header(Modbus应用协议) 头部

PDU = Protocol Data Unit (数据单元)

ADU:Application Data Unit

上面截图来源:http://www.modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf

头部MBAP:

例如:

3:功能码

来源:https://blog.csdn.net/iknow_nothing/article/details/84292914

modbus的操作对象有四种:线圈、离散输入、输入寄存器、保持寄存器

线圈:PLC的输出位,开关量,在MODBUS中可读可写 离散量:PLC的输入位,开关量,在MODBUS中只读 输入寄存器:PLC中只能从模拟量输入端改变的寄存器,在MODBUS中只读 保持寄存器:PLC中用于输出模拟量信号的寄存器,在MODBUS中可读可写 根据对象的不同,modbus的功能码有:

0x01:读线圈 0x02:读离散量输入 0x03:读保持寄存器

0x04:读输入寄存器

0x05:写单个线圈 0x06:写单个保持寄存器 0x10:写多个保持寄存器 0x0F:写多个线圈

4:实验

准备一个C# Socket的收发模型封装类,下载一个Modbus Slave工具

序列号:5455415451475662

0x01:读线圈 在从站中读1~2000个连续线圈状态,ON=1,OFF=0

下面截图来源:初识Modbus TCP————-C#编写Modbus TCP客户端程序(一)_thebestleo的专栏-CSDN博客_c# modbus tcp

请求:MBAP 功能码 起始地址H 起始地址L 数量H 数量L 响应:MBAP 功能码 数据长度 数据(一个地址的数据为1位) :在从站0x01中,读取开始地址为0x0002的线圈数据,读16位

请求:00 01 00 00 00 06 01 (Slave ID)01(功能码) 00 02 (起始地址)00 10(长度16转化16进制为10)

代码语言:javascript复制
byte[] data = new byte[] { 0x00,0x01,0x00,0x00,0x00,0x06, 0x01, 0x01, 0x00, 0x02, 0x00, 0x10 };

Jetbrains全家桶1年46,售后保障稳定

验证:0x55 转化为二进制位: 01010101

0x15转化为二进制位: 00010101

把上面2个二进制按一定的方向组合起来就和上图配置的 开关量保持一致了。从C# 程序上来说:

代码语言:javascript复制
byte[] data = new byte[] { 0x55, 0x15 };

data[0]是地位,data[1]是高位,深入到每个byte里面的二进制,高位在前,低位在后。ModBus使用Big-Endian表示地址和数据项。

0x02:读离散量输入

过程和0x01一致,略

0x03:读保持寄存器

从远程设备中读保持寄存器连续块的内容

  • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)
  • 响应:MBAP 功能码 数据长度 寄存器数据(长度:9 寄存器数量×2)
代码语言:javascript复制
 byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x4f, 0x00, 0x03 };

响应:

见下面0x04,过程一致;

0x04:读输入寄存器

从一个远程设备中读1~2000个连续输入寄存器

  • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)
  • 响应:MBAP 功能码 数据长度 寄存器数据 (长度:9 寄存器数量×2)
代码语言:javascript复制
byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x04, 0x00, 0x4f, 0x00, 0x05 };

得到响应如下图所示:

注意:16位的寄存器存储的最大带符号2进制数是32767

0x05:写单个线圈 将从站中的一个输出写成ON或OFF,0xFF00请求输出为ON,0x000请求输出为OFF

80的16进制为0x50

代码语言:javascript复制
 byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05, 0x00, 0x50, 0x00, 0x00 };

结果为:

0x06:写单个保持寄存器

  • 请求:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节)
  • 响应:MBAP 功能码 寄存器地址H 寄存器地址L 寄存器值H 寄存器值L(共12字节)
代码语言:javascript复制
byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06,0x01, 0x06,  0x00, 0x4f, 0x00, 0xa8 };

0x10:写多个保持寄存器

这里理解:寄存器数量:就是需要多少个寄存器去存数据,比如:1个、2个等;

字节数:就是寄存器值占用的bit位数/8,一个字节=8个位;

比如:float、32位int,都占用4个字节,寄存器使用2个来存储, 关于用2个寄存器存4个字节的解析,可以参考后文的介绍。

  • 请求:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L 字节长度 寄存器值(13 寄存器数量×2)
  • 响应:MBAP 功能码 起始地址H 起始地址L 寄存器数量H 寄存器数量L(共12字节)

例如:从0x02开始,写入0x03个寄存器,字节数为:0x06, 值分别为:00 0A,01 02,00 A8

代码语言:javascript复制
byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x01, 0x010, 0x00, 0x02, 0x00, 0x03, 0x06,0x00,0x0A,0x01,0x02,0x00,0xa8 };

更多举例:

0x0F:写多个线圈

  • 请求:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L 字节长度 输出值H 输出值L
  • 响应:MBAP 功能码 起始地址H 起始地址L 输出数量H 输出数量L

上图的字节数N = 输出数量/8 或不足整除 1

这里说明下为何协议里还要有一个字节数的存在,很好理解:假如输出值都是一致的,起始地址为0,输出16位长度和输出15个长度的请求如何区分呢,需要告诉PLC 改变的线圈的个数就由字节数来表示。

例如:从地址0开始写入11个线圈,值为0xcd: 11001101

代码语言:javascript复制
byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x01, 0x0f, 0x00, 0x00,0x00,0x0b,0x02, 0xcd, 0xcd };

5:长连接心跳

在实际测试过程中发现大概1到2分钟之间,再次发送数据包时提示连接已经断开。如果频繁的连接则一直会保持连接!

所以这里加一个定时器处理:

代码语言:javascript复制
  private void timer1_Tick(object sender, EventArgs e)
        {
            byte[] data = new byte[] { 0x00, 0x0f, 0x00, 0x00, 0x00, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01 };
            client.SendAsync(data);
        }

不知道这个模拟Modbus Slave的缘故还是内部有一些超时的机制在内面,通过测试发现有这个现象,还未拿到真正的PLC硬件测试,暂时做一个记录。下面贴图为一个参考: 可能说的是TCP Keep Alive 机制

6:Modbus 错误码

来源:modbus通信协议中的功能码、异常功能码和错误码_欧阳鑫-CSDN博客_modbus返回故障代码

这里贴过来,汇总整理,方便学习之用:

功能码表

数据类型

功能描述

功能码

功能码(十六进制)

异常功能码

比特访问

物理离散量输入

读输入离散量

02

0x02

0x82

内部比特或者物理线圈

读线圈

01

0x01

0x81

写单个线圈

05

0x05

0x85

写多个线圈

15

0x0F

0x8F

16比特访问

输入存储器

读输入寄存器

04

0x04

0x84

内部存储器或物理输出存储器(保持寄存器)

读多个寄存器

03

0x03

0x83

写单个寄存器

06

0x06

0x86

写多个寄存器

16

0x10

0x90

读/写多个寄存器

23

0x17

0x97

屏蔽写寄存器

22

0x16

0x96

文件记录访问

读文件记录

20

0x14

写文件记录

21

0x15

其中物理离散量输入和输入寄存器只能有I/O系统提供的数据类型,即只能是由I/O系统改变离散量输入和输入寄存器的数值,而上位机程序不能改变的数据类型,在数据读写上表现为只读,而内部比特或者物理线圈和内部寄存器或物理输出寄存器(保持寄存器)则是上位机应用程序可以改变的数据类型,在数据读写上表现为可读可写。

错误代码表

代码

名称

含义

01

非法功能

对于服务器(或从站)来说,询问中接收到的功能码是不可允许的操作,可能是因为功能码仅适用于新设备而被选单元中不可实现同时,还指出服务器(或从站)在错误状态中处理这种请求,例如:它是未配置的,且要求返回寄存器值。

02

非法数据地址

对于服务器(或从站)来说,询问中接收的数据地址是不可允许的地址,特别是参考号和传输长度的组合是无效的。对于带有100个寄存器的控制器来说,偏移量96和长度4的请求会成功,而偏移量96和长度5的请求将产生异常码02。

03

非法数据值

对于服务器(或从站)来说,询问中包括的值是不可允许的值。该值指示了组合请求剩余结构中的故障。例如:隐含长度是不正确的。modbus协议不知道任何特殊寄存器的任何特殊值的重要意义,寄存器中被提交存储的数据项有一个应用程序期望之外的值。

04

从站设备故障

当服务器(或从站)正在设法执行请求的操作时,产生不可重新获得的差错。

05

确认

与编程命令一起使用,服务器(或从站)已经接受请求,并且正在处理这个请求,但是需要长持续时间进行这些操作,返回这个响应防止在客户机(或主站)中发生超时错误,客户机(或主机)可以继续发送轮询程序完成报文来确认是否完成处理。

07

从属设备忙

与编程命令一起使用,服务器(或从站)正在处理长持续时间的程序命令,当服务器(或从站)空闲时,客户机(或主站)应该稍后重新传输报文。

08

存储奇偶性差错

与功能码20和21以及参考类型6一起使用,指示扩展文件区不能通过一致性校验。服务器(或从站)设备读取记录文件,但在存储器中发现一个奇偶校验错误。客户机(或主机)可重新发送请求,但可以在服务器(或从站)设备上要求服务。

0A

不可用网关路径

与网关一起使用,指示网关不能为处理请求分配输入端口值输出端口的内部通信路径,通常意味着网关是错误配置的或过载的。

0B

网关目标设备响应失败

与网关一起使用,指示没有从目标设备中获得响应,通常意味着设备未在网络中。

7:如何读取float型数据

通过上面的测试可以看到寄存器读到的是short型数据,float占两个寄存器,需要4个字节存储,p1、p2对应两个寄存器的值。

代码语言:javascript复制
 float GetFloat(ushort P1, ushort P2)
        {
            int intSign, intSignRest, intExponent, intExponentRest;
            float faResult, faDigit;
            intSign = P1 / 32768;
            intSignRest = P1 % 32768;
            intExponent = intSignRest / 128;
            intExponentRest = intSignRest % 128;
            faDigit = (float)(intExponentRest * 65536   P2) / 8388608;
            faResult = (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (faDigit   1);
            return faResult;
        }

 float GetFloat(short p1, short p2)
        {
            byte[] bytes = new byte[4];
            bytes[0] = (byte)(p2 & 0xFF);//低位
            bytes[1] = (byte)(p2 >> 8);//高位
            bytes[2] = (byte)(p1 & 0xFF);
            bytes[3] = (byte)(p1 >> 8);
            float value = BitConverter.ToSingle(bytes, 0);
            return value;
        }

本文完!2019年4月12日15:08:25

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/226916.html原文链接:https://javaforall.cn

0 人点赞