跳转至

学习 Modbus 协议和 ModbusRTU 通信

Modbus 协议基础知识


graph LR
  1(Modbus协议储存区说明) --> 2{{为什么要有储存区?}};

  2 ---> 6[站在协议制定者的角度思考];
  2 ---> 7[协议的目的是为了数据传输];
  2 ---> 8[数据的传输无非就是读取和写入];
  2 ---> 9[存储区类型分为布尔和数据];

  1 --> 3{{Modbus存储区有哪些?}};
  3 ---> 10[只读 线圈 输入线圈];
  3 ---> 11[读写 线圈 输出线圈];
  3 ---> 12[只读 寄存器 输入寄存器];
  3 ---> 13[读写 寄存器 输出寄存器];

  1 --> 4{{Modbus存储区代号}};
  4 ---> 14[输入线圈 1区];
  4 ---> 15[输出线圈 0区];
  4 ---> 16[输入寄存器 3区];
  4 ---> 17[输出寄存器 4区];

  1 --> 5{{Modbus存储区范围}};
  5 ---> 18[Modbus规定,每个存储区的最大范围是65536];

  5 ---> 19{{PLC地址}};
  19 ---> 20[西门子:MW100(M区100开始的两个字节变成了一个整型) DB1.DBD0(DB1存储区从0开始的4个字节组成一个数据,有可能是整型,有可能是浮点数)];
  19 ---> 21[三菱:D0 X0 Y0];
  19 ---> 22[欧姆龙:D0 W0];
  19 ---> 23[绝对地址 = 区号 + 相对地址];
  5 ---> 24[Modbus地址:绝对地址 = 区号 + (相对地址+1)]
  5 ---> 25[输出寄存器的第一个绝对地址就是4(区号)0001(相对地址+1)]

  5 ---> 26{{相对地址和绝对地址}};
  26 ---> 27[我们一般在人为交流或说明文档,使用绝对地址];
  26 ---> 28[一般在协议中,都会使用相对地址];
  26 ---> 29[因为在协议报文中,我们可以通过功能码知道是那个储存区];

长地址模型

存储区名称 存储区代号 绝对地址范围 相对地址范围
输入线圈 1 区 100001~165536 0~65535
输出线圈 0 区 000001~065536 0~65535
输入寄存器 3 区 300001~365536 0~65535
保持寄存器 4 区 400001~465536 0~65535

短地址模型

存储区名称 存储区代号 绝对地址范围 相对地址范围
输入线圈 1 区 10001~19999 0~9998
输出线圈 0 区 00001~09999 0~9998
输入寄存器 3 区 30001~39999 0~9998
保持寄存器 4 区 40001~49999 0~9998

如果我们使用不了那么多的数据,可以使用短地址模型,否则,我们就使用长地址模型


graph LR
  1(Modbus协议功能码说明) ---> 2{{为什么会有功能码?}};

  2 ---> 3[协议的目的是为了数据传输];
  2 ---> 4[已经确定好存储区,存储区会存储不同的数据类型];
  2 ---> 5[那么必然会对存储区输入或输出不同类型的数据];
  2 ---> 6[我们给每种行为指定一个代号,那么这个代号就是功能码];
  2 ---> 7[功能码其实就是行为的代号];

  1 ---> 8{{Modbus通信会有哪些行为}}
  8 ---> 9[读取输入线圈]
  8 ---> 10[读取输出线圈]
  8 ---> 11[读取输入寄存器]
  8 ---> 12[读取输出寄存器]
  8 ---> 13[写入输出线圈]
  8 ---> 14[写入输出寄存器]

  1 ---> 30{{Modbus常用功能码有哪些?}}
  30 ---> 31[0x01 读取输出线圈];
  30 ---> 32[0x02 读取输入线圈];
  30 ---> 33[0x03 读取输出寄存器];
  30 ---> 34[0x04 读取输入寄存器];
  30 ---> 35[0x05 写入单个线圈];
  30 ---> 36[0x06 写入单个寄存器];
  30 ---> 37[0x0F 写入多个线圈];
  30 ---> 38[0x10 写入多个寄存器];

  1 ---> 39[Modbus还会有一些其他的功能码,比如异常,自定义];
什么是线圈?

