完整的Modbus指南

2022-03-29 19:54:26 浏览数 (1)

  • Modbus概述
  • Modbus RTU 数据帧
  • Modbus 地址
  • 功能代码
    • 读取线圈 - 01
    • 读取离散输入 - 02
    • 读取持有寄存器 - 03
    • 读取输入寄存器 - 04
    • 写入单线圈 - 05
    • 写入单个寄存器 - 06
    • 写入多个线圈 - 0F
    • 写入多个寄存器 - 10
  • 异常响应
  • Modbus RTU CRC 计算
  • Modbus TCP

Modbus概述

什么是Modbus?

尽管它已经很老了,但Modbus仍然是现场通信最常用的协议之一。它的相对简单性、稳健性和开放性使其成为许多自动化硬件和软件供应商的首选协议。因此,Modbus是组织承诺的安全选择,因为总有设备支持它。

Modbus的另一个主要好处是它没有规定特定的物理层。相反,Modbus可以在RS-232,RS-485或以太网上的TCP / IP上运行。这些都是便宜的,并且已经在企业中普遍使用。这意味着无需投资昂贵的特定于协议的网络基础设施。

根据数据编码格式、传输层和其他一些注意事项,有不同类型的 Modbus 实现。最流行的协议类型是:

  • Modbus RTU(串行链路上的二进制文件)
  • Modbus ASCII(基于文本的串行链路)
  • Modbus TCP(通过 TCP/IP 传输的二进制文件)

Modbus RTU 协议概述

Modbus RTU是一种主从协议。这意味着只允许一个设备(主设备)启动通信。网络上的其他设备称为从站,它们只能响应请求。Modbus RTU 可以在同一物理网络上支持多达 247 台设备。可以修改协议以支持更多的从站,但在大多数应用中,从站的标准限制如果足够的话。

Modbus RTU 将数据编码为二进制,并对 16 位值使用大端编码。这意味着首先发送 16 位字的最大有效字节。

以下是Modbus RTU请求和响应消息的示例,其中包含每个项目的说明。首先,主服务器发送一个请求,告诉从站返回一个从地址开始的寄存器的值。12

代码语言:javascript复制
slave id (1 byte)
|  function code (read holding registers)
|  |      address of first register to read (2 bytes)
|  |      |     number of registers to read (1 byte)
|  |      |     |  checksum (2 bytes)
|  |      |     |  | 
01 03 00 02 00 01 25 CA

该请求还包括一个校验和,用于确保消息在到达从站的途中未损坏。除 从站外,所有从站都必须忽略该消息。从站应发送类似于以下内容的响应消息:11

代码语言:javascript复制
slave id (repeats own id)
|  function code (repeats requested code)
|  |   number of bytes of data (2)
|  |   |     the value of the register (0x07FF)
|  |   |     |     checksum
|  |   |     |     |
01 03 02 07 FF FA 34

Modbus ASCII

Modbus ASCII的工作原理类似于Modbus RTU,但它使用基于文本的数据编码。这使得请求和响应具有人类可读性,这是 RTU 的主要优势。另一方面,它的效率要低得多,因为消息的长度是原来的两倍。因此,Modbus ASCII仅用于测试,很少用于生产。

Modbus RTU 和 ASCII 的局限性

该协议的低要求和简单性有其缺点:

  • 没有好的方法在同一网络上有多个主站,或者实现双向通信。这是因为没有机制来控制媒体访问,从而避免冲突。
  • 很难通过串行链路(如RS-485)支持许多从站。事实上,只有通过构建一个复杂的主站和从站嵌套层次结构,才能使用几十个以上的设备。
  • 串行链路的带宽限制为 115200 波特。按照现代标准,这是相当低的,但仍然适用于许多应用程序。

Modbus TCP

Modbus TCP是Modbus的改编版,用于现代TCP / IP网络之上。有两种类型的Modbus TCP实现:

  • 通过 TCP 进行 Modbus RTU,它只是使用 TCP 作为 RTU 消息的传输层
  • 普通的Modbus TCP,在消息格式上有一些变化。

由于Modbus TCP使用以太网网络,因此数据传输速度远高于使用串行链路的RTU。缺点是,在某些类型的现场设备中,TCP / IP堆栈更难以支持,在这些设备中,Modbus RTU可以正常工作。

Modbus RTU 数据帧

Modbus数据帧是通过Modbus网络传输的消息。有请求帧和响应帧。请求是从主站到从站的消息。响应是从属服务器发回主站的消息。

数据帧的长度和内容因所执行的读/写操作的类型而异。我们稍后将介绍所有这些内容。但首先让我们检查一下请求框架的基本结构:

代码语言:javascript复制
01 03 02 00 01 25 CA

这些是通过Modbus RTU网络发送的8位十六进制字符。在我们的例子中,整个消息的长度是七个字节。让我们看看框架内部的内容:

代码语言:javascript复制
01         03        02 00 01                 25 CA
-----------------------------------------------------
slave id   function  function-specific data   CRC
1 byte     1 byte                             2 bytes

每个 Modbus 消息的第一个字节都是 .主服务器指定请求消息所针对的从站的 ID。从站必须在每个响应消息中指定自己的 ID:slave id

代码语言:javascript复制
01 03 02 00 01 25 CA
|
slave id (1 byte)

每个 Modbus 消息的第二个字节是 .此代码确定从站要执行的操作类型。我们稍后将介绍函数代码。function code

代码语言:javascript复制
01 03 02 00 01 25 CA
   |
   function code (1 byte)

每条 Modbus 消息的最后两个字节是 。这些是根据帧的前一个字节计算的,并允许主从站验证接收消息的完整性。稍后我们将介绍 CRC 计算中使用的算法。CRC bytes

代码语言:javascript复制
01 03 02 00 01 25 CA
               -----
                 |
                 CRC (2 bytes)
                

以下是一般形式的Modbus消息,我们将在本文的其余部分使用:

代码语言:javascript复制
[ID][FC][DATA][CRC]

这里对应于 ,is ,是特定于函数的可变长度字符序列,并且是一个 2 字节校验和。IDslave idFCfunction codedataCRC

Modbus 地址

Modbus 设备有 4 种类型的地址:

  • 线圈
  • 离散输入
  • 输入寄存器
  • 持有登记

线圈是1位(布尔值)读/写设备。每个线圈可以处于或状态之一。分立输入类似于线圈,但它们是只读的 - 无法设置分立输入的值。您可以将线圈视为 PLC 的输出,将离散输入视为 PLC 的输入。onoff

保持寄存器类似于PLC存储器。它们是16位字,您可以通过Modbus协议读取和写入。输入寄存器也是16位字,但它们是只读的,就像传感器的读数一样。

Modbus 地址是一个 16 位无符号整数,随每个请求一起传输,以指示应读取或写入哪些数据。地址在Modbus消息中占据两个字符,并且首先发送最重要的字节(大端)。

在此示例请求中,我们引用的地址为 或Holding Register 1HR1

代码语言:javascript复制
01 03 02 00 01 25 CA
         -----
           |
           Address (2 bytes)

功能代码

在本节中,我们将介绍Modbus函数代码,并解释每个函数代码的数据帧构造的细节。

读取线圈 - 0x01

该函数代码允许主站查询从机线圈的状态。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][CRC]
  • ADDR- 第一个线圈的地址(2字节)
  • NUM- 要读取的线圈数量(2字节)

读取线圈请求的长度始终为 8 个字节。以下是从地址开始读取线圈的示例请求:20xA

代码语言:javascript复制
 ID   FC   ADDR     NUM    CRC
[01] [01] [00 0A] [00 02] [9D C9]
响应
代码语言:javascript复制
[ID][FC][BC][DATA(1 )][CRC]
  • BC- 响应中的字节数(1 字节)DATA
  • DATA- 包含线圈状态的字节序列(每 8 个线圈 1 个字节)

读取线圈响应的长度至少为 6 个字节。在我们的示例中,我们只有一个字节,因为我们只请求两个线圈,因此所有数据都适合单个字节:DATA

代码语言:javascript复制
 ID   FC   BC   DATA  CRC
[01] [01] [01]  [03] [11 89]

读取离散输入 - 0x02

此函数代码允许主站查询从站离散输入的状态。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][CRC]
  • ADDR- 第一个离散输入的地址(2字节)
  • NUM- 要读取的输入数(2字节)

读取离散输入请求的长度始终为 8 个字节。以下是读取从地址开始的离散输入的示例请求:20x00

代码语言:javascript复制
 ID   FC   ADDR     NUM     CRC
[01] [02] [00 00] [00 02] [F9 CB]
响应
代码语言:javascript复制
[ID][FC][BC][DATA(1 )][CRC]
  • BC- 响应中的字节数(1 字节)DATA
  • DATA- 包含离散输入状态的字节序列(每 8 个输入 1 个字节)

读取离散输入响应的长度至少为 6 个字节。例:

代码语言:javascript复制
 ID   FC   BC  DATA   CRC
[01] [02] [01] [02] [20 49]

读取持有寄存器 - 0x03

此函数代码允许主站查询从站的持有寄存器的状态。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][CRC]
  • ADDR- 第一个寄存器的地址(2字节)
  • NUM- 要读取的寄存器数量(2 字节)

