Modbus TCP 入门学习

记录下我入门学习的过程,供日后回看,文字部分多是转载他人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 (数据单元)

Modbus TCP 入门学习_第1张图片

ADU:Application Data Unit

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

头部MBAP:

Modbus TCP 入门学习_第2张图片

例如:Modbus TCP 入门学习_第3张图片

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

Modbus TCP 入门学习_第4张图片

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

下面截图来源:https://blog.csdn.net/thebestleo/article/details/52269999#commentsedit

Modbus TCP 入门学习_第5张图片

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

Modbus TCP 入门学习_第6张图片Modbus TCP 入门学习_第7张图片

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

byte[] data = new byte[] { 0x00,0x01,0x00,0x00,0x00,0x06, 0x01, 0x01, 0x00, 0x02, 0x00, 0x10 };

 Modbus TCP 入门学习_第8张图片

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

           0x15转化为二进制位:  00010101

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

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)
 byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x4f, 0x00, 0x03 };

Modbus TCP 入门学习_第9张图片

响应:

见下面0x04,过程一致;

0x04:读输入寄存器

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

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

得到响应如下图所示:

Modbus TCP 入门学习_第10张图片

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

Modbus TCP 入门学习_第11张图片

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

Modbus TCP 入门学习_第12张图片

80的16进制为0x50 

 byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x05, 0x00, 0x50, 0x00, 0x00 };

结果为:

Modbus TCP 入门学习_第13张图片

 

0x06:写单个保持寄存器

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

 Modbus TCP 入门学习_第14张图片

 0x10:写多个保持寄存器

Modbus TCP 入门学习_第15张图片

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

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

byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x0D, 0x01, 0x010, 0x00, 0x02, 0x00, 0x03, 0x06,0x00,0x0A,0x01,0x02,0x00,0xa8 };

Modbus TCP 入门学习_第16张图片

 0x0F:写多个线圈

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

Modbus TCP 入门学习_第17张图片

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

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

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

byte[] data = new byte[] { 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x01, 0x0f, 0x00, 0x00,0x00,0x0b,0x02, 0xcd, 0xcd };

Modbus TCP 入门学习_第18张图片

 5:长连接心跳

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

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

  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 机制

Modbus TCP 入门学习_第19张图片

6:Modbus 错误码

来源:https://blog.csdn.net/ouyangxin95/article/details/78174071

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

功能码表

  数据类型 功能描述 功能码 功能码(十六进制) 异常功能码
比特访问 物理离散量输入 读输入离散量 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占两个寄存器,需要两个字节存储,p1、p2对应两个寄存器的值。

https://www.cnblogs.com/derekhan/p/10041679.html

public static 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;
        }

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

你可能感兴趣的:(Modbus,TCP)