简单的理解一下Modbus TCP/IP协议的内容,就是去掉了modbus协议本身的CRC校验,增加了MBAP 报文头。TCP/IP上的MODBUS的请求/响应如下图所示:
首先来看一下,MBAP 报文头都包括了哪些信息和内容
域 | 长度 | 描述 | 客户机 | 服务器 |
---|---|---|---|---|
事务元标识符 | 2个字节 | MODBUS请求/响应事务处理的识别码 | 客户机启动 | 服务器从接收的请求中重新复制 |
协议标识符 | 2个字节 | 0=MODBUS协议 | 客户机启动 | 服务器从接收的请求中重新复制 |
长度 | 2个字节 | 以下字节的数量 | 客户机启动(请求) | 服务器(响应)启动 |
单元标识符 | 1个字节 | 串行链路或其它总线上连接的远程从站的识别码 | 客户机启动 | 服务器从接收的请求中重新复制 |
事务元标识符(2个字节):用于事务处理配对。在响应中,MODBUS服务器复制请求的事务处理标识符。这里在以太网传输中存在一个问题,就是先发后至,我们可以利用这个事务处理标识符做一个TCP序列号,来防止这种情况所造成的数据收发错乱(这里我们先不讨论这种情况,这个事务处理标识符我们统一使用0x00,0x01)
协议标识符(2个字节):modbus协议标识符为0x00,0x00
长度(2个字节):长度域是下一个域的字节数,包括单元标识符和数据域。
单元标识符(1个字节):该设备的编号。(可以使用PLC的IP地址标识)。
在MODBUS MODBUS+串行链路子网中对设备进行寻址时,这个域是标识设备地址。在这种情况下,“Unit Identifier”携带一个远端设备的MODBUS从站地址:
在收到来自用户应用的需求后,客户端必须生成一个MODBUS请求,并发送到TCP管理。下表显示MODBUS请求ADU编码:
类型 | 描述 | 字节大小 | 实例 |
---|---|---|---|
MBAP报文头 | 事务处理标识符Hi | 1 | 0x15 |
事务处理标识符Lo | 1 | 0x01 | |
协议标识符 | 2 | 0x0000 | |
长度 | 2 | 0x0006 | |
单元标识符 | 1 | 0xFF | |
MODBUS请求 | 功能码 | 1 | 0x03 |
起始地址 | 2 | 0x0005 | |
寄存器数量 | 2 | 0x0001 |
一旦处理请求,MODBUS 服务器必须使用适当的MODBUS服务器事务处理生成一个响应,并且必须将响应发送到TCP管理组件。
根据处理结果,可以生成两类响应:
异常码 | MODBUS名称 | 备注 |
---|---|---|
01 | 非法的功能码 | 服务器不了解功能码 |
02 | 非法的数据地址 | 与请求有关 |
03 | 非法的数据值 | 与请求有关 |
04 | 服务器故障 | 在执行过程中,服务器故障 |
05 | 确认 | 服务器接受服务调用,但是需要相对长的时间完成服务。因此,服务器仅返回一个服务调用接收的确认。 |
06 | 服务器繁忙 | 服务器不能接受MODBUS请求PDU。客户应用由责任决定是否和何时重发请求。 |
0A | 网关故障 | 网关路经是无效的。 |
0B | 网关故障 | 目标设备没有响应。网关生成这个异常信息。 |
通过上面的两步,一个Modbus TCP的客户端连接已经建立起来了,下面我们就来分析Modbus TCP协议的具体内容与实现方式了。
基本表格 | 对象类型 | 访问类型 | 内容 |
离散量输入 | 单个比特 | 只读 | I/O系统提供这种类型数据 |
线圈 | 单个比特 | 读写 | 通过应用程序改变这种类型数据 |
输入寄存器 | 16比特字 | 只读 | I/O系统提供这种类型数据 |
保持寄存器 | 16比特字 | 读写 | 通过应用程序改变这种类型数据 |
功能码 | |||||
码 | 子码 | 十六进制 | |||
比特访问 | |||||
物理离散量输入 | 读输入离散量 | 02 | 02 | ||
内部比特或物理线圈 | 读线圈 | 01 | 01 | ||
写单个线圈 | 05 | 05 | |||
写多个线圈 | 15 | 0F | |||
16比特访问 | 输入寄存器 | 读输入寄存器 | 04 | 04 | |
内部存储器或物理输出存储器 | 读多个寄存器 | 03 | 03 | ||
写单个寄存器 | 06 | 06 | |||
写多个寄存器 | 16 | 10 | |||
读/写多个寄存器 | 23 | 17 | |||
屏蔽写寄存器 | 22 | 16 | |||
文件记录访问 | 读文件记录 | 20 | 6 | 14 | |
写文件记录 | 21 | 6 | 15 |
我们直接用实例来说明。
在一个远程设备中,使用该功能码读取线圈的1 至2000 连续状态。请求PDU 详细说明了起始地址,即指定的第一个线圈地址和线圈编号。从零开始寻址线圈。因此寻址线圈1-16 为0-15。
根据数据域的每个比特将响应报文中的线圈分成为一个线圈。指示状态为1= ON 和0= OFF。
第一个数据字节的LSB(最低有效位)包括在询问中寻址的输出。其它线圈依次类推,一直到这个字节的高位端为止,并在后续字节中从低位到高位的顺序。
如果返回的输出数量不是八的倍数,将用零填充最后数据字节中的剩余比特(一直到字节的高位端)。字节数量域说明了数据的完整字节数。
请求与响应格式
请求PDU
功能码 | 1个字节 | 0x01 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
线圈数量 | 2个字节 | 1至2000(0x7D0) |
响应PDU
功能码 | 1个字节 | 0x01 |
---|---|---|
字节数 | 1个字节 | N* |
线圈状态 | N个字节 | n=N或N+1 |
注:*N=输出数量/8,如果余数不等于0,那么N=N+1
错误
差错码 | 1个字节 | 功能码+0x80 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求读离散量输出20-38 的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 01 | 功能 | 01 |
起始地址Hi | 00 | 字节数 | 03 |
起始地址Lo | 13 | 输出状态27-20 | CD |
输出数量Hi | 00 | 输出状态35-28 | 6B |
输出数量Lo | 13 | 输出状态38-36 | 05 |
将输出27-20 的状态表示为十六进制字节值CD,或二进制1100 1101。输出27 是这个字节的MSB,输出20 是LSB。
通常,将一个字节内的比特表示为MSB 位于左侧,LSB 位于右侧。第一字节的输出从左至右为27至20。下一个字节的输出从左到右为35至28。当串行发射比特时,从LSB向MSB传输:20 . . .27、28 . . . 35 等等。
在最后的数据字节中,将输出状态38-36表示为十六进制字节值05,或二进制0000 0101。输出38 是左侧第六个比特位置,输出36 是这个字节的LSB。用零填充五个剩余高位比特。
注:用零填充五个剩余比特(一直到高位端)。
在一个远程设备中,使用该功能码读取离散量输入的1 至2000 连续状态。请求PDU 详细说明了起始地址,即指定的第一个输入地址和输入编号。从零开始寻址输入。因此寻址输入1-16 为0-15。
根据数据域的每个比特将响应报文中的离散量输入分成为一个输入。指示状态为1= ON 和0=OFF。第一个数据字节的LSB(最低有效位)包括在询问中寻址的输入。其它输入依次类推,一直
到这个字节的高位端为止,并在后续字节中从低位到高位的顺序。
如果返回的输入数量不是八的倍数,将用零填充最后数据字节中的剩余比特(一直到字节的高位端)。字节数量域说明了数据的完整字节数。
请求PDU
功能码 | 1个字节 | 0x02 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
线圈数量 | 2个字节 | 1至2000(0x7D0) |
响应PDU
功能码 | 1个字节 | 0x82 |
---|---|---|
字节数 | 1个字节 | N* |
线圈状态 | N*x1个字节 |
注:*N=输出数量/8,如果余数不等于0,那么N=N+1
错误
差错码 | 1个字节 | 功能码+0x82 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求读离散量输入197-218 的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 02 | 功能 | 02 |
起始地址Hi | 00 | 字节数 | 03 |
起始地址Lo | C4 | 输出状态204-197 | AC |
输出数量Hi | 00 | 输出状态212-205 | DB |
输出数量Lo | 16 | 输出状态218-213 | 35 |
将离散量输入状态204-197表示为十六进制字节值AC,或二进制1010 1100。输入204是这个字节的MSB,输入197 是这个字节的LSB。
将离散量输入状态218-213表示为十六进制字节值35,或二进制0011 0101。输入218位于左侧第3 比特,输入213 是LSB。
注:用零填充2 个剩余比特(一直到高位端)。
在一个远程设备中,使用该功能码读取保持寄存器连续块的内容。请求PDU说明了起始寄存器地址和寄存器数量。从零开始寻址寄存器。因此,寻址寄存器1-16 为0-15。
将响应报文中的寄存器数据分成每个寄存器有两字节,在每个字节中直接地调整二进制内容。
对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
请求
功能码 | 1个字节 | 0x03 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
寄存器数量 | 2个字节 | 1至125(0x7D) |
响应
功能码 | 1个字节 | 0x03 |
---|---|---|
字节数 | 1个字节 | 2xN* |
寄存器值 | N*x2个字节 |
注:*N=输出数量/8,如果余数不等于0,那么N=N+1
错误
差错码 | 1个字节 | 0x83 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求读保持寄存器108-110 的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 03 | 功能 | 03 |
高起始地址 | 00 | 字节数 | 06 |
低起始地址 | 6B | 寄存器值Hi(108) | 02 |
高寄存器编号 | 00 | 寄存器值Lo(108) | 2B |
低寄存器编号 | 03 | 寄存器值Hi(109) | 00 |
寄存器值Lo(109) | 00 | ||
寄存器值Hi(110) | 00 | ||
寄存器值Lo(110) | 64 |
将寄存器108的内容表示为两个十六进制字节值02 2B,或十进制555。将寄存器109-110的内容分别表示为十六进制00 00 和00 64,或十进制0 和100。
在一个远程设备中,使用该功能码读取1 至大约125 的连续输入寄存器。请求PDU 说明了起始地址和寄存器数量。从零开始寻址寄存器。因此,寻址输入寄存器1-16 为0-15。
将响应报文中的寄存器数据分成每个寄存器为两字节,在每个字节中直接地调整二进制内容。
对于每个寄存器,第一个字节包括高位比特,并且第二个字节包括低位比特。
请求
功能码 | 1个字节 | 0x04 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
寄存器数量 | 2个字节 | 0x0000至0x007D |
响应
功能码 | 1个字节 | 0x04 |
---|---|---|
字节数 | 1个字节 | 2xN* |
寄存器值 | N*x2个字节 |
注:*N=输入寄存器的数量
错误
差错码 | 1个字节 | 0x84 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求读输入寄存器9的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 04 | 功能 | 04 |
起始地址Hi | 00 | 字节数 | 02 |
起始地址Lo | 08 | 输入寄存器值Hi(9) | 00 |
输入寄存器数量Hi | 00 | 输入寄存器值Lo(9) | 0A |
输入寄存器Lo | 01 |
将输入寄存器9 的内容表示为两个十六进制字节值00 0A,或十进制10。
在一个远程设备上,使用该功能码写单个输出为ON 或OFF。
请求数据域中的常量说明请求的ON/OFF状态。十六进制值FF 00请求输出为ON。十六进制值00 00 请求输出为OFF。其它所有值均是非法的,并且对输出不起作用。
请求PDU说明了强制的线圈地址。从零开始寻址线圈。因此,寻址线圈1 为0。线圈值域的常量说明请求的ON/OFF 状态。十六进制值0XFF00请求线圈为ON。十六进制值0X0000请求线圈为
OFF。其它所有值均为非法的,并且对线圈不起作用。
正常响应是请求的应答,在写入线圈状态之后返回这个正常响应。
请求
功能码 | 1个字节 | 0x05 |
---|---|---|
输出地址 | 2个字节 | 0x0000至0xFFFF |
输出值 | 2个字节 | 0x0000至0xFF00? |
响应
功能码 | 1个字节 | 0x05 |
---|---|---|
输出地址 | 2个字节 | 0x0000至0xFFFF |
输出值 | 2个字节 | 0x0000至0xFF00 |
错误
差错码 | 1个字节 | 0x85 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求写线圈173为ON的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 05 | 功能 | 05 |
输出地址Hi | 00 | 输出地址Hi | 00 |
输出地址Lo | AC | 输出地址Lo | AC |
输出值Lo | 00 | 输出值Lo | 00 |
在一个远程设备中,使用该功能码写单个保持寄存器。
请求PDU 说明了被写入寄存器的地址。从零开始寻址寄存器。因此,寻址寄存器1 为0。
正常响应是请求的应答,在写入寄存器内容之后返回这个正常响应。
请求
功能码 | 1个字节 | 0x06 |
---|---|---|
寄存器地址 | 2个字节 | 0x0000至0xFFFF |
寄存器值 | 2个字节 | 0x0000至0xFFFF |
响应
功能码 | 1个字节 | 0x06 |
---|---|---|
寄存器地址 | 2个字节 | 0x0000至0xFFFF |
寄存器值 | N*x2个字节 | 0x0000至0xFFFF |
错误
差错码 | 1个字节 | 0x86 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求将十六进制 00 03 写入寄存器2的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 06 | 功能 | 06 |
寄存器地址Hi | 00 | 输出地址Hi | 00 |
寄存器地址Lo | 01 | 输出地址Lo | 01 |
寄存器值Hi | 00 | 输出值Hi | 00 |
寄存器值Lo | 03 | 输出值Hi | 03 |
在一个远程设备中,使用该功能码强制线圈序列中的每个线圈为ON 或OFF。请求PDU说明了强制的线圈参考。从零开始寻址线圈。因此,寻址线圈1 为0。
请求数据域的内容说明了被请求的ON/OFF 状态。域比特位置中的逻辑“1”请求相应输出为ON。域比特位置中的逻辑“0”请求相应输出为OFF。
正常响应返回功能码、起始地址和强制的线圈数量。
请求PDU
功能码 | 1个字节 | 0x0F |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
输出数量 | 2个字节 | 0x0001至0x07B0 |
字节数 | 1个字节 | N* |
输出值 | N*X1个字节 |
注:*N=输出数量/8,如果余数不等于0,那么N=N+1
响应PDU
功能码 | 1个字节 | 0x0F |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
输出数量 | 2个字节 | 0x0001至0x07B0 |
错误
差错码 | 1个字节 | 0x8F |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求从线圈20开始写入10个线圈的实例:
请求的数据内容为两个字节:十六进制CD 01 (二进制1100 1101 0000 0001)。使用下列方法,二进制比特对应输出。
比特 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
输出 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | – | – | – | – | – | – | 29 | 28 |
传输的第一字节(十六进制CD)寻址为输出27-20,在这种设置中,最低有效比特寻址为最低输出(20)。
传输的下一字节(十六进制01)寻址为输出29-28,在这种设置中,最低有效比特寻址为最低输出(28)。
应该用零填充最后数据字节中的未使用比特。
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 0F | 功能 | 0F |
起始地址Hi | 00 | 起始地址Hi | 00 |
起始地址Lo | 13 | 起始地址Lo | 13 |
输出数量Hi | 00 | 输出数量Hi | 00 |
输出数量Lo | 0A | 输出数量Lo | 0A |
字节数 | 02 | ||
输出值Hi | CD | ||
输出数量Lo | 01 |
在一个远程设备中,使用该功能码写连续寄存器块(1 至约120 个寄存器)。
在请求数据域中说明了请求写入的值。每个寄存器将数据分成两字节。
正常响应返回功能码、起始地址和被写入寄存器的数量。
请求
功能码 | 1个字节 | 0x10 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
寄存器数量 | 2个字节 | 0x0001至0x0078 |
字节数 | 1个字节 | 2xN* |
寄存器值 | N*x2个字节 |
注:*N=寄存器的数量
响应
功能码 | 1个字节 | 0x04 |
---|---|---|
起始地址 | 2个字节 | 0x0000至0xFFFF |
寄存器数量 | 2个字节 | 1至123(0x7B) |
错误
差错码 | 1个字节 | 0x90 |
---|---|---|
异常码 | 1个字节 | 01或02或03或04 |
这是一个请求将十六进制00 0A 和01 02 写入以2 开始的两个寄存器的实例:
请求 | 响应 | ||
域名 | 十六进制 | 域名 | 十六进制 |
功能 | 10 | 功能 | 10 |
起始地址Hi | 00 | 起始地址Hi | 00 |
起始地址Lo | 01 | 起始地址Lo | 01 |
寄存器数量Hi | 00 | 寄存器数量Hi | 00 |
寄存器数量Lo | 02 | 寄存器数量Lo | 02 |
字节数 | 04 | ||
寄存器值Hi | 00 | ||
寄存器值Lo | 0A | ||
寄存器值Hi | 01 | ||
寄存器值Lo | 02 |