前面介绍 Modbus 的内容中多次提到线圈(coil)和寄存器(register)的概念,尤其是 Modbus 功能码 中,操作的对象基本上都是线圈和寄存器。

在 Modbus 协议中之所以仍然称为线圈和寄存器,完全是历史原因。在 PLC 应用领域,一个线圈就代表一个 PLC 输出点,也称为输出继电器。通过控制线圈导通与否来改变继电器输出状态,实现弱电控制强电。

2.5.webp

但实际上,在如今的 Modbus 设备中,它们都只是对应一块内存区域而已。其中,线圈代表位操作(bit),表示一个布尔变量;寄存器代表字操作(word),表示一个整型变量(当然也可以通过多个字的组合,表示浮点数以及其他复合数据结构)。在 Modbus 协议中,字(word)的长度是 16 位,即 2 个字节。


graph LR
  1(Modbus协议分类及测试) ---> 2{{报文帧}};
  2 ---> 3[ModbusRTU(CRC校验)];
  2 ---> 4[ModbusASCll(LRC校验)];
  2 ---> 5[ModbusTCP(没有校验)];

  1 ---> 6{{通信介质}};
  6 ---> 7{{串口通信}};
  6 ---> 8{{以太网通信}};
  6 ---> 9[232/485/422];
  8 ---> 10[TCP/IP];
  8 ---> 11[UDP/IP];

  1 ---> 12{{协议分类}};
  12 ---> 13[ModbusRTU协议【常用】(运行在串口上)];
  12 ---> 14[ModbusASCll协议【其次】(运行在串口上)];
  12 ---> 15[ModbusRTUOverTCP协议【其次】(ModbusTCP本身无校验,串口通信(232/485/422)也没有校验;以太网本身会带校验,RTU本身也会带校验)];
  12 ---> 16[ModbusRTUOverUDP协议];
  12 ---> 17[ModbusASCllOverTCP协议];
  12 ---> 18[ModbusASCllOverUDP协议];
  12 ---> 19[ModbusTCP协议【常用】(ModbusTCP运行在TCP/IP上)];
  12 ---> 20[ModbusUDP协议(ModbusTCP运行在UDP/IP上)];

graph LR
  1(ModbusRTU通信格式说明) ---> 2{{通用报文帧格式}};

  2 ---> 3[从站地址(1个字节/8bit)];
  3 ---> 7[要和那个设备通信];

  2 ---> 4[功能码(1个字节/8bit)];
  4 ---> 8[要做什么];

  2 ---> 5[数据部分(N个字节)];
  5 ---> 9{{读取发送}};
  9 ---> 15[开始地址];
  9 ---> 15[读取数量];

  5 --->10{{读取接收}};
  10 ---> 16[字节计数];
  10 ---> 17[具体数据];

  5 --->11{{写入单发送}};
  11 ---> 18[具体地址];
  11 ---> 18[写入数据];

  5 ---> 12{{写入单接收}};
  12 ---> 19[具体地址];
  12 ---> 20[写入数据];

  5 ---> 13{{写入多发送}};
  13 ---> 21[开始地址];
  13 ---> 22[写入数量];
  13 ---> 23[写入数据];

  5 ---> 14{{写入多接收}};
  14 ---> 24[开始地址];
  14 ---> 25[写入数量];

  1 ---> 26{{校验部分}};
  26 ---> 27[这里的校验和串口的奇/偶/无校验是没有关系];
  26 ---> 28{{为什么要有校验?}};
  28 ---> 29[保证数据的准确];
  26 ---> 30{{校验的本质是什么?}};
  30 ---> 31{{校验的本质就是一种算法}};


  2 ---> 6[校验部分(2个字节/16bit)];

01H 功能码读取输出线圈(01 Coil Status/0x )

1.avif

发送报文01 01 00 0A 00 14 1C 07
01是从站地址,1 号站点
01是功能码
00 0A十六进制转换十进制是 10,即从 10 开始的 10
00 14十六进制转换为十进制是 20,是 20 个线圈
1C 07是 CRC 校验码

