Skip to content

Modbus 协议基础

写在前面

作者对 Modbus 并没有很深入的使用,只是工作中用到了 Modbus 的某些方面, 以下仅是作者在学习及实际使用 Modbus 后对它的一个理解与总结,如有不对的地方可与作者讨论

我们平常说的 Modbus 协议 是一种规定,它其实有多种实现,目前常见的标准实现主要有以下几种

  • Modbus RTU: 基于 RS-485/RS-232 的串行通信,使用 CRC-16 校验确保数据完整性
  • Modbus TCP: 基于 TCP/IP 以太网通信,依赖 TCP 协议自带的校验确保数据完整性
  • Modbus ASCII: 也是基于串行通信,但是数据使用 ASCII 编码,以字符串形式呈现,传输效率低

还有 Modbus over TLS 及一些第三方实现的协议,市面上不为常见,这里也就不谈了

尽管 Modbus 协议 有多种实现,但它们的核心基本是一致的,我们只要了解了一个其它的也很容易就懂了

本文中会先讲协议通用部分,后面会针对不同协议做说明

通信架构

Modbus 采用主从模式通信架构(即一对多),由一个主机控制通信流程,多个从机只能被动响应

  • 主机(Master): 唯一拥有通信发起权,主动发送请求帧(如读取数据、写入命令)
  • 从机(Slave): 被动响应,仅在收到匹配自身地址的请求时回复,无法主动上报数据

Modbus设计

通信过程

  1. 主机(Master)发起请求

    • 构造请求帧(请求报文),通过串口(Modbus RTU/ASCII)或以太网(Modbus TCP)发送
    • 请求帧示例(Modbus RTU 格式)
    [01][03][00 01][00 04][2B 14] // Modbus RTU 请求帧格式
  2. 从机(Slave)响应

    • 从机执行接收到的功能码操作并返回数据
    • 响应帧示例(Modbus RTU 格式)
    [01][03][08][40 27 AE 14 41 C8 0 0][12 71] // Modbus RTU 响应帧格式
  3. 主机处理响应结果,校验数据有效性(如 Modbus RTU 的 CRC 校验)

是的,通信过程就是这么简单,至于请求/响应帧如何构建我们后面会详细讲到

寄存器

我们想要通过 Modbus 对设备进行操作,无非就是要对设备进行一些设置或者读取一些信息,那么设备内肯定就会有存储这些数据的容器,存储数据的容器就是 寄存器

对于 Modbus 寄存器 而言,很多人在初学时会被这些什么线圈、散离输入等名词搞得云里雾里,其实想的太复杂,你可以简单的理解为是一些不同类型的容器,用来存放设备相关的数据,并提供了读/写的能力

Modbus 中有 4 种不同类型的寄存器,每种寄存器也有多个,我们在请求时会通过 功能码 来区分操作(读/写)哪种类型的寄存器, 通过 寄存器地址 来确定具体操作的是哪些寄存器

  1. 线圈寄存器(Coils)

    • 数据类型: 1 位(bit),存储值 01
    • 读写属性: 可读可写(功能码: 0x01 读、0x05 写单个、0x0F 写多个)
    • 地址范围: 00001 ~ 09999
    • 用途理解: 控制设备开关量(继电器状态),因为 bit 只能表示两种状态( 01,也相当于 OFFON),比如用于控制设备开关、指示灯开关等
  2. 离散输入寄存器(Discrete Inputs)

    • 数据类型: 1 位(bit),存储值 01
    • 读写属性: 只读(功能码: 0x02 读)
    • 地址范围: 10001 ~ 19999
    • 用途理解: 一般用于监测设备外部信号,比如监测设备某个按钮是按下还是松开的状态
  3. 保持寄存器(Holding Registers)

    • 数据类型: 16 位(两个字节),存储值 0-65535(无符号)或 -32768~32767(有符号)
    • 读写属性: 可读可写(功能码: 0x03 读、0x06 写单个、0x10 写多个)
    • 地址范围: 40001 ~ 49999
    • 用途理解: 这类寄存器就可以存放具体的数据量了,一般可用于存放设备可读写的配置、参数等,比如设置设备显示的时间,可以设置当然也能获取
  4. 输入寄存器(Input Registers)

    • 数据类型: 16 位(两个字节),存储值 0-65535(无符号)或 -32768~32767(有符号)
    • 读写属性: 只读(功能码: 0x04 读)
    • 地址范围: 30001 ~ 39999
    • 用途理解: 与保持寄存器类似,但是只支持读取,一般用于存放设备的实时数据,比如温度传感器的温度值(有些设备可能会使用保持寄存器来存储这类数据)

