Modbus协议是一种已广泛应用于当今工业控制领域的通用通讯协议。通过此协议,控制器相互之间、或控制器经由网络(如以太网)可以和其它设备之间进行通信。MODBUS是一个请求/应答协议,并且提供功能码规定的服务。
Modbus通讯物理接口可以选用串口(包括RS232和RS485),也可以选择以太网口。根据应用通信规程的不同,Modbus协议可分为串行链路上的 MODBUS和TCP/IP 上的 MODBUS;其中,应用于串行链路上的Modbus协议根据传输模式的不同,又可以分为RTU模式和ASCII模式,RTU传输模式凭借其简单、高效得到广泛应用,本文主要介绍RTU模式。
第一部分:Modbus 协议
1 引言
MODBUS是OSI模型第7层上的应用层报文传输协议,它在连接至不同类型总线或网络的设备之间提供客户机/服务器通信。
自从1979年出现工业串行链路的事实标准以来,MODBUS 使成千上万的自动化设备能够通信。目前,继续增加对简单而雅观的MODBUS 结构支持。互联网组织能够使 TCP/IP栈上的保留系统端口502 访问MODBUS。
MODBUS是一个请求/应答协议,并且提供功能码规定的服务。MODBUS功能码是MODBUS请求/应答PDU的元素。本文件的作用是描述MODBUS事务处理框架内使用的功能码。
2 总体协议描述
2.1 协议描述
MODBUS协议定义了一个与基础通信层无关的简单协议数据单元(PDU)。特定总线或网络上的MODBUS协议映射能够在应用数据单元(ADU)上引入一些附加域。
图1 通用MODBUS帧
启动 MODBUS 事务处理的客户机创建 MODBUS 应用数据单元。功能码向服务器指示将执行哪种操作。
MODBUS 协议建立了客户机启动的请求格式。
用一个字节编码 MODBUS 数据单元的功能码域。有效的码字范围是十进制 1-255(128-255 为异常响应保留)。当从客户机向服务器设备发送报文时,功能码域通知服务器执行哪种操作。
向一些功能码加入子功能码来定义多项操作。
从客户机向服务器设备发送的报文数据域包括附加信息,服务器使用这个信息执行功能码定义的操作。这个域还包括离散项目和寄存器地址、处理的项目数量以及域中的实际数据字节数。
在某种请求中,数据域可以是不存在的(0 长度),在此情况下服务器不需要任何附加信息。功能码仅说明操作。
如果在一个正确接收的 MODBUS ADU 中,不出现与请求MODBUS功能有关的差错,那么服务器至客户机的响应数据域包括请求数据。如果出现与请求 MODBUS 功能有关的差错,那么域包括一个异常码,服务器应用能够使用这个域确定下一个执行的操作。
例如,客户机能够读一组离散量输出或输入的开/关状态,或者客户机能够读/写一组寄存器的数
据内容。
当服务器对客户机响应时,它使用功能码域来指示正常(无差错)响应或者出现某种差错(称为异常响应)。对于一个正常响应来说,服务器仅对原始功能码响应。
图2 MODBUS事务处理(无差错)
图3 MODBUS事务处理(异常响应)
☞注释:需要管理超时,以便明确地等待可能不会出现的应答。
串行链路上第一个 MODBUS 执行的长度约束限制了 MODBUS PDU 大小(最大 RS485ADU=256字节)。因此,对串行链路通信来说,MODBUS PDU=256-服务器地址(1 字节)-CRC(2 字节)=253字节。
从而:
RS232/RS485ADU=253字节+服务器地址(1byte)+CRC(2字节)=256字节。
TCP MODBUS ADU = 249 字节+ MBAP (7 字节) = 256 字节。
MODBUS 协议定义了三种PDU。它们是:
●MODBUS 请求PDU,mb_req_pdu
●MODBUS 响应PDU,mb_rsp_pdu
●MODBUS 异常响应PDU,mb_excep_rsp_pdu
2.2 数据编码
MODBUS 使用一个‘big-Endian’表示地址和数据项。这意味着当发送多个字节时,首先发送最高有效位。例如:
16–比特 0x1234 发送的第一字节为 0x12 然后 0x34
2.3 MODBUS数据模型
MODBUS以一系列具有不同特征表格上的数据模型为基础。四个基本表格为:
关于数据模型的理解可以结合下文的具体应用实例,重点注意对象类型是bit还是字节,以及访问类型是只读还是读写。
对于基本表格中任何一项,协议都允许单个地选择 65536 个数据项,而且设计那些项的读写操作可以越过多个连续数据项直到数据大小规格限制,这个数据大小规格限制与事务处理功能码有关。
很显然,必须将通过 MODBUS 处理的所有数据放置在设备应用存储器中。但是,存储器的物理地址不应该与数据参考混淆。要求仅仅是数据参考与物理地址的链接。
MODBUS 功能码中使用的 MODBUS 逻辑参考数字是以 0 开始的无符号整数索引。
3 RTU传输模式
当设备使用 RTU (Remote Terminal Unit) 模式在 Modbus串行链路通信,报文中每个8位字节含有两个4位十六进制字符。这种模式的主要优点是较高的数据密度,在相同的波特率下比 ASCII 模式有更高的吞吐率。每个报文必须以连续的字符流传送。
RTU 模式每个字节 ( 11 位 ) 的格式为 :
编码系统: 8–位二进制
报文中每个8位字节含有两个4位十六进制字符(0–9,A–F)
Bits per Byte: 1 起始位
8 数据位, 首先发送最低有效位
1 位作为奇偶校验
1 停止位
偶校验是要求的,其它模式 ( 奇校验, 无校验 ) 也可以使用。 为了保证与其它产品的最大兼
容性,同时支持无校验模式是建议的。默认校验模式模式 必须为偶校验。
注 : 使用无校验要求 2 个停止位。
字符是如何串行传送的:
每个字符或字节均由此顺序发送(从左到右):
最低有效位 (LSB) . . . 最高有效位 (MSB)
图4 RTU模式位序列
设备配置为奇校验、偶校验或无校验都可以接受。如果无奇偶校验,将传送一个附加的停止位以填充字符帧:
图5 RTU模式位序列(无校验的特殊情况)
帧检验域: 循环冗余校验 (CRC)
帧描述 :
图6 RTU报文帧
→ Modbus RTU 帧最大为 256 字节。
3.1 Modbus 报文 RTU 帧
由发送设备将 Modbus报文构造为带有已知起始和结束标记的帧。这使设备可以在报文的开始接收新帧,并且知道何时报文结束。不完整的报文必须能够被检测到而错误标志必须作为结果被设置。在RTU模式,报文帧由时长至少为3.5个字符时间的空闲间隔区分。在后续的部分,这个时间区间被称作t3.5。
图7 RTU报文帧
整个报文帧必须以连续的字符流发送。如果两个字符之间的空闲间隔大于1.5个字符时间,则报文帧被认为不完整应该被接收节点丢弃。
注 :
RTU接收驱动程序的实现,由于t1.5和t3.5的定时,隐含着大量的对中断的管理。在高通信速率下,这导致CPU负担加重。因此,在通信速率等于或低于19200 Bps时,这两个定时必须严格遵守;对于波特率大于19200Bps的情形,应该使用2个定时的固定值:建议的字符间超时时间(t1.5)为 750µs,帧间的超时时间 (t3.5) 为1.750ms。
3.2 CRC校验
在 RTU 模式包含一个对全部报文内容执行的,基于循环冗余校验 (CRC - Cyclical Redundancy Checking) 算法的错误检验域。CRC 域检验整个报文的内容。不管报文有无奇偶校验,均执行此检验。
CRC 包含由两个 8 位字节组成的一个 16 位值。CRC 域作为报文的最后的域附加在报文之后。计算后,首先附加低字节,然后是高字节。CRC高字节为报文发送的最后一个子节。附加在报文后面的 CRC 的值由发送设备计算。接收设备在接收报文时重新计算 CRC 的值,并将计算结果于实际接收到的 CRC 值相比较。如果两个值不相等,则为错误。CRC 的计算, 开始对一个 16 位寄存器预装全 1。 然后将报文中的连续的 8 位子节对其进行后续的计算。只有字符中的 8 个数据位参与生成 CRC 的运算,起始位,停止位和校验位不参与 CRC计算。
CRC 的生成过程中, 每个 8–位字符与寄存器中的值异或。然后结果向最低有效位(LSB)方向移动(Shift) 1 位,而最高有效位(MSB)位置充零。 然后提取并检查 LSB:如果 LSB 为 1, 则寄存器中的值与一个固定的预置值异或;如果 LSB 为 0, 则不进行异或操作。这个过程将重复直到执行完 8 次移位。完成最后一次(第 8 次)移位及相关操作后,下一个 8位字节与寄存器的当前值异或,然后又同上面描述过的一样重复 8 次。当所有报文中子节都运算之后得到的寄存器忠的最终值,就是 CRC。
当 CRC 附加在报文之后时,首先附加低字节,然后是高字节。Modbus中文版协议在附录 B 含有 CRC 生成的详细示例。
第二部分:Modbus--RTU协议在串行链路上的实现
1 读AI模拟量输入
举例:读取12节单体电压数据,子板地址为1:
Modbus RTU 格式: 《十六进制》
上位机发送:01 04 00 00 00 0C F0 0F
从机应答: 01 04 18 80 00 81 00 82 00 83 00 84 00 85 00 86 00 87 00 88 00 89 00 90 00 91 00 0F 3C
报文详解:
上位机发送的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 04H | 读输入寄存器<READ_INPUT> |
起始寄存器地址 | 2 | 0000H | 输入寄存器地址: 0000H—对应电压采集通道0<30001寄存器> 0001H—对应电压采集通道1<30002寄存器>
。。。。。。。。。 000CH—对应电压采集通道12<30012寄存器> 该寄存器地址位于【3】区 数据发送顺序:高字节在前,如0007H,顺序00 07 |
读取寄存器数量 | 2 | 000CH | 读取12个寄存器里的内容<30001-30012寄存器> 数据发送顺序:高字节在前,如000C,则顺序:00 0C |
CRC校验 | 2 | 0FF0H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
下位机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 04H | 读输入寄存器 <READ_INPUT> |
字节数 | 1 | 18H | 其中: 18H = AI 个数* 2 = 12 * 2 |
返回数据 | 24 | 8000H 8100H ······ 9100H | 0通道,32768 1通道,33024 ······ 11通道,37120 数据发送顺序:高字节在前,如8000,则顺序:80 00 |
CRC校验 | 2 | 3C0FH | 前面所有数据的CRC校验 数据发送顺序:低字节在前,如A050,则顺序:50 A0 |
2 读DI开关量输入
举例:读取8路DI,模块地址=1:
Modbus RTU格式: 《十六进制》
上位机发送:01 02 00 00 00 08 79 CC
从机应答: 01 02 01 02 20 49
举例:读取18路DI,模块地址=1:
Modbus RTU格式: 《十六进制》
上位机发送:01 02 00 00 00 12 F8 07
从机应答: 01 02 03 03 FF FF 89 FE
报文详解:
上位机发送的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 02H | 读离散量输入 <READ_STATE> |
起始寄存器地址 | 2 | 0000H | 0000H-该寄存器对应DI0的开关状态<10001寄存器> 0001H-该寄存器对应DI1的开关状态<10002寄存器> 。。。。。。。。。 0007H-该寄存器对应DI7 的开关状态<10012寄存器> 该寄存器地址位于【1】区 数据发送顺序:高字节在前,如0007H,则顺序:00 07 |
输入数量 | 2 | 0008H | 读取8个开关量输入状态<10001-10008寄存器> 数据发送顺序:高字节在前,如0008,则顺序:00 08 |
CRC校验 | 2 | CC79H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
下位机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址 = 1 |
功能码 | 1 | 02H | 读取寄存器 <READ_STATE> |
返回字节长度 | 1 | 01H | 返回01个字节的开关量输入状态 其中: 01H = 开关量个数 / 8 = 8 / 8
如果个数>8,且<=16,则 01H 应该改为:02H 以此类推个数 |
返回数据 | 1 | 02H | 02H从低位到高位代表DI0-DI7的输入状态 02H即表示:DI1高电平ON,其他低电平OFF 如果个数>8,且<=16, 则字节数=2, 发送报文=0102H, 数据发送顺序为高字节在前: 01 02 01 即表示: DI0-DI7 02 即表示: DI8-DI15 以此类推个数 |
CRC校验 | 2 | 4920H | 前面所有数据的CRC校验 数据发送顺序:低字节在前,如A050,则顺序:50 A0 |
3 写AO单路模拟量输出
举例:模块地址=1:
Modbus RTU 格式: 《十六进制》
主机发送《AO-0 输出十进制 10000》: 01 06 00 00 27 10 93 F6
从机应答《AO-0 输出十进制 10000》: 01 06 00 00 27 10 93 F6
主机发送《AO-1 输出十进制 10000》: 01 06 00 01 27 10 C2 36
从机应答《AO-1 输出十进制 10000》: 01 06 00 01 27 10 C2 36
主机发送《AO-2 输出十进制 10000》: 01 06 00 02 27 10 32 36
从机应答《AO-2 输出十进制 10000》: 01 06 00 02 27 10 32 36
报文详解:
主机发送的报文格式:《设置模拟量输出AO-0通道》
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 06H | 写寄存器 <WRITE_1_HOLD> |
寄存器地址 | 2 | 0000H | 0000H -该寄存器对应通道 0 <40001 寄存器> 。。。。。。 0003H - 该寄存器对应通道 3 <40004 寄存器> 该寄存器地址位于【4】区 数据发送顺序:高字节在前,如 0003,则顺序: 00 03 |
写入数据 | 2 | 2710H | 设置通道 0 的输出值为 10000 |
CRC校验 | 2 | F693H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
从机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 06H | 写寄存器 <WRITE_1_HOLD> |
寄存器地址 | 2 | 0000H | 同“主机发送的报文格式”解析 |
写入数据 | 2 | 2710H | 同“主机发送的报文格式”解析 |
CRC校验 | 2 | F693H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
4 读AO模拟量输出
举例:读取4路AO 数据,模块地址=1:
Modbus RTU 格式: 《十六进制》
主机发送: 01 03 00 00 00 04 44 09
从机应答: 01 03 08 21 E6 3B 95 11 22 00 00 7C 6D
报文详解:
主机发送的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 03H | 读取寄存器 <READ_HOLD> |
起始寄存器地址 | 2 | 0000H | 0000H -该寄存器对应通道AO0<40001 寄存器> 。。。。。。 0003H - 该寄存器对应通道AO3 <40004 寄存器> 该寄存器地址位于【4】区 数据发送顺序:高字节在前,如 0003,则顺序: 00 03 |
读取寄存器数量 | 2 | 0004H | 读取4个寄存器里的内容 <40001-40004 寄存器> 数据发送顺序:高字节在前,如0004,则顺序:00 04 |
CRC校验 | 2 | 0944H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
从机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 03H | 读取寄存器 <READ_HOLD> |
返回数据长度 | 1 | 08H | 其中: 08H = AO个数* 2 = 4 * 2 |
返回数据 | 8 | 21E6H 。。。。。。 0000H | 0 通道, 8678 。。。。。。 3 通道, 0000 数据发送顺序:高字节在前,如21E6,则顺序:21 E6 |
CRC校验 | 2 | 6D7CH | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
5 写DO单路开关量输出(例如继电器控制)
举例:模块地址=1:
Modbus RTU 格式: 《十六进制》
主机发送《DO-0 闭合》: 01 05 00 00 FF 00 8C 3A 《Modbus RTU》
从机应答《DO-0 闭合》: 01 05 00 00 FF 00 8C 3A 《Modbus RTU》
主机发送《DO-0 断开》: 01 05 00 00 00 00 CD CA 《Modbus RTU》
从机应答《DO-0 断开》: 01 05 00 00 00 00 CD CA 《Modbus RTU》
主机发送《DO-1 闭合》: 01 05 00 01 FF 00 DD FA 《Modbus RTU》
从机应答《DO-1 闭合》: 01 05 00 01 FF 00 DD FA 《Modbus RTU》
主机发送《DO-1 断开》: 01 05 00 01 00 00 9C 0A 《Modbus RTU》
从机应答《DO-1 断开》: 01 05 00 01 00 00 9C 0A 《Modbus RTU》
报文详解:
主机发送的报文格式: 《设置 DO_0 闭合,模块地址=1:》
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 05H | 写寄存器 <WRITE_1_COIL> |
起始寄存器地址 | 2 | 0000H | 寄存器地址: 0000H -开关量输出通道0的开关状态<00001寄存器> 。。。 。。。 0007H -开关量输出通道7的开关状态<00008寄存器> 该寄存器地址位于【0】区 数据发送顺序:高字节在前,如 0007,则顺序: 00 07 |
写入数据 | 2 | FF00H | 将 FF00H 写入0000H 寄存器中 FF00H:表示 DO 闭合 0000H:表示 DO 断开 数据发送顺序:高字节在前,如FF00,则顺序: FF 00 |
CRC校验 | 2 | 3A8C | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
从机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 05H | 写寄存器 <WRITE_1_COIL> |
起始寄存器地址 | 2 | 0000H | 同“主机发送的报文格式”解析 |
写入数据 | 2 | FF00H | 同“主机发送的报文格式”解析 |
CRC校验 | 2 | 3A8C | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
6 读DO开关量输出(例如读取继电器状态)
举例:读取8路DO状态,模块地址=1:
Modbus RTU 格式: 《十六进制》
主机发送: 01 01 00 00 00 08 3D CC
从机应答: 01 01 01 03 11 89
举例:读取 12 路 DO 状态,模块地址=7:
Modbus RTU 格式: 《十六进制》
主机发送: 07 01 00 00 00 0C 3C 69
从机应答: 07 01 02 03 FF 71 4C
报文详解:
主机发送的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 01H | 读取寄存器 <READ_COIL> |
起始寄存器地址 | 2 | 0000H | 0000H –开关量输出通道0状态<00001 寄存器> 。。。 。。。 0007H -开关量输出通道7状态<00008 寄存器> 该寄存器地址位于【0】区 数据发送顺序:高字节在前,如0007,则顺序:00 07 |
读取线圈数量 | 2 | 0008H | 读取 8 个寄存器里的内容 <00001-00008 寄存器> 数据发送顺序:高字节在前,如 0008,则顺序: 00 08 |
CRC校验 | 2 | CC3D | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
从机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 01H | 读取开关量输出 <READ_COIL> |
返回数据长度 | 1 | 01H | 其中: 01H = 通道个数 / 8 = 8 / 8 如果个数>8,且<=16,则 01H 应该改为: 02H 以此类推个数 |
返回数据 | 1 | 03H | 03H 从低位到高位代表 DO0-DO7 的开关状态 03H 即表示:DO0、DO1闭合ON, DO2-7断开OFF 如果个数>8,且<=16, 则字节数=2, 发送报文=0302H, 数据发送顺序为高字节在前: 03 02 03 即表示: DO0-DO7 02 即表示: DO8-DO15 以此类推个数 |
CRC校验 | 2 | 8911H | 前面所有数据的CRC校验 数据发送:低字节在前,如A050,则顺序:50 A0 |
7 读AD模块采集的温度数值
举例:读取 8 路 PT100 数据,模块地址=1:
Modbus RTU 格式: 《十六进制》
主机发送: 01 04 00 00 00 08 F1 CC
从机应答: 01 04 10 A0 B0 A1 B1 A2 B2 A3 B3 A4 B4 A5 B5 A6 B6 A7 B7 E2 B0
报文详解:
上位机发送的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 04H | 读取寄存器 <READ_INPUT> |
起始寄存器地址 | 2 | 0000H | 输入寄存器地址: 0000H—对应电压采集通道0<30001寄存器> 。。。。。。。。。 0007H—对应电压采集通道12<30012寄存器> 该寄存器地址位于【3】区 数据发送顺序:高字节在前,如0007H,顺序00 07 |
读取寄存器数量 | 2 | 0008H | 读取8个寄存器里的内容<30001-30008寄存器> 数据发送顺序:高字节在前,如0008,则顺序: 00 08 |
CRC校验 | 2 | CCF1H | 前面所有数据的CRC校验 数据发送:低字节在前,如CCF1,则顺序:F1 CC |
下位机应答的报文格式:
发送内容 | 字节数 | 发送报文 | 备注 |
模块地址 | 1 | 01H | 模块地址=1 |
功能码 | 1 | 04H | 读取寄存器 <READ_INPUT> |
返回数据长度 | 1 | 10H | 其中:10H = 通道个数* 2 = 8 * 2 |
返回数据 | 16 | 21E6H 3B95H 。。。。。。 0000H | 0 通道,8678 1 通道,15253 。。。。。。 7 通道, 0000 数据发送顺序:高字节在前,如21E6,则顺序:21 E6 |
CRC校验 | 2 | B0E2H | 前面所有数据的CRC校验 数据发送:低字节在前,如B0E2,则顺序: E2 B0 |
附录说明:
本文档仅对Modbus-RTU协议在串行链路上的使用做简单说明,文中对于Modbus协议各个阶段的错误处理并未涉及,Modbus中文版协议对于各个错误状态的反馈报文有较为详细的解释,有需要可以参阅。