嗨,相信在上一篇经过我的兄弟 RTU 的介绍之后,已经对 Modbus 有了一定的了解了吧;那么本篇就跟紧我的脚步一起学习新的知识吧。
# 描述
Modbus 在串行设备中通过实现主从模型结构,解决了电子设备之间的数据通讯问题;在采用 Modbus 协议时,它有两种主要的原始传输方式 ---- Modbus RTU 和 Modbus ASCII。而 Modbus RTU 已经在上一篇介绍了,那么就在本篇中瞅瞅 ASCII 吧。
# 通讯方式
# 帧格式
Name | Length (bytes) | Description |
---|---|---|
Start | 1 | Starts with colon : (ASCII hex value is 3A)<br/>(以冒号 : 开头,ASCII 十六进制值为 3A) |
Address | 2 | Node address in hex<br/>(十六进制节点地址,字符表示) |
Function | 2 | Function code in hex<br/>(十六进制功能码,字符表示) |
Data | n x 2 | n is the number of data bytes, it depends on function<br/>(n 是数据字节数,它取决于功能码) |
LRC | 2 | Longitudinal redundancy check<br/>(LRC 校验码) |
End | 2 | CR / LF |
注:地址、功能、数据和 LRC 都是表示 8 位值 (0-255) 的大写十六进制可读字符对;即:在 Modbus ASCII 中,每个数据字节被分割成表示十六进制值中的两个 ASCII 字符的两个字节。
在 ASCII 模式下,消息以冒号 :
字符开头(ASCII 表示为 0x3A),以回车换行对 \r\n
(ASCII 表示为 0x0D 和 0x0A)结尾;所有其他字段传输的数据所允许的十六进制表示字符为的 0-9
、 A-F
。
START | ADDRESS | FUNCTION | DATA | LRC CHECK | END |
---|---|---|---|---|---|
1 CHAR<br/>: | 2 CHARS | 2 CHARS | n CHARS | 2 CHARS | 2 CHARS<br/>CRLF |
# 功能码
ASCII 最常用的功能代码跟 RTU 的功能代码定义是一样的,这里就不多说了,可以去查看 《Modbus 家族之 RTU》篇章的功能码部分,这里只是格式上有所不同而已,下一篇会对这两个原始传输方式进行对比的。 嘛,还是直接合并到本篇,对 RTU 和 ASCII 进行对比分析吧,顺便回顾一下 RTU 协议。
访问地址:address | 映射地址 | 描述 | 功能 | R/W |
---|---|---|---|---|
1 ~ 10000 | address-1 | Coils | 01/05/15 | R/W |
10001 ~ 20000 | address-10001 | Discrete Inputs | 02 | R |
30001 ~ 40000 | address-30001 | Input Registers | 04 | R |
40001 ~ 50000 | address-40001 | Holding Registers | 03/06/16 | R/W |
在这里,简单的举个 ASCII 传输例子:
例如,要读取 VAR1,你需要从地址 0x20C1 读取 2 个寄存器,所以你需要发送以下 ASCII 消息:
:010420C1000218<CRLF>
请求:
Name Description ‘:’ Start of message - 0x3A ‘0’ ‘1’ Node address – 0x01 ‘0’ ‘4’ Function code (Read Input Registers) – 0x04 ‘2’ ‘0’ ‘C’ ‘1’ Register address for reading VAR1 – 0x20C1 ‘0’ ‘0’ ‘0’ ‘2’ Length of registers to be read (must be 2) – 0x0002 ‘1’ ‘8’ LRC <CRLF> End of message, carriage return and line feed – 0x0D0A
此消息的响应如下:
:01040400001234B1<CRLF>
响应:
Name Description ‘:’ Start of message - 0x3A ‘0’ ‘1’ Node address – 0x01 ‘0’ ‘4’ Function code (Read Input Registers) – 0x04 ‘0’ ‘4’ Read data length (4 bytes) – 0x04 ‘0’ ‘0’ ‘0’ ‘0’ ‘1’ ‘2’ ‘3’ ‘4’ Value read from VAR1 – 0x00001234 ‘B’ ‘1’ LRC <CRLF> End of message, carriage return and line feed – 0x0D0A
好了,那么就直入主题吧,常用功能码部分依然是如下几个:
# 功能 01(01H)读线圈
请求
读取从机中线圈的 ON/OFF 状态。不支持广播。请求消息指定了开始线圈和要读取的线圈数量。
下面是一个请求读取线圈的例子:19 - 55(Coil 20 to 56),37 个线圈,从设备节点 3(注意起始地址是 19 或 0x13,比线圈 20 小 1):
响应
线圈状态响应消息被打包为数据字段的每比特表示一个线圈。状态表示为:1 = ON,0 = OFF。第一个数据字节的 LSB 包含请求中寻址的线圈。其他线圈跟随这个字节的高阶末端,并在随后的字节中从低阶到高阶。
例如,当线圈 20 - 27 的状态显示
ON - ON - OFF - OFF - ON - OFF - ON - OFF - ON - OFF
时,以字节值二进制0101 0011 (0x53)
表示。一个字节包含八个线圈的状态。如果返回的线圈数量不是 8 的倍数,则最终数据字节中的剩余位将用 0 填充 (朝向字节的高阶末端);字节计数字段指定数据的完整字节数。Figure 6 shows an example of a response to the query shown in Figure 5:
# 功能 02(02H)读离散输入
请求
读取从机中离散输入的 ON/OFF 状态。不支持广播。请求消息指定起始输入和要读取的输入数量。
下面是一个从从设备节点 3 读取离散输入 10101 - 10120,总共 20 个输入的例子(注意起始地址是 100 或 0x64,比输入 10101 小 10001):
响应
离散输入状态响应消息的构造与线圈状态 (01H) 操作相同。
Figure 8 shows an example of a response to the query shown in Figure 7:
# 功能 03(03H)读保持寄存器
请求
读取从机中保持寄存器的二进制内容。不支持广播。请求消息指定起始寄存器和要读取的寄存器数量。
下面是一个从从设备节点 7 读取保持寄存器 40201 - 40203,总共 3 个寄存器的请求的例子(注意起始地址是 200 或 0xC8,比寄存器 40201 小 40001):
响应
响应消息中的保持寄存器数据在每个寄存器中打包为两个字节,二进制内容在每个字节中右对齐;对于每个寄存器,第一个字节包含高阶位,第二个字节包含低阶位。
Figure 10 shows an example of a response to the query shown in Figure 9:
# 功能 04(04H)读输入寄存器
请求
读取从机中保持寄存器的二进制内容。不支持广播。请求消息指定起始寄存器和要读取的寄存器数量。
下面是一个从从设备节点 7 读取输入寄存器 30301 - 30303,总共 3 个寄存器的请求的例子(注意起始地址是 300 或 0x12C,比寄存器 30301 小 30001):
响应
读输入寄存器数据的响应消息的构造与读取保持寄存器 (03H) 操作相同。
Figure 12 shows an example of a response to the query shown in Figure 11:
# 功能 05(05H)写单线圈
请求
将单个线圈写入 ON 或 OFF。当广播时,该函数强制所有附加的从机使用相同的线圈引用。请求消息指定要写入的线圈引用(启动线圈和状态)。
FF 00
的值要求线圈打开,值为00 00
的请求为关闭,所有其他值都是非法的,不会影响线圈。下面是一个在从设备节点 3 中请求打开线圈 150 的例子(注意起始地址是 149 或 0x95,比线圈 150 小 1):
响应
正常的响应是请求的回显,在写入线圈状态之后返回。
Figure 14 shows an example of a response to the query shown in Figure 13:
# 功能 06(06H)写单个保持寄存器
请求
将一个值写入单个保持寄存器中。当广播时,该函数在所有附加的从机上设置相同的寄存器引用。请求消息指定要写入的寄存器引用(指定地址和数值)。
下面是一个请求从从设备节点 3 中的保持寄存器 40150 写入 1000 数值的例子(注意起始地址为 149 或 0x95,比寄存器 40150 小 40001):
响应
正常的响应是请求的回显,在写入保持寄存器内容之后返回。
Figure 16 shows an example of a response to the query shown in Figure 15:
# 功能 15(0FH)写多个线圈
请求
将一个线圈序列中的每个线圈写入 ON 或 OFF。当广播时,该函数强制所有附加的从机使用相同的线圈引用。请求消息指定要写入的线圈引用(起始线圈和状态)。
下面的示例显示了从设备节点 5 中的线圈 20 开始写入一系列 10 个线圈状态的请求。二进制位与线圈的对应方式如下(注意起始地址是 19 或 0x13,比线圈 20 小 1):
Bit 1 1 0 1 0 0 0 1 0 0 0 0 0 1 0 1 Coil 27 26 25 24 23 22 21 20 ... ... ... ... ... 30 29 28 响应
正常响应返回从地址、功能代码、起始地址和写入的线圈数量,不包括字节数和对应写入的状态。
Figure 24 shows an example of a response to the query shown in Figure 23:
# 功能 16(10H)写多个保持寄存器
请求
将值写入到一个保持寄存器序列中。当广播时,该函数在所有附加的从机上设置相同的寄存器引用。请求消息指定要写入的寄存器引用(起始寄存器和数值)。
下面是一个请求从从设备节点 5 中的保持寄存器 40020 到 40022 写入以下数据的示例(注意起始地址是 19 或 0x13,比寄存器 40020 小 40001):
address data 40020 0x0164 40021 0x0165 40022 0x0166 响应
正常响应返回从地址、功能代码、起始地址和写入的寄存器数量,不包括字节数和对应写入的数据。
Figure 26 shows an example of a response to the query shown in Figure 25:
# LRC 校验
unsigned char | |
ucMBLRC( unsigned char * pucFrame, unsigned short usLen ) | |
{ | |
unsigned char ucLRC = 0; /* LRC char initialized */ | |
while( usLen-- ) | |
{ | |
ucLRC += *pucFrame++; /* Add buffer byte without carry */ | |
} | |
/* Return twos complement */ | |
ucLRC = ( UCHAR ) ( -( ( CHAR ) ucLRC ) ); | |
return ucLRC; | |
} |
校验原理可看 常用校验算法 - LRC 章节