Modbus是通过主从关系实现的请求-响应协议。在主从关系中,通信总是成对发生(一个设备必须发起请求,然后等待响应),并且发起请求的设备(主设备)负责发起每次交互。通常,主设备是人机界面(HMI)或者监控和数据采集(SCADA)系统,而从设备是传感器、可编程逻辑控制器(PLC)或可编程自动化控制器(PAC)。这些请求和响应的内容以及发送这些消息的网络层由协议的不同层来定义。
在最初的做法中,Modbus是建立在串行端口之上的单一协议,因此它不能被分成多个层。随着时间的推移,该协议引入了不同的应用数据单元,来更改串行通信所用的数据包格式,或允许使用TCP/IP和用户数据报协议(UDP)网络。这样便实现了核心协议和网络层的分离,前者用于定义协议数据单元(PDU),后者用于定义应用数据单元(ADU)。
PDU及其处理代码构成了Modbus应用协议规范的核心。该规范定义了PDU的格式、协议使用的各种数据概念、如何使用功能码访问数据,以及每个功能码的具体实现和限制。
Modbus PDU格式可定义为功能码,后跟一组关联数据。该数据的大小和内容由功能码定义,整个PDU(功能码和数据)的大小不能超过253个字节。每个功能码都有一个特定的行为,从设备可以根据所需的应用程序行为灵活地实现这些行为。PDU规范定义了数据访问和操作的核心概念;但是,从设备可能会以规范中未明确定义的方式处理数据。
一般来说,Modbus可访问的数据存储在以下4个数据库或地址范围中的其中一个:线圈、离散量输入、保持寄存器和输入寄存器。与许多规范一样,这些名称可能因行业或应用而异。例如,保持寄存器可能称为输出寄存器,线圈可能称为数字或离散量输出。这些数据库定义了所包含数据的类型和访问权限。从设备可以直接访问这些数据,因为这些数据由设备本地托管。Modbus可访问的数据通常是设备主存的一个子集。相反,Modbus主设备必须通过各种功能码请求访问这些数据。表1中描述了每个区块的行为。
内存区块 | 数据类型 | 主设备访问 | 从设备访问 |
线圈 | 布尔 | 读取/写入 | 读取/写入 |
离散量输入 | 布尔 | 只读 | 读取/写入 |
保持寄存器 | 无符号双字节整型 | 读取/写入 | 读取/写入 |
输入寄存器 | 无符号双字节整型 | 只读 | 读取/写入 |
表1.Modbus数据模型区块
通过这些区块,您可以限制或允许访问不同的数据元素,还可以在应用层提供简化的机制来访问不同的数据类型。
这些区块是完全概念性的。它们可能作为独立的内存地址存在于给定的系统中,但也可能重叠。例如,线圈1可能存在于与保持寄存器1所代表的字的第一位相同的内存中。寻址方案完全由从设备定义,其对每个内存区块的解释是设备数据模型的重要组成部分。
该规范将每个区块定义为包含多达65,536 (216)个元素的地址空间。在PDU的定义中,Modbus定义了每个数据元素的地址,范围是从0到65,535。然而,每个数据元素的编号从1到n,其中n的最大值为65,536。也就是说,线圈1位于地址0的线圈区块中,而保持寄存器54位于从设备定义为保持寄存器的内存部分中的地址53。
规范允许的全部范围不需要给定设备实现。例如,设备可能会选择不执行线圈、离散量输入或输入寄存器,而只使用保持寄存器150至175和200至225。这是完全可以接受的,而且可以通过例外来处理无效的访问尝试。
虽然规范将不同的数据类型定义为存在于不同的区块中,并为每种类型分配一个本地地址范围,但这并不一定能转化为用于记录或了解给定设备的Modbus可访问内存的直观寻址方案。为了简化对内存区块位置的理解,我们引入了一种编号方案,即向所讨论数据的地址中添加前缀。
例如,在设备手册中,数据项不会表示为位于地址13的保持寄存器14,而是表示为位于地址4,014、40,014或400,014的数据项。在这几种情况中,第一个数字都是4,表示保持寄存器,其余数字则用于指定地址。4XXX、4XXXX和4XXXXX的区别取决于设备所用的地址空间。如果65,536个寄存器全部都在使用,应该使用4XXXXX符号,因为它支持400,001到465,536的范围。如果只有几个寄存器在使用,通常的做法是使用4,001到4,999的范围。
在这种寻址方案中,每种数据类型都被分配了一个前缀,如表2所示。
数据区块 | 前缀 |
线圈 | 0 |
离散量输入 | 1 |
输入寄存器 | 3 |
保持寄存器 | 4 |
表2.数据范围前缀
线圈的前缀为0,这意味着4001的引用可能是指保持寄存器1或线圈4001。出于这个原因,建议所有的新实现都使用带前导零的6位数寻址,并在记录时注明这一点。因此,保持寄存器1的地址为400,001,而线圈4001的地址则为004,001。
内存地址和引用编号之间的差异会因给定应用程序选择的索引而进一步复杂化。如前所述,保持寄存器1位于地址0。通常,引用编号索引从1开始,这意味着给定范围的起始值为1。因此,400,001即表示位于地址0的保持寄存器00001。一些实现选择以0作为范围起始值,即400,000表示位于地址0的保持寄存器。表3展示了这个概念。
地址 | 寄存器编号 | 编号(索引从1开始,标准) | 编号(索引从0开始,备选) |
0 | 1 | 400001 | 400000 |
1 | 2 | 400002 | 400001 |
2 | 3 | 400003 | 400002 |
表3.寄存器索引方案
从1开始的索引范围应用较为广泛,强烈建议采用这种方案。无论哪种情况,每个范围的起始值都应在记录时注明。
Modbus标准提供了一个相对简单的数据模型,该模型不包含除无符号双字节整型和位值之外的其他数据类型。如果系统的位值对应于螺线管和继电器,并且双字节整型值对应于未缩放的ADC值,上述模型便已足够;但对于更高级的系统,则无法满足需求。因此,许多Modbus实现都包含跨寄存器边界的数据类型。NI LabVIEW数据记录和监控(DSC)模块以及KEPServerEX都定义了许多引用类型。例如,存储在保持寄存器中的字符串应遵循标准格式(400,001),但后跟一个十进制数、长度和字符串的字节序(400,001.2H是指保持寄存器1中包含两个字符的字符串,其中高位字节对应到字符串的第一个字符)。这是必需的,因为每个请求的大小都是有限的,所以Modbus主设备必须知道字符串的确切范围,而不是搜索长度或分隔符(如NULL)。
除了允许访问跨寄存器边界的数据之外,一些Modbus主设备还支持对寄存器中各个位的引用。由于允许设备将相同内存范围内的每种类型的数据组合在一起,而不必将二进制数据分成线圈和离散量输入范围,因此该功能非常有益。通常使用小数点和位索引或编号进行索引,具体取决于如何实现。也就是说,第一个寄存器的第一位可能是400,001.00或400,001.01。建议所有文档均说明所使用的索引方案。
通过将数据拆分到两个寄存器,多寄存器数据(如单精度浮点值)可以轻松地通过Modbus进行传输。由于这不是由标准定义的,因此此类拆分的字节序未作规定。尽管每个无符号双字节整型必须以网络(大端)字节序发送才能满足标准,但许多设备会颠倒多字节数据的字节序。图2所示的范例虽然不太常见,但有效地展示了这一观点。
图2.多字数据的字节序交换
主设备需要了解从设备如何将信息存储在内存中,然后才能对其进行正确解码。建议文档写明系统所使用的字序。如果在实现过程中需要更加灵活,也可以将字节序添加为系统配置选项,该选项可提供基础的编码和解码功能。
字符串可以轻松地存储在Modbus寄存器中。为了简单起见,某些实现方法要求字符串长度为2的倍数,并使用空值来填充额外的空间。字节序也是字符串交互中的一个变量。字符串格式可能包含也可能不包含NULL(作为最终值)。举个例子,一些设备的数据存储方法可能如图3所示。
图3.Modbus字符串中的字节序反转
数据模型可能因设备而异,但功能码则与之不同,功能码及其数据由标准明确定义。每个功能都遵循一种模式。首先,从设备会验证功能码、数据地址和数据范围等输入。然后,执行所请求的操作并发送与代码相符的响应。如果此过程中的任何步骤失败,则会向请求程序返回异常。这些请求的数据传输就称为PDU。
PDU由一个单字节的功能码组成,后跟多达252字节的功能特定数据。
功能码是第一个需要验证的项。如果用于接收请求的设备未识别功能码,则会返回异常。如果功能码被接受,则从设备会根据功能定义开始分解数据。
由于数据包大小限制为253字节,因此设备可传输的数据量有限。最常见的功能码可以在从数据模型中传输240到250字节的实际数据,具体取决于代码。
正如由数据模型所定义,不同的功能会访问不同的概念数据块。一种常见的做法是让代码访问静态内存位置,但其他行为仍然可用。例如,功能码1(读取线圈)和3(读取保持寄存器)可以访问内存中相同的物理位置。而功能码3(读取保持寄存器)和16(写入保持寄存器)可以访问内存中完全不同的位置。因此,建议在定义从数据模型时考虑每个功能码的执行情况。
无论执行的是何种实际行为,所有从设备都应遵循每个请求的简单状态图。图5是功能码1(读取线圈)的状态图范例。
图5.Modbus协议规范定义的读取线圈状态图
每个从设备必须验证功能码、输入数量、起始地址、总范围以及实际进行读取行为的从属定义功能的执行。
尽管上面的状态图包含了静态地址范围,但真实系统的需求可能会导致静态地址范围与所定义编号有所不同。在某些情况下,从设备无法传输协议所定义的最大字节数。也就是说,如果主设备请求0x07D0输入,从设备只能用0x0400进行响应。同样,从数据模型能够将可接受线圈值的范围定义为地址0到500。如果主设备从地址0开始请求125,则没有问题,但如果主设备从地址400开始发出相同的请求,最后一个线圈将位于地址525,这无疑超出了该设备的范围,因而会出现状态图所定义的异常02。
规范中列出了每个标准功能码的定义。即使对于最常见的功能码,主设备上启用的功能与从设备可处理的功能之间也会出现不匹配的情况,这种情况无法避免。为了解决这个问题,Modbus TCP规范的早期版本定义了三个一致性类。官方的Modbus一致性测试规范虽未引用这些类,而是在每个功能的基础上定义一致性,但这些内容仍然便于理解。建议任何文档都遵循测试规范,并根据所支持的代码(而不是传统分类)来定义其一致性。
0类代码通常被认为是有效Modbus设备的最低配置,因为此类代码可使主设备能够读取或写入数据模型。
代码 | 说明 |
3 | 读取多寄存器 |
16 | 写入多寄存器 |
表4.0类代码一致性规范
1类功能码由访问所有类型的数据模型所需的其他代码组成。在原始定义中,此列表包含功能码7(读取异常)。但是,当前规范规定此代码为仅限于串行的代码。
代码 | 说明 |
1 | 读取线圈 |
2 | 读取离散量输入 |
4 | 读取输入寄存器 |
5 | 写入单个线圈 |
6 | 写入单个寄存器 |
7 | 读取异常状态(仅限串行) |
表5.1类代码一致性规范
2类功能码表示不太常用但更为专业化的功能。例如,读取/写入多个寄存器可能有助于减少请求-响应周期的总数,但该行为仍可用0类代码实现。
代码 | 说明 |
15 | 写入多个线圈 |
20 | 读取文件记录 |
21 | 写入文件记录 |
22 | 屏蔽写入寄存器 |
23 | 读取/写入多个寄存器 |
24 | 读取FIFO |
表6.2类代码一致性规范
Modbus封装接口(MEI)代码(即功能43)用于封装Modbus数据包内的其他数据。目前,提供了两个MEI编号,即13 (CANopen)和14(设备识别)。
功能43/14(设备识别)非常有用,因为它允许传输多达256个唯一的对象。其中一些对象已预定义并预留,例如供应商名称和产品代码,但应用程序可以将其他对象定义为作为通用数据集传输。
此类代码并不常用。
从设备使用异常来指示各种不良状况,比如错误请求或不正确输入。但是,异常也可以作为对无效请求的应用程序级响应。从设备不会响应发出异常的请求,而是忽略不完整或损坏的请求,并开始等待新的消息传入。
异常以定义好的数据包格式报告给用户。首先,将功能码返回给等同于原始功能码的请求主设备,设置最高有效位的情况除外。这等同于为原始功能码的值加上0x80。异常响应包括一个异常码,用于代替与给定功能响应相关的正常数据。
根据标准,四个最常见的异常码是01、02、03和04。表7中显示了这些代码,并附有每个功能的标准含义。
异常码 | 含义 |
01 | 不支持接收的功能码。要确认原始功能码,请从返回值中减去0x80。 |
02 | 请求尝试访问的地址无效。根据标准,只有在起始地址和所请求值的编号超过216时才会发生这种情况。但是,有些设备可能会限制其数据模型中的地址空间。 |
03 | 请求包含不正确的数据。在某些情况下,这意味着参数不匹配,例如所发送寄存器的数量与“字节总数”字段之间的参数不匹配。更常见的情况是,主设备请求的数据高于从设备或协议所允许的上限。例如,主设备一次只能读取125个保持寄存器,而资源受限的设备可能会将此值限制为更少的寄存器。 |
04 | 尝试处理请求时发生不可恢复的错误。这是一个常见异常码,表示请求有效,但从设备无法执行该请求。 |
表7.常见的Modbus异常码
每个功能码的状态图至少应包含异常码01,通常包含异常码04、02、03,并且任何其他定义的异常码都是可选的。
除了Modbus协议的PDU核心所定义的功能外,您还可以使用多种网络协议。最常见的协议是串行和TCP/IP,但也可以使用UDP等其他协议。为了在这些层之间传输Modbus所需的数据,Modbus包含一组专为每种网络协议量身定制的ADU。
Modbus需要特定的功能来提供可靠的通信。每种ADU格式都需要使用单元ID或地址,以便为应用层提供路由信息。每个ADU都带有一个完整的PDU,其中包含给定请求的功能码和相关数据。为了保证可靠性,每条消息都包含错误检查信息。最后,所有的ADU都提供了一种机制来确定请求帧的开始和结束,但实现方式各不相同。
ADU的三种标准格式分别是TCP、远程终端单元(RTU)和ASCII。RTU和ASCII ADU通常用于串行线路,而TCP则用于现代TCP/IP或UDP/IP网络。
TCP ADU由Modbus应用协议(MBAP)报文头和Modbus PDU组成。MBAP是一个通用的报文头,依赖于可靠的网络层。此ADU的格式(包括报文头)如图6所示。
报文头的数据字段代表其用途。首先,它包含一个事务处理标识符。这有助于网络允许同时发生多个未处理的请求。也就是说,主设备可以发送请求1、2和3。在稍后的时间点,从设备能以2、1、3的顺序进行响应,并且主设备可以将请求匹配到响应并准确解析数据。这对于以太网网络来说很有用。
协议标识符通常为零,但您可以使用它来扩展协议的行为。协议使用长度字段来描述数据包其余部分的长度。此元素的位置也表明了这个报文头格式在可靠网络层上的依赖关系。由于TCP数据包具有内置的错误检查功能,并可确保数据一致性和传递,因此数据包长度可位于报文头的任何位置。在可靠性较差的网络上(比如串行网络),数据包可能会丢失,其影响是即使应用程序读取的数据流包含有效的事务处理和协议信息,长度信息的损坏也会使报文头无效。TCP为这种情况提供了适当的保护。
TCP/IP设备通常不使用单元ID。但是,Modbus是一种常见的协议,因此通常会开发许多网关来将Modbus协议转换为其他协议。在最初的预期应用中,Modbus TCP/IP转串行网关用于连接新的TCP/IP网络与旧的串行网络。在这种环境中,单元ID用于确定PDU实际对应的从设备的地址。
最后,ADU还包含一个PDU。对于标准协议,PDU的长度仍限制为253字节。
RTU ADU看起来要简单得多,如图7所示。
与较为复杂的TCP/IP ADU不同,除了核心PDU之外,RTU ADU仅包含两条信息。首先,地址用于定义PDU对应的从设备。在大多数网络中,地址0定义的是“广播”地址。也就是说,主设备可以将输出命令发送到地址0,而所有从设备都应处理该请求,但是不做出任何响应。除了此地址外,CRC还用于确保数据的完整性。
然而,现在的实现机制远没有那么简单。数据包的首尾各有一段静默时间,即总线上没有通信的时段。对于9,600的波特率,此间隔大约是4毫秒。标准定义了一个最小的静默时长,无论波特率如何,都低于2毫秒。
首先,这存在性能缺陷,因为设备必须等待空闲时间结束后才能处理数据包。然而,更危险的是串行传输引入了不同的技术,并且波特率比标准更快。例如,使用USB/串口转换器电缆,您无法控制数据分包和数据传输。测试表明,结合NI-VISA驱动程序使用USB转串口线缆时,会在数据流中引入尺寸可变的大间隙,而这些间隙(静默时段)会“诱骗”符合规范的代码相信消息是完整的。由于消息不完整,通常会导致CRC无效,并导致设备将ADU解释为损坏。
除了传输问题之外,现代驱动程序技术大幅实现串行通信抽象化,并且通常需要应用程序代码中的轮询机制。例如,除非通过轮询端口上的字节,.NET Framework 4.5 SerialPort Class和NI-VISA驱动程序都不提供用于检测串行线路上的静默时段的机制。这会导致性能降低(如果轮询执行过慢)或CPU使用率过高(如果轮询执行过快)。
解决这些问题的常用方法是打破Modbus PDU和网络层之间的抽象层。也就是说,串行代码会询问Modbus PDU数据包以确定功能码。结合数据包中的其他数据,可以发现剩余数据包的长度,从而确定数据包的结尾。了解这些信息后,可以设置更长的超时时间,即使出现传输间隙也可应对,并且应用程序级的轮询速度也会变得更慢一些。建议针对新的开发使用这种机制。如果不为代码采用这种机制,可能会遇到大于预期数量的“损坏”数据包。
如图8所示,ASCII ADU比RTU更复杂,但也避免了RTU数据包的许多问题。然而,它自身也有一些缺点。
为了解决确定数据包大小的问题,ASCII ADU为每个数据包定义了一个明确且唯一的开始和结束。换而言之,每个数据包以“:”开始,并以回车(CR)和换行符(LF)结束。另外,像NI-VISA和.NET Framework SerialPort Class这样的串行API可以轻松读取缓冲区中的数据,直到收到CR/LF等特定字符为止。这些特性有助于在现代应用程序代码中有效地处理串行线路上的数据流。
ASCII ADU的缺点是所有数据都以ASCII编码的十六进制字符进行传输。也就是说,设备针对功能码3 (0x03)发送的不是单个字节,而是发送ASCII字符“0”和“3”或0x30/0x33。这使得协议更具可读性,但也意味着必须通过串行网络传输两倍的数据,并且发送和接收应用程序必须能够解析ASCII值。
Modbus是一种相对简单和开放的标准,可以进行修改以适应给定应用的需求。这常用于HMI和PLC或PAC之间的通信,因为在这种情况下组织可以控制协议的首尾。例如,传感器的开发人员更可能遵守书面标准,因为他们通常只控制其从设备的实现,互通性也是可能实现的。
一般来说,不建议修改协议。本节仅作为对其他人用来调整协议行为的机制的确认。
Modbus标准定义了一些功能码,但也允许您开发更多的功能码。具体而言,功能码1至64、73至99以及111至127是预留且保证唯一的公共代码。其余代码(即65至72和100至110)可由用户自定义。使用这些用户定义的代码时,您可以使用任何数据结构。数据甚至可能超过Modbus PDU的标准253字节限制,但应验证整个应用程序以确保其他层在PDU超过标准限制时按预期工作。高于127的功能码预留为异常响应。
除了串行和TCP之外,Modbus还可以在许多网络层上运行。一种可能的实现是UDP,因为UDP适合于Modbus通信风格。Modbus本质上是基于消息的协议,因此UDP能够发送明确定义的信息包,而不需要任何额外的应用程序级信息(如起始字符或长度),这使得Modbus非常易于实现。Modbus PDU数据包可以使用标准的UDP API发送,并由另一端完全接收,而不需要额外的ADU或重新使用现有的ADU。虽然TCP内置确认系统且对某些协议有利,但Modbus是在应用层执行确认。因此,以这种方式使用UDP会消除TCP ADU中的事务处理标识符字段,从而消除了存在多个同时发生的未完成事务的可能性。因而,主设备必须是同步主设备,或者UDP数据包必须有一个标识符,用于帮助主设备组织请求和响应。建议的做法是在UDP网络层上使用TCP/IP ADU。
最后,应用程序可以选择修改ADU,或使用现有ADU的未使用部分(如TCP)。例如,TCP定义了一个16位长度字段、一个16位协议和一个8位单元ID。鉴于最大的Modbus PDU是253字节,长度字段的高字节始终为零。对于Modbus/TCP,协议字段和单元ID始终为零。有一种简单的协议扩展方式是,通过将协议字段更改为非零数字并使用两个未使用的字节(单元ID和长度字段的高字节),发送两个附加PDU的长度,从而同时发送三个数据包(见图9)。