- 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
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
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
01 03 02 00 01 25 CA
|
slave id (1 byte)
每个 Modbus 消息的第二个字节是 .此代码确定从站要执行的操作类型。我们稍后将介绍函数代码。function code
01 03 02 00 01 25 CA
|
function code (1 byte)
每条 Modbus 消息的最后两个字节是 。这些是根据帧的前一个字节计算的,并允许主从站验证接收消息的完整性。稍后我们将介绍 CRC 计算中使用的算法。CRC bytes
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
01 03 02 00 01 25 CA
-----
|
Address (2 bytes)
功能代码
在本节中,我们将介绍Modbus函数代码,并解释每个函数代码的数据帧构造的细节。
读取线圈 - 0x01
该函数代码允许主站查询从机线圈的状态。
请求
代码语言:javascript复制[ID][FC][ADDR][NUM][CRC]
ADDR
- 第一个线圈的地址(2字节)NUM
- 要读取的线圈数量(2字节)
读取线圈请求的长度始终为 8 个字节。以下是从地址开始读取线圈的示例请求:20xA
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
ID FC BC DATA CRC
[01] [01] [01] [03] [11 89]
读取离散输入 - 0x02
此函数代码允许主站查询从站离散输入的状态。
请求
代码语言:javascript复制[ID][FC][ADDR][NUM][CRC]
ADDR
- 第一个离散输入的地址(2字节)NUM
- 要读取的输入数(2字节)
读取离散输入请求的长度始终为 8 个字节。以下是读取从地址开始的离散输入的示例请求:20x00
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
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
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
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
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
ID FC EC CRC
[01] [81] [02] [C1 91]
的函数代码是 ,但在异常响应中,最高有效位已设置为 ,因此代码变为:read coils0x0110x81
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