Modbus tcp 格式说明 通讯机制 附C#测试工具用于学习,测试

 

前言:


之前的博客介绍了如何用C#来读写modbus tcp服务器的数据,文章:http://www.cnblogs.com/dathlin/p/7885368.html

当然也有如何创建一个服务器文章:http://www.cnblogs.com/dathlin/p/7782315.html

但是上面的两篇文章是已经封装好的API,只要调用就可以实现功能了,对于想了解modbus tcp的原理的人可能就不适合了,最近有不少网友的想要了解这个协议,所以在这里再写一篇介绍Modbus tcp的文章,不过这篇文章是简易版本的,未来我再研究深入的话,再开一篇高级版,在简易版中,就略去了成功标志位及其他数据标志,这些到等到后面再说。

先分享一下,我自己学习的地址来源:http://blog.csdn.net/thebestleo/article/details/52269999  声明:本文并非转载,并非照搬原文章,是在我参照原博客的基础上,理解了基本的modbus通讯,并结合自己的理解,重新写一篇更好入门的文章,此处贴出原作者的帖子以示尊重知识产权,原文章有些地方有一点错误,而且早就停止更新了,也没有提供方便的测试工具,官方的modbus 测试工具是试验版本的,需要购买序列号才可以,所以此处提供我自己的测试工具,地址如下,下面的介绍的例子都是基于这个工具来实现的。

https://github.com/dathlin/ModBusTcpTools/raw/master/download/download.zip

关于该测试工具也是开放源代码的,如果想要查看源代码:https://github.com/dathlin/ModBusTcpTools

技术支持QQ群:592132877

准备条件:


在上面的测试工具下载之前,需要一些额外的知识补充,此处不管你是学习什么语言的,对于socket通信层来说,其实是一样的,下面的讲解的内容是直接基于底层的,无关语法的操作。

但是需要你对字节概念非常清晰,一般都是byte数组,一个byte有8个位,这个也要非常的清晰,如果连byte是什么都搞不清楚,那么对本文下面的内容理解会非常的吃力,那么还是建议你再看看计算机原理这些书,对于socket通信,每种语言都有不同的写法,但是所有的语言都有两个共同点,都能实现把数据发送到socket和从socket接收数据,至于这个如果去做,就参照你自己需要使用的语言了,此处不做这方面的说明了。

关于十六进制文本,在本文的下面的内容上,所有的byte字节数组都表示成十六进制形式,比如 FF 10 代表2个字节,一个是255,另一个是16。

1
2
3
byte[] temp = new byte[2];
temp[0] = 0xFF;
temp[1] = 0x10;

如果将上述的temp看作是读取到的线圈的数据,那么转换规则如下:

先将上述数据转化成二进制 : 1111 1111    (第一个byte,我们从高位写到地位)             0001 0000   (第二个byte,我们从高位写到地位)

对应的线圈就是,线圈7-线圈0,,,,第二个byte对应的线圈是,线圈15-线圈8     这里一定要好好理解,从byte上来说,temp[0]是地位,temp[1]是高位,深入到每个byte里面的二进制,高位在前,低位在后。

在C#里等同于下面的代码,和C语言,java也是非常的相近,还算比较好理解。

如果我说,发送 00 00 00 00 00 06 FF 01 00 00 00 01 到socket上去,那么也就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte[] temp = new temp[12];
temp[0] = 0x00;
temp[1] = 0x00;
temp[2] = 0x00;
temp[3] = 0x00;
temp[4] = 0x00;
temp[5] = 0x06;
temp[6] = 0xFF;
temp[7] = 0x01;
temp[8] = 0x00;
temp[9] = 0x00;
temp[10] = 0x00;
temp[11] = 0x01;
 
socket.Send(temp);

先不要管上面的数据是什么含义,知道上面的代码是啥含义就行了。接下来就是下载上面的测试工具,开始真正的学习modbus tcp协议了!

测试工具初始化


先运行Server.exe文件,端口里输入502,然后点击启动服务即可,如下:

然后运行Client.exe程序,在Ip地址里输入127.0.0.1,端口里输入502,点击配置即可,我们看到,如果你的服务器程序运行在了别的电脑上,甚至是云端,只要客户端的ip修改成服务器的ip,端口号对应上,就可以访问到服务器的数据了。