读取保持寄存器请求的长度始终为 8 个字节。在下面的请求中,我们阅读了在地址的保持注册:10x02

代码语言:javascript复制
 ID   FC   ADDR     NUM    CRC
[01] [03] [00 02] [00 01] [25 CA]
响应
代码语言:javascript复制
[ID][FC][BC][DATA(2 )][CRC]
  • BC- 响应中的字节数(1 字节)DATA
  • DATA包含保持寄存器值的字节序列(每个寄存器 2 个字节)

读取保持寄存器响应的长度至少为 7 个字节。例:

代码语言:javascript复制
 ID   FC   BC   DATA     CRC
[01] [03] [02] [07 FF] [FA 34]

读取输入寄存器 - 0x04

使用此函数代码,主站查询从属输入寄存器的状态。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][CRC]
  • ADDR- 第一个寄存器的地址(2字节)
  • NUM- 要读取的寄存器数量(2 字节)

读取输入寄存器请求的长度始终为 8 个字节。在下面的请求中,我们阅读了在地址的保持注册:10x00

代码语言:javascript复制
 ID   FC   ADDR     NUM    CRC
[01] [04] [00 00] [00 01] [31 CA]
响应
代码语言:javascript复制
[ID][FC][BC][DATA(2 )][CRC]
  • BC- 响应中的字节数(1 字节)DATA
  • DATA包含保持寄存器值的字节序列(每个寄存器 2 个字节)

读取保持寄存器响应的长度至少为 7 个字节。例:

代码语言:javascript复制
 ID   FC   BC   DATA     CRC
[01] [04] [02] [03 FF] [F9 80]

写入单线圈 - 0x05

设置单个从线圈的值。

请求
代码语言:javascript复制
[ID][FC][ADDR][VAL][CRC]
  • ADDR- 要写入的线圈的地址(2字节)
  • VAL- 线圈写入的值(2字节),将线圈设置为,将线圈设置为0xFF00ON0x0000OFF

写入单个线圈请求的长度始终为 8 个字节。例:

代码语言:javascript复制
 ID   FC   ADDR     NUM    CRC
[01] [05] [00 0A] [00 00] [ED C8]
响应

写入单线圈的成功响应将精确地重复该请求:

代码语言:javascript复制
 ID   FC   ADDR     NUM    CRC
[01] [05] [00 0A] [00 00] [ED C8]

写入单个寄存器 - 0x06

设置单个从站的保持寄存器的值。

请求
代码语言:javascript复制
[ID][FC][ADDR][VAL][CRC]
  • ADDR- 要写入的寄存器的地址(2字节)
  • VAL- 要写入的寄存器的值(2 个字节)

写入单个寄存器请求的长度始终为 8 个字节。例:

代码语言:javascript复制
 ID   FC   ADDR     VAL    CRC
[01] [06] [00 02] [0C 00] [2D 0A]
响应

写入单个寄存器的成功响应与请求非常相似:

代码语言:javascript复制
 ID   FC   ADDR     VAL    CRC
[01] [06] [00 02] [0C 00] [2D 0A]

写入多个线圈 - 0x0F

设置从线圈的连续范围的值。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][BC][DATA(1 )][CRC]
  • ADDR- 第一个要写入的线圈的地址(2字节)
  • NUM- 要写入的线圈数量(2字节)
  • BC- 请求中数据的字节数(1 字节)
  • DATA- 要设置的线圈值(每8个线圈1个字节)

在此示例中,我们将 () 线圈的值设置为:100x0A0x01ON

代码语言:javascript复制
 ID   FC   ADDR    NUM     BC   DATA     CRC
[01] [0F] [00 01] [00 0A] [0F] [FF 03] [AB CD]
响应
代码语言:javascript复制
 ID   FC   ADDR    NUM     CRC
[01] [0F] [00 01] [00 0A] [AB CD]
  • ADDR- 第一个更新的线圈的地址(2字节)
  • NUM- 更新的线圈数量(2字节)

写入多个寄存器 - 0x10

设置从站保持寄存器的连续范围的值。

请求
代码语言:javascript复制
[ID][FC][ADDR][NUM][BC][DATA(2 )][CRC]
  • ADDR- 第一个要写入的寄存器的地址(2字节)
  • NUM- 要写入的寄存器数量(2字节)
  • BC- 请求中数据的字节数(1 字节)
  • DATA- 要设置的寄存器值(每个寄存器2个字节)

在这个例子中,我们写了2个寄存器,从:0x0A

代码语言:javascript复制
 ID   FC   ADDR    NUM     BC      DATA        CRC