接收报文01 01 03 03 00 00 CC 4E
01是从站地址,1 号站点
01是功能码
03是三个字节计数,因为 20 个线圈的值需要二十个位储存,一个字节是八位(八位寄存器),所以需要三个字节来储存
03 00 0003 二进制为 0011,上图中从地址 10 开始两个 11,后面全部补 0 即03 00 00
CC 4E是 CRC 校验码

02H 功能码读取输入线圈(02 Input Status/1x)

2.avif

发送报文05 02 00 14 00 0A B9 8D
05是从站地址,1 号站点
02是功能码
00 14十六进制转换十进制是 20,即从 20 开始的 20
00 0A十六进制转换为十进制是 10,是 10 个线圈
B9 8D是 CRC 校验码

接收报文05 02 02 03 00 48 88
05是从站地址,5 号站点
02是功能码
02是两个字节计数,因为 10 个线圈的值需要十个位储存,一个字节是八位(八位寄存器),所以需要两个字节来储存
03 0003 二进制为 0011,上图中从地址 20 开始两个 11,后面全部补 0 即03 00
48 88是 CRC 校验码

03H 功能码读取输出寄存器(03 Holding Register/4x)

3.avif

发送报文02 03 00 0A 00 04 64 38
02是从站地址,2 号站点
03是功能码
00 0A十六进制转换十进制是 10,即从 10 开始的 10
00 04十六进制转换为十进制是 4,是 4 个寄存器
64 38是 CRC 校验码

接收报文02 03 08 00 01 00 02 00 03 00 04 02 50
02是从站地址,2 号站点
03是功能码
08是八个计数,一个寄存器是十六个 bit,两个字节(十六位寄存器),所以四个寄存器需要 8 个字节来储存
00 01 00 02 00 03 00 04是数据,00 01是数据 1,00 02是数据 2,00 03是数据 3,00 04是数据 4
02 50是 CRC 校验码

04H 功能码读取输入寄存器(04 Input Registers/4x)

4.avif

发送报文02 04 00 0A 00 04 DI F8
02是从站地址,2 号站点
04是功能码
00 0A十六进制转换十进制是 10,即从 10 开始的 10
00 04十六进制转换为十进制是 4,是 4 个寄存器
DI F8是 CRC 校验码

接收报文02 04 08 00 01 00 02 00 03 00 04 B3 8A
02是从站地址,2 号站点
04是功能码
08是八个计数,一个寄存器是十六个 bit,两个字节(十六位寄存器),所以四个寄存器需要 8 个字节来储存
00 01 00 02 00 03 00 04是数据,00 01是数据 1,00 02是数据 2,00 03是数据 3,00 04是数据 4
B3 8A是 CRC 校验码

05H 功能码预置单线圈(01 Coil Status/0x )

5.avif