特殊测试不用去管,和我们现在学习的东西不一致。

功能码详细解释


对于modbus来说,涉及的功能码也就是0x01,0x02,0x03,0x05,0x06,0x0F,0x10了,其实分类来说,就只有两种,线圈和寄存器,也就是位读写和字读写,首先需要清楚的是功能码不一样,对应数据的解析规则也不一样,下面就针对不同的情况来说。

首先说明的是,modbus协议呢,最终目的还是为了实现数据交互,既然是数据交互,那就是包含了数据读和写,我们把我们的想法转化成一串数据,发送给设备(或者叫服务器),它返回一串数据,根据规则解析出来,这样就得到了我们真正想要的数据。下面就来第一个想法实现吧。

另外,在modbus服务器端,数据是使用地址的方式来公开的,这很好理解,服务器端保存了很多数据,你想要访问某个数据肯定需要指定唯一的身份标识,从连续的地址来区分数据是最常用的做法,不仅好理解,还便于扩展,比如你还可以读取连续地址的数据块。如果采用字符串名字来标识数据,就没有这个特点。

对于位操作来说(各种线圈和离散量),一个地址代表了一个bool变量,即 0 和 1,要么通要么断,就好比一些普通的开关。

对于寄存器来说,一个地址代表了2个byte,共有65536种方式,可以满足大多数日常使用了,比如我们读取地址0的寄存器,返回 00 00 及代表寄存器0数据为0,如果返回 01 00 ,那么代表寄存器0数据为 256

功能码0x01:


我不直接上一串数据,这样看着也累,我们从例子出发,现在我们需要读取线圈(离散量)操作,我想读取地址0的线圈是否是通还是断的。我们有了这个功能需求后,就可以根据需求来写出特殊的指令了。根据协议指定,需要填写长度为12的byte数组

byte[0]    byte[1]    byte[2]   byte[3]   byte[4]   byte[5]   byte[6]   byte[7]   byte[8]   byte[9]   byte[10]   byte[11]

byte[0]    byte[1]             : 消息号---------随便指定,服务器返回的数据的前两个字和这个一样

byte[2]   byte[3]              :modbus标识,强制为0即可

byte[4]   byte[5]               :指示排在byte[5]后面所有字节的个数,也就是总长度-6

byte[6]:                       站号,随便指定,00  -- FF 都可以

byte[7] :                     功能码,这里就需要填入我们的真正的想法了

byte[8]  byte[9]               :起始地址,比如我们想读取地址0的数据,就填 00 00 ,如果我们想读取地址1000的数据,怎么办,填入 03 E8 ,也就是将1000转化十六进制填进去。

byte[10]  byte[11]              :指定想读取的数据长度,比如我们就想读取地址0的一个数据,这里就写 00 01,如果我们想读取地址0-999共计一个数据的长度,就写 03 E8。和起始地址是一样的。

有了上面的格式之后,接下来我们就按照格式来填写数据吧,我们需要读取地址0的数据,那么指定如下

00 00 00 00 00 06 FF 01 00 00 00 01

消息号设为0,站号FF,功能码01,地址01,长度01:将上面的指令在客户端程序里进行输入,点击发送,这样就在下面的响应框里接收到服务器反馈的数据,我们最终需要的信息就在反馈的数据里了。

前面是接收到数据的时间,自动忽略,那么返回的数据就是 00 00 00 00 00 04 FF 01 01 00 共计10个字节的数据,ok,这玩意到底是什么意思呢,我们来分别解析下:

byte[0]  byte[1] :             消息号,我们之前写发送指令的时候,是多少,这里就是多少。

byte[2]  byte[3]:           必须都为0,代表这是modbus 通信

byte[4]  byte[5]:           指示byte[5]后面的所有字节数,你数数看是不是4个?所以这里是00 04,如果后面共有100个,那么这里就是 00 64

byte[6]:                        站号,之前我们写了FF,那么这里也就是FF

byte[7]:                        功能码,我们之前写了01的功能码,这里也是01,和我们发送的指令是一致的