[01] [10] [00 0A] [00 02] [04] [00 0A 01 02] [AB CD]
响应
代码语言:javascript复制
 ID   FC   ADDR    NUM     CRC
[01] [10] [00 0A] [00 02] [AB CD]
  • ADDR- 第一个更新的寄存器的地址(2字节)
  • NUM- 更新的寄存器数量(2字节)

异常响应

在某些情况下,从站可能无法处理主请求。对于这种情况,Modbus定义了一个异常响应帧

代码语言:javascript复制
[ID][FC][EC][CRC]
  • FC- 导致异常的请求的功能代码,其中最高有效位设置为(1字节)1
  • EC- 解释发生的事情的异常代码(1字节)

对请求的异常响应示例:read coils

代码语言:javascript复制
 ID   FC   EC    CRC
[01] [81] [02] [C1 91]

的函数代码是 ,但在异常响应中,最高有效位已设置为 ,因此代码变为:read coils0x0110x81

代码语言:javascript复制
0000 0001 => 1000 0001
 (0x01)       (0x81)

标准异常代码

以下是最常见的Modbus异常代码列表:

  • 01 - 非法函数 - 指定的函数代码不受从站支持
  • 02 - 非法数据地址 - 从属服务器上未定义指定的数据地址
  • 03 - 无效数据值 - 指定的数据无效
  • 04 - 设备故障 - 从站无法生成响应
  • 05 - 确认 - 从站接受命令并正在处理它
  • 06 - 繁忙 - 从站正忙,无法处理消息

Modbus RTU CRC 计算

每条Modbus RTU消息(帧)在末尾包含两个CRC字节:

代码语言:javascript复制
01 03 02 00 01 25 CA
               -----
                 |
         CRC bytes

主从节点使用这些字节来验证所有接收消息的完整性。

CRC 值是根据消息的其余部分计算的。换句话说,除了最后两个字节之外,还使用了所有字节:

代码语言:javascript复制
01 03 02 00 01 25 CA
--------------
 used to 
 compute CRC

当Modbus设备收到消息时,它会计算它自己的CRC值,并将其与收到的CRC值进行比较。

CRC 算法

CRC 代表 循环冗余校验。下面是计算 CRC 的示例代码#

代码语言:javascript复制
public static void CRC(byte[] data, out byte msb, out byte lsb)
{
    ushort crcFull = 0xFFFF;
    for (var i = 0; i < data.length; i  )
    {
        crcFull = (ushort)(crcFull ^ data[i]);
        for (var j = 0; j < 8; j  )
        {
            var crclsb = (char)(crcFull & 0x0001);
            crcFull = (ushort)((crcFull >> 1) & 0x7FFF);
            if (crclsb == 1)
            {
                crcFull = (ushort)(crcFull ^ 0xA001);
            }
        }
    }

    lsb = (byte)((crcFull >> 8) & 0xFF);
    msb = (byte)(crcFull & 0xFF);
}

请注意,Modbus ASCII使用不同的算法来计算消息校验和,称为LRC。

Modbus TCP

Modbus TCP 是设计用于使用 TCP/IP 堆栈传输 Modbus 帧的协议,通常通过以太网物理层传输。

Modbus和TCP可以通过两种方式协同工作。一个是实际的Modbus TCP协议,另一个是Modbus RTU-over-TCP。

Modbus TCP vs RTU-over-TCP

在这两种情况下,TCP 都是一种承载 Modbus 消息的传输协议。但信息是不同的。

在 Rtu-over-TCP 中,TCP 用于传输与 Modbus RTU(串行)中使用的消息完全相同的消息。

另一方面,在Modbus TCP中,消息(帧)本身具有不同的结构,因此两种格式不兼容。

Modbus TCP Frame

Modbus TCP帧不同于常规Modbus RTU帧。

RTU 框架具有以下常规结构:

代码语言:javascript复制
[slave ID][data][CRC bytes]

要将其转换为 TCP 帧,我们必须:

  • 删除slave ID
  • 删除CRC bytes
  • 在消息前面添加MBAP header

喜欢这个:

代码语言:javascript复制
[slave ID][data][CRC bytes]
    [MBAP][data]

MBAP 代表Modbus Application Protocol。MBAP 标头本身具有以下结构:

代码语言:javascript复制
[transaction ID][protocol ID][ length ][unit id]
    2 bytes        2 bytes    2 bytes   1 byte

Transaction ID是主站为每个新请求设置的随机数,并且必须由从站用于响应。始终在 Modbus TCP 中。是后面的字节数,包括 和剩余的 字节数。是类似于从属 ID 的设备地址Protocol ID0Lengthunit iddataUnit ID

0 人点赞