数据交互

前面 通信架构 中说明了 Modbus 是主从架构,即 一个主机对多个从机,那么主机要怎么知道要对哪个从机进行操作呢?

是的,每个 Modbus 从机都会有一个地址: 从机地址 ,地址范围是 1-247,且不会重复

为什么从机地址是 1-247 ?

  • 协议规范限制: Modbus 协议规定了协议帧中的从机地址字段为 8 位(也就是 1 字节),理论范围也就是 0-255 之间,但是这 256 个地址不会都给 Modbus 从机使用
    • 0: 广播地址(仅 RTU/ASCII),若请求时指定从机地址是 0,那么这个请求会发送到所有从机,从机收到请求后不会响应。一般会用于给所有设备设置参数
    • 1-247: 实际可用的从机地址(最大 247 个设备)
    • 248-255: 保留未使用(部分厂商自定义)
  • 历史遗留: 早期工业设备规模较小,247 个从机已满足需求,且 RS-485 总线负载能力有限

结合前文所讲,已经满足主从机交互条件,现在你大概也能知到 Modbus 通信需要哪些参数了

  • 从机地址 明确需要交互的是哪一台从机设备
  • 功能码 明确操作(是读还是写)以及操作的寄存器类型(线圈寄存器、保持寄存器等)
  • 数据域 明确操作的具体是哪些地址的寄存器以及实际的数据

另外除了上面这些参数外,我们还需要保证数据传输的完整性,所以 Modbus 还有 校验码 用来验证本次传输的数据是否完整。 不过这个校验规则并不是所有协议都相同( Modbus RTU 使用 CRC-16Modbus ASCII 使用 LRCModbus TCP 则没有,因为 TCP 协议本身就具有数据可靠性)

提示

Modbus RTU / ASCII / TCP 这几种协议的通信都略有差别,我们后面会将这些协议分别来讲,你要知道的是不论哪种 Modbus 协议的请求/响应帧中都离不开上面这些参数, 交互时我们必须要将这些参数按照你使用的 Modbus 协议规定的帧格式进行构造

Modbus RTU

注意

之前说过 Modbus 不同协议的核心内容是基本是相同的,所以我会重点讲解一下 Modbus RTU 协议,了解了这个协议后其他的协议很容易就懂了

Modbus RTU 是使用最广泛的 Modbus 协议,是 Modbus 协议 的串口通信模式,它基于 RS-485/RS-232 物理层传输, RS-485 尤其常见,因为它支持多点通信、长距离传输(1200 米)、抗干扰能力强,且数据使用二进制编码,效率高、速度快(相对于 Modbus ASCII), 不过对时序要求严格,使用时需要格外注意波特率和帧间间隔(后面会说明)

帧字段说明: Modbus RTU 协议的请求/响应帧均遵循以下结构模板

[从机地址] [功能码] [数据区域] [CRC校验码]

字段长度说明
从机地址1 字节目标设备地址(1-247),广播地址为 0
功能码1 字节指定操作类型(比如 读保持寄存器、写线圈 等)
数据域可变长度包含寄存器地址、数量、实际数据
校验码2 字节CRC-16(循环冗余检验 16 位算法)

注意:在不同的操作中 数据域 的结构可能会不一样,我们在下面会举例说明

提示

在看下面的例子前,你需要了解:协议帧中的寄存器地址都是 0x0000 - 0xFFFF,与实际寄存器的地址不一样,就比如 保持寄存器是 30001-39999, 这是因为 Modbus 将协议帧中的地址进行了偏移

寄存器类型Modbus 地址范围协议帧地址范围偏移值
线圈00001 ~ 099990x0000 ~ 0xFFFF0
离散输入10001 ~ 199990x0000 ~ 0xFFFF10000
输入寄存器30001 ~ 399990x0000 ~ 0xFFFF30000
保持寄存器40001 ~ 499990x0000 ~ 0xFFFF40000

公式:Modbus 地址 = 协议帧地址 + 偏移值 + 1