byte[8]:                        指示byte[8]后面跟随的字节数量,因为跟在byte[8]后面的就是真实的数据,我们最终想要的结果就在byte[8]后面

byte[9]:                        真实的数据,哈哈,这肯定就是我们真正想要的东西了,我们知道一个byte有8位,但是我们只读取了一个位数据,所有这里的有效值只是byte[9]的最低位,二进制为 0000 0000 我们看到最低位为0,所以最终我们读取的地址0的线圈为断。

假设我们读取地址10,开始的共10个线圈呢,那么会返回什么?所以我们发送 00 00 00 00 00 06 FF 01 00 0A 00 0A

我们接收到了:00 00 00 00 00 05 FF 01 02 79 01      前面的8个字节的信息参照上面的分析,是一致的,我们就针对后面三个字节着重分析。我们读取了10个位,那么一个字节可以表示8个位,那么我们的结果至少需要2个byte才能表示完,所以最终的数据肯定是2个字节,那么02就是后面的字节数量,也就是真实的数据长度。

要想从 79 01 数据中分析出我们真实想要的数据,还需要经过最后一次数据转换。先转为二进制:

0111 1001           0000 0001

第二步:按每八位进行分割,上述其实已经分割好了,中间空格多的是分割,以字为单位,将二进制顺序颠倒:

1001 1110         1000000

第三步:最终数据就是    线圈10-线圈19的通断情况是:通,断,断,通,通,通,通,断,通,断     再后面的0都是无效的

至此我们获取到了我们最终的数据!因为此处服务器都是0,所以所有的线圈都是断,等会可以结合05功能码写线圈进行联合测试。

功能码0x02:


这个功能码和上面的一致,在本服务器里不支持这个功能码。发送和解析规则和上面的一致,不再赘述。

功能码0x05:


我们先讲解05功能码,这个功能码是实现数据写入,它能实现什么功能呢,我们可以利用这个功能码来指定某个线圈通或断,具体怎么操作呢,有了之前01功能码的经验,下面的代码看起来就顺利多了。

比如我要指定地址0的寄存器为通: 00 00 00 00 00 06 FF 05 00 00 FF 00    前面的含义都是一致的,我们就分析 05 00 00 FF 00

05 是功能码, 00 00 是我们指定的地址,如果我们想写地址1000为通,那么就为 03 E8,至于FF 00是规定的数据,如果你想地址线圈通,就填这个值,想指定线圈为断,就填 00 00 ,其他任何的值都对结果无效。

然后我们看看写入的操作服务器返回了什么 ?  我们看到也是  00 00 00 00 00 06 FF 05 00 00 FF 00   因为在你写入的操作中,是不带读取数据的,所以服务器会直接复制一遍你的指令并返回。

下面再举例一些方便理解(我们只需要指定地址及是否通断的情况即可):

写入地址100为通: 00 00 00 00 00 06 FF 05 00 64 FF 00

写入地址1000为断:00 00 00 00 00 06 FF 05 03 E8 00 00

功能码0x0F:


我们已经实现了0x05来单个的线圈写入,我们可以指定线圈100为通,其实就两个信息需要指定,线圈地址是什么,通还是端,然后我们就可以自然而然的写出指令码了,但是现在我们需要实现一个功能时,将地址0-999共计1000个线圈全部为off,这怎么搞?

按照我们之前的经验,可以发送一千次的0x05功能码的指令来实现,大不了写1000次么。。。。。(写到第100次的时候估计已经吐血了)

所以我们就继续研究有没有其他的功能码来实现,突然发现0x0F这一个神奇的功能码,这个功能码是什么意思呢,就是为了批量写入而存在的,就比如上面的例子0-999都为off,那么指令是什么呢。

00 00 00 00 00 84 FF 0F 00 00 03 E8 7D ...(后面跟125个byte,都是00)

上面的指令就实现了我们的需求,现在来详细解释下,它怎么就实现了我们的需求。分析之前,我们发现不同的功能码,的前8个字节的规律是一模一样了,都是标识号+modbus号+长度+站号,后面基本是跟地址和长度,或是直接是地址和数据。

00 00         消息标识号,随便写什么,反正你写什么数据,服务器就复制一遍而已。

00 00         modbus标志号,都是00 就对了。

