大家好,又见面了,我是你们的朋友全栈君。
记录下我入门学习的过程,供日后回看,文字部分多是转载他人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)
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)
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字节)
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