Skip to content

这是一份modbus-tcp的指导性说明文件,如果未来会有代码指导的话,我打算使用c语言作为指导语言

License

Notifications You must be signed in to change notification settings

nyanyaww/modbus-tcp-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 

Repository files navigation

modbus-tcp-guide

这是一份modbus-tcp的指导性说明文件,如果未来会有代码指导的话,我打算使用c语言作为指导语言。

首先是这次的作业要求:

1.tcp/ip的server/client架构去接收与发送信息,收发的信息就是所谓的modbus协议封装的数据。

2.实现modbus协议,注意这个modbus协议非常地特殊,并不是标准modbus协议,实际上它应该是modbus-rtu协议加上modbus-tcp头的协议,或者说是modbus-tcp加上crc校验的协议。而这个作业实际的工作量是需要实现6个功能码的协议,仔细算是请求和应答各1个,实际有12种不同的(只有细微的不同)的协议格式。

3.要有用户交互界面。

4.实现的语言设备不限,但是需要提供2个可执行程序,一个是Server,另一个是Client。

我简短分析一下作业要求,这次作业最重要部分不在于tcp的c/s部分。其原因在于此次的c/s均为非轮询单线程同步阻塞式的,也意味着c/s部分你只需要做一个可以接收请求以及做出应答的功能就ok了。细分server功能,给client发送modbus请求,得到client的modubus应答;client功能,得到server请求,处理请求并发送给server。再仔细思考你会发现,server端只有2个操作,发送请求,接收应答;而client端有4个操作,收到请求,解析请求成命令,处理解析后的命令而后封装应答,发送应答。这意味着实际上我们写的2个程序,只有Server的发送请求是需要我们手动输入,之后的server的1个操作以及client的4个操作我们是不需要手动干预的,是自动化的。 而我认为最重要的部分在于请求和应答的modbus协议该怎么写,我在后文中会慢慢说明。

1. modbus的协议说明

最基本的格式(请求和应答)是下文所述的RTU格式,我们抽取其中最关键的来说一下,其中源地址(device addr)和crc校验(crc check)在后文会解释。

在这个标题部分我只解释最关键的功能码,数据地址以及数据。

+-----------+-------------+---------+----+---------+
|device addr|function code|data addr|data|crc check|
+-----------+-------------+---------+----+---------+

源地址 | 功能码 | 数据地址 | 数据 | crc校验