发送报文02 05 00 05 FF 00 9C 08
02是从站地址,2 号站点
05是功能码
00 05十六进制转换十进制是 5,即从 05 地址置位的 5
FF 00是置位(00 00 是复位,1 变 0
9C 08是 CRC 校验码

接收报文相同

06H 功能码预置单寄存器(03 Holding Register/4x)

6.avif

发送报文05 06 00 0A 00 7B E8 6F
05是从站地址,5 号站点
06是功能码
00 0A十六进制转换十进制是 10,即从 10 地址写入的 10
00 7B是写入 123 的 123
E8 6F是 CRC 校验码

接收报文相同

0FH 功能码预置多线圈(01 Coil Status/0x )

7.avif

发送报文01 0F 00 01 00 05 01 16 D3 58
01是从站地址,1 号站点
0F是功能码
00 01十六进制转换十进制是 1,即从 1 开始的 1
00 05是数量即 5 个线圈
01 16是一个字节,八位;16 是如图的 Ture 和 False 的占位,00010110,所以是 16
D3 58是 CRC 校验码

接收报文01 0F 00 01 00 05 C4 08
字节计数和数据去掉,其他相同
C4 08是 CRC 校验码

8.avif

需要注意的是不会改变其他线圈的值

10H 功能码预置多寄存器(03 Holding Register/4x)

9.avif

发送报文01 10 00 0A 00 05 0A 00 01 00 02 00 03 00 04 00 05 E0 60
01是从站地址,1 号站点
10是功能码
00 0A十六进制转换十进制是 10,即从 10 开始的 10
00 05是数量即 5 个寄存器
0A五个寄存器是十个字节
00 01 00 02 00 03 00 04 00 05是数据
E0 60是 CRC 校验码

接受报文01 10 00 0A 00 05 20 08
字节计数和数据去掉,其他相同
E0 60是 CRC 校验码

字节计数,如上图的 0A 最大限制是 FF 即 255,所以长度最多是 127

为什么字节计数最多是 255,长度是 127?
  1. 字节计数最大值为 255: 由于字节计数字段只有 1 个字节,其最大值为 255。因此,数据部分最多可以有 255 个字节。
  2. 寄存器数量最大值是 127: 由于每个寄存器占 2 个字节,所以要传送 127 个寄存器数据,总共需要 127×2 = 254 字节。为了将这个数据合适地封装在报文中,字节计数限制为 255 字节,而寄存器的数量限制为 127(因为要保证字节计数的有效性)。

因此,理论上最多可以写 127 个寄存器(每个寄存器 2 字节),这时数据部分的字节数为 254 字节,加上字节计数 1 字节,总报文长度为 255 字节。

大小端

Modbus 协议 中,大小端(Endianness) 是指多字节数据的存储顺序问题。对于单个寄存器(16 位)而言,不存在大小端问题,因为一个寄存器本身只能存储 2 个字节(16 位),其内部字节顺序由 Modbus 协议本身决定。但当涉及多个寄存器组成的多字节数据(如 32 位整数、浮点数等)时,大小端问题就会变得非常重要。


1. 什么是大小端?

  • 大端模式(Big-Endian):高位字节存储在低地址(或寄存器的前一个字节)。
  • 小端模式(Little-Endian):低位字节存储在低地址(或寄存器的前一个字节)。

例如,一个 32 位整数 0x12345678 的存储方式:

  • 大端0x12 0x34 0x56 0x78(高位在前)
  • 小端0x78 0x56 0x34 0x12(低位在前)

2. 为什么单个寄存器不存在大小端问题?

  • 一个 Modbus 寄存器是 16 位(2 字节),其内部字节顺序由 Modbus 协议固定为 大端模式
  • 例如,数值 0x1234 在寄存器中存储为 0x12(高字节)和 0x34(低字节)。
  • 直接读取单个寄存器的值时,它的字节顺序是固定的,没有歧义,因此不存在大小端问题。

3. 多寄存器数据的大小端问题

当需要读取多个寄存器组成的数据(如 32 位整数、浮点数)时,需考虑两种顺序:

  1. 字节顺序(Byte Order):每个寄存器内部的字节顺序。
  2. 字顺序(Word Order):多个寄存器的排列顺序。

例如,一个 32 位整数 0x12345678 占用两个寄存器,可能有以下存储方式:

数据格式 寄存器 1 寄存器 2 实际字节流
ABCD 0x1234 0x5678 0x12 0x34 0x56 0x78
BADC 0x3412 0x7856 0x34 0x12 0x78 0x56
CDAB 0x5678 0x1234 0x56 0x78 0x12 0x34
  • ABCD:大端字节序 + 大端字序(最常见)。
  • BADC:小端字节序 + 大端字序。
  • CDAB:大端字节序 + 小端字序。

4. 为什么需要关心大小端?

如果设备与 Modbus 主站的数据格式不一致,会导致解析错误。例如:

  • 设备发送 0x12345678(ABCD 格式),但主站按 BADC 解析,会得到错误值 0x34127856

5. 实际应用示例

假设从寄存器 40001 读取一个 32 位浮点数:

  • 原始寄存器值40001=0x4148, 40002=0x0000(对应浮点数 12.5)。
  • 解析方式
  • ABCD 格式0x41 0x48 0x00 0x00 → 正确值 12.5
  • BADC 格式0x48 0x41 0x00 0x00 → 错误值 6.3e-05

总结

  • 单个寄存器(16 位):没有大小端问题,直接按大端解析。
  • 多寄存器(32 位及以上):必须明确设备的数据格式(如 ABCD、BADC 等),否则会导致解析错误。

实际开发中,需参考设备文档或通过实验确定数据格式。3:55:11


参考:

C#上位机开发零基础入门到精通

Modbus 寄存器