00 84         我们先转化为十进制,0x0084转化十进制就是132,也就是说,00 84(不包含00 84)后面跟了132个字节

FF              站号,其实也是随便写,反正服务器返回一样的

00 00         起始地址,此处就是0,如果起始地址为100,那么就写00 64,如果起始地址为1000,那么就写03 E8

03 E8         我们需要写的数据长度,因为我们需要写1000个线圈,就是03 E8,如果我们写999个线圈,那么就是03 E7。

7D             这个字节代表后面跟随的真实写入的数据的长度,为125个字节。

125个字节  真实的数据,我们写1000个位,那么一个字节为8位,那么刚好125个字节可以塞完数据,那么问题来了,如果我们想实现000-998共计999个地址都是off。那怎么搞。

那么指令为 00 00 00 00 00 84 FF 0F 00 00 03 E7 7D ...(后面125个byte,都是00)咦,怎么还是125个,原来无论写多少个,比如x个,如果是8的倍数,刚好x/8个byte,如果除不尽怎么办,就是x/8+1个字节,这样才能装满我们需要写的数据。

既然后面都是125个字节,那么写1000个还是999个,那么区分的关键就在于长度,03 E8还是03 E7。


大致的数据在上面已经说明了,具体怎么写数据看下面,比如我们写入地址10-地址19共计10个长度的线圈,要求的结果分别是,On,Off,Off,On,On,On,On,Off,On,Off,也就是 通,断,断,通,通,通,通,断,通,断,接下来转换0和1,如下:

1001111010

接下来就是关键了,怎么转化成真实的byte,这样我们就可以最终写出来指令了。

第一步:以8个8个为单位进行切割,结果为 10011110    10

第二步:第一步的字单位,每个单位前后顺序颠倒,不然不足8位,前面补零,结果为   0111 1001          0000 0001

第三步:这下可以写成真实的数据了,79 01

那么最接下来我们就可以写最终的指令了,实现写入地址10-19为:通,断,断,通,通,通,通,断,通,断  也即 00 00 00 00 00 09 FF 0F 00 0A 00 0A 02 79 01         (地址10-19的线圈分别为 通,断,断,通,通,通,通,断,通,断)

注意上述第二步为什么要顺序颠倒,那是因为在计算机的单个byte存储中,高位在前,地位在后,而对于多个连续的byte来说,地位在前,高位在后,所以需要颠倒,如果还是不明白,就先死记,终有一天会恍然大悟。

现在应该能实现任何的连续线圈的写入了吧。


写入之后,看看了服务器返回了什么:

00 00 00 00 00 06 FF 0F 00 0A 00 0A    :现在再来看这个数据就很简单了,就是返回了我们写入数据的前12分字节,然后把00 09长度更改为实际的长度 00 06,因为是写入操作,所以返回的数据没什么意义。

功能码0x03:


该功能码实现寄存器的数据读取,我们需要知道的是,一个寄存器占2个byte,而且是高位在前,地位在后,那么如果寄存器0的数据为1000,那么我们读取到的数据就是03 E8,这是我们最终想要的东西,03功能码和01功能码很接近,就是功能码替换一下,返回的数据解析不一样而已,比如我们需要读取地址0的寄存器数据:

00 00 00 00 00 06 FF 03 00 00 00 01               是不是很熟悉?,当你看到这个的时候,脑力里马上就是功能码03,读取寄存器,地址0,长度1        返回如下:

00 00 00 00 00 05 FF 03 02 03 E8                    主要就是看功能码后面的数据了,我们想要的真实数据肯定藏在后面,也就是 02 03 E8,不是说一个寄存器返回2个字节嘛,怎么就变成3个了?事实上第一个字节不是代表数据,而是代表后面的字节长度是2个字节,那么03 E8就是真实的数据了,代表了寄存器0存储了1000这个数据。

未完待续...