功能码的请求与应答

  1. 读取线圈function code 0x01

    • 上位机请求

      功能码 起始地址 线圈数量
      0x01 0x0000->0xFFFF(2 char) 0x0001->0x7D00(2 char)
    • 下位机应答

      功能码 字节数 线圈状态
      0x01 N(1 char) (N char)

    ps. N = n % 8 如果N == 0,那么 N = N + 1

    功能码很简单是0x01,数据地址是也不难,没什么好说的。重点是数据的解释,我们请求的时候是请求下位机发送一个线圈的(包括这个线圈)之后的n个线圈的状态。

    而下位机应答的时候,他需要发送字节数以及线圈的状态,其实这一点并不好理解,但是由于这个作业并不涉及太多modbus协议栈所以我不仔细解释。

    下面我们来模拟一次功能码01的执行过程:

    1. 首先是请求(hex):

      01 00 13 00 13

         +--+-----+-----+
         |01|00 13|00 13|
         +--+-----+-----+
    2. 其次是应答(hex):

      01 03 CD 6B 05

         +--+--+--------+
         |01|03|CD 6B 05|
         +--+--+--------+

    请求的时候,首先使用是功能码01,那么显然是使用读取线圈的功能。线圈地址是0x0013也就是对应着十进制的19,说明我们的线圈的起始地址是19号线圈。请求的数据也是0x0013,意味着读取19号寄存器之后的19个值。

    应答的时候,使用的也是功能码01,对,就也是使用读取线圈的功能。在从机(client)收到主机(server)的命令的时候,它明白了要发送19个值给主机,19个值需要2个8bit的数据以及1个3bit的数据去表示,我们把CD,6B,05转化为二进制就是1100 1101 0110 1011,注意最后一个05转化为0000 0101,实际我们取MSB(最高有效位)。

  2. 读取离散量输入function code 0x02

    与第1条几乎一模一样,除了功能码的不同,其余一致。

  3. 读保持寄存器function code 0x03

    • 上位机请求

      功能码 起始地址 寄存器数量
      0x03 0x0000->0xFFFF(2 char) 0x0001->0x007D(2 char)
    • 下位机应答

      功能码 字节数 寄存器值
      0x03 N * 2(1 char) (2 * N char)

    ps. N = 寄存器数量

    这里的请求与功能码01与02的区别仅仅在于01和02的线圈数量的范围是(1-2000,最大7D00),而03的寄存器数量是(1-125,最大007D)。

    而应答的不同就在于寄存器值得分为高4位低4位两个char值去封装。

    模拟一次功能码03的运行:

    1. 首先是请求(hex):

      03 00 6B 00 03

         +--+-----+-----+
         |03|00 6B|00 03|
         +--+-----+-----+
    2. 其次是应答(hex):

      03 06 02 2B 00 00 00 64

         +--+--+-----------------+
         |03|06|02 2B|00 00|00 64|
         +--+--+-----------------+

    其实也不难理解,上位机发送了我要读保持寄存器,从006B这个地址开始读起,也就是读取107号寄存器组的值,往后读3个。

    下位机做了什么呢,下位机说ok,我返回给你3个值,但是实际上是6个字节,0x022B 0x0000 0x0064,这个值一般来说是十进制的,也就是说我得到的那三个寄存器的值是557 0 100,这样就ok了。

  4. 读输入寄存器function code 0x04

    这个与功能码03一致,不予解释。

  5. 写单个线圈function code 0x05

    • 上位机请求

      功能码 输出地址 输出值
      0x05 0x0000->0xFFFF(2 char) 0x0001->0xFF00(2 char)
    • 下位机应答

      功能码 输出地址 输出值
      0x05 0x0000->0xFFFF(2 char) 0x0001->0xFF00(2 char)

    这边需要注意的是请求与应答是一致的,还有请注意输出值是只有0xFF00(ON)和0x0000(OFF)是合法值,其余值均不合法,收到舍弃。

    仍然模拟运行一次:

    1. 首先是请求(hex):

      05 00 AC FF 00

         +--+-----+-----+
         |05|00 AC|FF 00|
         +--+-----+-----+
    2. 其次是应答(hex):

      05 00 AC FF 00

         +--+-----+-----+
         |05|00 AC|FF 00|
         +--+-----+-----+

    上位机请求说,我需要把0x00AC也就是172号线圈写上0xFF00(ON)的命令。

    下位机执行发现语句合法,于是执行完置172线圈的值为ON之后,将请求语句作为应答语句返回。

  6. 写单个寄存器function code 0x06

    • 上位机请求

      功能码 寄存器地址 输出值
      0x06 0x0000->0xFFFF(2 char) 0x0001->0xFFFF(2 char)
    • 下位机应答

      功能码 寄存器地址 输出值
      0x06 0x0000->0xFFFF(2 char) 0x0001->0xFFFF(2 char)

    这里没有什么值得注意的部分,寄存器的值可以是任意的(0000->FFFF)也就是(0->65535)的任意值。

    仍然模拟运行一次:

    1. 首先是请求(hex):

      06 00 04 10 01

         +--+-----+-----+
         |06|00 04|10 01|
         +--+-----+-----+
    2. 其次是应答(hex):

      06 00 04 10 01

         +--+-----+-----+
         |06|00 04|10 01|
         +--+-----+-----+

    请求部分,上位机说我需要给4号寄存器写入1001

    下位机收到于是将请求作为应答返回。

2. 如何设计mobus的每一帧数据

+-----------+-------------+---------+----+---------+
|device addr|function code|data addr|data|crc check|
+-----------+-------------+---------+----+---------+

需要注意的是,我们每次发送与接收数据都得遵守这个格式,device addr是设备地址,但是请注意,他并非ip地址,这个设备地址是与我们约定的,比如说叫0x01,而不是192.168.0.1这样的地址。

至于这些如何使用我们的常见数据类型来表示,这个就随意了。

我们来分析一下,0x00->0xFF0->255,在java中就用char来表示吧,实际上应该是byte & 0xff这样来表示的。但是这个是作业,我想了想还是不要那么麻烦,在cs部分可以自己处理的。我就以char作为底层,封装成String型发送请求,应答也是封装成String型。

在java中,char是16bit的数据,所以我们写代码的时候会和理论的8bit类型有所不同。但是你需要知道的是理论是上文所述的就可以了。而且实际发送的时候,cs的库已近帮助我们把数据从string转换成byte数组了,所以不用担心。

总之最后请求的封装是类似于010100130013这样的string类型。

而下位机收到请求之后需要先解包,解包成类似于01 01 0013 0013的形式。然后再封装应答,当然,这一部分理应是相当简单的一部分,不过我也会给出代码的指导。

让我们来整理一下到底需要做什么

上位机发送请求:

    char a,b,c,d
    String request = a + b + c + d;

下位机收到请求:
    收到 reauest
    解包得到 a b c d

下位机发送应答:
    根据功能码封装应答 response
    发送

上位机收到应答:
    收到 response

3. crc校验

About

这是一份modbus-tcp的指导性说明文件,如果未来会有代码指导的话,我打算使用c语言作为指导语言

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published