这里暂时先有个概念就行,看了下面的示例后自然会明白

  • 读取寄存器
    • 示例:读取保持寄存器(功能码 0x03),读取寄存器地址范围 40002-40005
    • 请求帧:[01][03][00 01][00 04][2B 14]
      从机地址功能码起始地址读取数量CRC
      [ 01 ][ 03 ][ 00 01 ][ 00 04 ][ 2B 14 ]

      上面我们说了偏移值,就以这个请求帧来说:帧中的地址是 0x0001,因为功能码是 03 所以需要加上 40000 + 1,由此得出实际寄存器地址为 40002

    • 响应帧:[01][03][08][40 27 AE 14 41 C8 0 0][12 71]
      从机地址功能码数据长度数据CRC
      [ 01 ][ 03 ][ 08 ][ 40 27 AE 14 41 C8 0 0 ][ 12 71 ]
  • 写入寄存器(单个)
    • 示例:写入单个保持寄存器(功能码 0x06),写入寄存器地址 40003
    • 请求帧:[01][06][00 02][12 34][08 18]
      从机地址功能码地址数据CRC
      [ 01 ][ 06 ][ 00 02 ][ 12 34 ][ 08 18 ]
    • 响应帧:与请求帧相同
  • 写入寄存器(多个)
    • 示例:写入多个保持寄存器(功能码 0x10),写入寄存器地址范围 40001-40002
    • 请求帧:[01][10][00 00][00 02][04][12 34 56 78][2B 26]
      从机地址功能码起始地址写入数量数据长度数据CRC
      [ 01 ][ 10 ][ 00 00 ][ 00 02 ][ 04 ][ 12 34 56 78 ][ 2B 26 ]
    • 响应帧:与请求帧相同

提示

由于线圈/散离输入寄存器存储单位是一位(1 bit),所以 8 个寄存器才会占用 1 字节,9 个寄存器占用 2 字节...

例:[01][0F][00 00][00 04][01][05][8E 09] 表示写入线圈寄存器 00001-00004, 数据长度 1,写入数据 05(二进制表示 00000101),低位在前,所以寄存器 1=ON, 2=OFF, 3=ON, 4=OFF

写入单个线圈寄存器时,数据使用固定值表示:FF00(ON)0000(OFF)

例:[01][05][00 02][FF 00][8C 3A] 表示写入 ON 到地址 00003,写单个线圈的情况较少,更推荐直接使用 0x0F

注意

你也可能也注意到了,数据域中的 数据长度 是一个字节表示,也就代表最大只能是 255

其实这在 Modbus 标准文档中也规定了每帧长度不可超过 256 个字节,我们在使用时还要考虑到从机地址、功能码等字段占用的字节数, 所以理论读取时(0x03)最大可读取 125((256-6)/2)个寄存器,理论写入(0x10)最大 123((256-9)/2)个寄存器, 需要读取/写入更多寄存器时,我们需要考虑分批操作

时序 / 帧间隔

我们现在知道了交互中数据传输的格式,现在我们来说说如何区分完整的一帧(一次请求/响应)

我们要知道 Modbus RTU 是基于串口通信(UART)的,而在 UART 中传输的是字符流(依次发送从机地址、功能码...),UART 可不会管你传输的是从机地址还是什么数据,它有数据就发送, 你可以想想,如果连续发送多次请求时,两个 Modbus RTU 帧是不是就连在一起了? 而 Modbus RTU 帧中也没有"起始符"与"结束符"来标识一帧的开始与结束,那么我们要怎么来区分呢? 根据时间间隔 !

串口通信(UART)

如果你不知道串口通信(UART),请看这里,我这里就简单讲一下 UART 的传输机制

我们得知道,数据传输的本质是传输二进制(bit 也就是高电平与低电平),这对人类很不友好,我们如果直接通过二进制来做什么事的话就非常的困难, 所以 UART 给我们封装了更高层次的传输方式(以字节为单位),对于我们来说传输单位就是字节,这很棒!那它是怎么实现的呢?

UART 为了确保每字节的数据能够正常解析,所以制定了以下规则:

[起始位][数据位][校验位(可选)][停止位]

  • 起始位(Start Bit): 表示数据开始,固定为 0(低电平)
  • 数据位(Data Bits): 这个是我们实际的数据,通常是 8 位(1 字节),按 从低位到高位(LSB) 传输
  • 校验位(Parity Bit): 这个是可选的,None(无校验)、Odd(奇校验)、Even(偶校验)
  • 停止位(Stop Bit): 表示数据结束,固定为 1(高电平)

有多种模式可以选择,Modbus 通常使用 8N1 模式,也就是数据是 8 位(1 字节)且无校验位

例如:8N1 模式