modbus tcp 入门详解的更多相关文章

  1. Redis快速入门详解

    Redis入门详解 Redis简介 Redis安装 Redis配置 Redis数据类型 Redis功能 持久化 主从复制 事务支持 发布订阅 管道 虚拟内存 Redis性能 Redis部署 Redis ...

  2. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  3. SQL注入攻防入门详解

    =============安全性篇目录============== 本文转载 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱,事实上是没机 ...

  4. SQL注入攻防入门详解(2)

    SQL注入攻防入门详解 =============安全性篇目录============== 毕业开始从事winfrm到今年转到 web ,在码农届已经足足混了快接近3年了,但是对安全方面的知识依旧薄弱 ...

  5. Quartz 入门详解

    Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用.Quartz可以用来创建简单或为运行十个,百个, ...

  6. TCP/IP详解学习笔记

    TCP/IP详解学习笔记(1)-基本概念 TCP/IP详解学习笔记(2)-数据链路层 TCP/IP详解学习笔记(3)-IP协议,ARP协议,RARP协议 TCP/IP详解学习笔记(4)-ICMP协议, ...

  7. TCP/IP详解 (转)

    TCP/IP详解学习笔记(1)-基本概念 为什么会有TCP/IP协议 在世界上各地,各种各样的电脑运行着各自不同的操作系统为大家服务,这些电脑在表达同一种信息的时候所使用的方法是千差万别.就好像圣经中 ...

  8. 『TCP/IP详解——卷一:协议』读书笔记——10

    2013-08-22 22:57:17 3.8 ifconfig命令 这个命令在Linux系统下可以通过下面的指令阅读说明文档: ifconfig 由于书中作者用的系统比较早的某Unix系统,所以我的 ...

  9. 《TCP/IP详解卷1:协议》第1章 概述-读书笔记

    章节回顾: <TCP/IP详解卷1:协议>第1章 概述-读书笔记 <TCP/IP详解卷1:协议>第2章 链路层-读书笔记 <TCP/IP详解卷1:协议>第3章 IP ...

随机推荐

  1. ss linux终端配置

    最近ss莫名宕机,懒得重新安装了,就安装了一个非gui版本,安装非gui版本还有一个优点就是在远程服务器的时候可以用proxychains进行终端代理,非常友好实用.下面简单的说一下如何进行终端ss ...

  2. HDU - 5157 :Harry and magic string (回文树,求多少对不相交的回文串)

    Sample Input aca aaaa Sample Output 3 15 题意: 多组输入,每次给定字符串S(|S|<1e5),求多少对不相交的回文串. 思路:可以用回文树求出以每个位置 ...

  3. 基于C#利用ffmpeg提取视频帧

    利用ffmepg提取视频帧实际上是利用C#调用ffmepg命令行进行处理对应的视频,然后输出出视频帧 GetPicFromVideo("); static public string Get ...

  4. 如何解决VMware 12 安装Ubuntu 16.04时无网络连接问题

    刚安装玩Ubuntu,打开后上网没有网络连接 ,点击右上角的数据连接,显示已经启动联网,但是用火狐还是无法上网: 解决方法如下: 先查看虚拟机的网络适配器:点击虚拟机左上角的编辑,里面有个网络适配器 ...

  5. EasyUI datagrid 一个可以 直接运行例子一个文件 六

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta ht ...

  6. (18)模型层 -ORM之msql 多表操作(字段的属性)

    数据库表的对应关系 1.一对一   #关联字段写在那张表都可以 PS:只要写OneToOneField就会自动加一个id 2.一对多  #关系确立,关联字段写在多的一方 3.多对多   #多对多的关系 ...

  7. 在Java中,以下关于方法重载和方法重写描述正确的是?

    public class TTTTT extends SuperC{ public String get(){ return null; } } class SuperC{ Object get(){ ...

  8. drone 1.0 新的定时任务界面&&构建任务支持重启

    drone 1.0 的定时任务是一个不错的功能,早期的版本是必须使用cron 表达式的 最近发布的版本支持通过配置就可以了,很方便,只是目前比较简单的,支持小时. 天.周.月.年的模式 环境准备 do ...

  9. Understanding the managed heap

    Understanding the managed heap   Another common problem faced by many Unity developers is the unexpe ...

  10. IntelliJ IDEA使用心得

    前言:我原来一直使用的是Eclipse,但是发现有的教程上使用的是Intellij这个IDE,而且我发现Eclipse在Web编程上特别是页面上的自动补全上确实有些不足,而且Intellij这个软件的 ...