我们发送数据 0x12(二进制:00010010)时,对于我们来说发送的是 8 位(1 字节)数据,但实际发送的是 10 位, UART 给我们的数据包装了一下,加上了起始位与停止位,0 00010010 1,我们将这个数据称为 1 个字符,这样接收方如果也是 8N1 模式的话就能正常解析数据

Modbus RTU 规定:

一帧内的数据传输间隔时间必须 <= 1.5 个字符的时间,两帧之间的间隔必须 ≥ 3.5 个字符的时间

  • 帧内间隔 ≤ 1.5 字符时间: 如果同一帧内两个字节之间的传输时间间隔超过 1.5 字符时间,那么会认为帧损坏(CRC 校验也会失败),该帧会被丢弃
  • 帧间间隔 ≥ 3.5 字符时间: 如果线路空闲超过 3.5 字符时间,接收方就知道这帧结束了,准备接收下一帧。如果两帧之间小于 3.5 个字符,同样 CRC 校验也会失败

计算公式:1 字符时间 = (1 起始位 + 8 数据位 + 1 停止位 + 校验位) / 波特率

示例(波特率 9600):

T1.5=1596001.56 msT3.5=3596003.65 ms

Modbus ASCII

提示

因为这个协议在市面上很少见,在这里就简单的说明一下,能有个了解就好

Modbus ASCII 最主要的一点就是:它把所有数据(地址、功能码、数据域、校验码)都转换成 ASCII 字符(0-9, A-F),原本 1 个字节数据它用 2 个 ASCII 字符呈现

可以简单的理解为它把原始数据输出成了 16 进制的字符串显示

比如:寄存器值 0x1234(两个字节 0x120x34)→ 转换为 ASCII 字符串 1234(4 个字符,我们知道 ASCII 编码中,一个字符使用一个字节编码,也就是 4 个字节)

帧字段说明

字段长度说明
帧起始符1 字节固定 : ,表示本次请求开始
从机地址2 ASCII 字符目标设备地址(1-247),广播地址为 0
功能码2 ASCII 字符指定操作类型(比如 读保持寄存器、写线圈 等)
数据域N × 2 ASCII 字符包含寄存器地址、数量、实际数据
校验码2 ASCII 字符LRC(纵向冗余检验)
帧结束符2 字节固定 CR LF(回车+换行),表示本次请求结束

从上面的帧字段能看出,和 Modbus RTU 相比,除了主要使用 ASCII 编码外,还多出了两个字段 帧起始符帧结束符,这与 Modbus RTU 的时序不同, Modbus ASCII 使用这两个字段构建一段完整的帧,这就相当于是手动控制一段请求的开始与结束,这就对时序没有要求

其次就是 Modbus ASCII 使用 LRC 进行校验计算,这与 Modbus RTU 的 CRC-16 也不同

Modbus TCP

现在我们来说说 Modbus TCP,故名思意它是基于 TCP/IP 网络的通信协议,默认使用 502 端口, 同样使用二进制编码进行数据传输(与 Modbus RTU 帧结构类似,但是增加了 MBAP 报文头,且去除了 CRC 校验)。 相比串口通信,TCP 速度快、距离远,特别适合 SCADA 系统(数据采集与监控系统)

Modbus TCP 报文格式

alt text

Modbus TCP 的报文由 MBAP 头(前 7 字节) + Modbus PDU(协议数据单元) 组成

  • MBAP 报文头
    • 事务标识(2 字节): request id,用于请求/响应的配对,一般由主机递增生成,从机原样返回
    • 协议标识(2 字节): 固定为 0x0000(表示 Modbus 协议)
    • 字节长度(2 字节): 后续的字节数(从 单元标识 开始计算)
    • 单元标识(1 字节): 设备地址,通常为 0xFF(可忽略),因为设备通过 IP 地址直接寻址
  • Modbus PDU: 参考 Modbus RTU 帧结构
    • 读(请求): 功能码(1 字节) 起始地址(2 字节) 数量(2 字节)
    • 读(响应): 功能码(1 字节) 数据长度(1 字节) 数据
    • 写(请求): 功能码(1 字节) 起始地址(2 字节) 数量(2 字节) 数据长度(1 字节) 数据
    • 写(响应): 功能码(1 字节) 起始地址(2 字节) 数量(2 字节)

提示

Modbus TCP 协议去除了 CRC 校验,因为 TCP 协议本身就是可靠的数据传输

由于是基于 TCP 协议, 所以也没有 Modbus RTU 所有的串行通信的波特率限制以及帧间隔时间

如有转载请标注本站原文地址