1. 协议基础
主从架构:单主站(Master)轮询多个从站(Slave),从站仅在收到主站请求后响应。
传输模式:
二进制编码:数据以二进制形式传输,效率高于ASCII模式。
物理层:通常基于RS-485(多点通信)或RS-232(点对点)。
波特率:常用9600、19200、38400等,需所有设备一致。
地址范围:从站地址为1~247(0为广播地址,但实际中极少使用)。
2. 数据帧格式
Modbus RTU帧由地址、功能码、数据、CRC校验组成,格式如下:
字段 | 长度 | 说明 |
起始符 | 无 | 帧间需保持至少3.5字符时间的静默(由波特率计算) |
从站地址 | 1字节 | 1~247 |
功能码 | 1字节 | 定义操作类型(如读/写寄存器) |
数据字段 | N字节 | 具体操作的数据(如寄存器地址、数量、值等) |
CRC校验 | 2字节 | 循环冗余校验(Cyclic Redundancy Check) |
结束符 | 无 | 帧间需保持至少3.5字符时间的静默 |
示例帧(读取保持寄存器):
主站请求:01 03 00 6B 00 03 76 87
01:从站地址
03:功能码(读保持寄存器)
00 6B:起始寄存器地址(0x006B = 十进制107)
00 03:寄存器数量(3个)
76 87:CRC校验
从站响应:01 03 06 02 2B 00 00 00 64 45 6F
01:从站地址
03:功能码
06:数据字节数(6字节)
02 2B 00 00 00 64:寄存器值(按顺序解析)
45 6F:CRC校验
3. 核心功能码
常用功能码如下:
功能码 | 名称 | 操作 |
01 | 读线圈状态 | 读取单个或多个线圈(输出)的ON/OFF状态 |
02 | 读输入状态 | 读取单个或多个输入(离散量输入)的状态 |
03 | 读保持寄存器 | 读取单个或多个保持寄存器的值(常用) |
04 | 读输入寄存器 | 读取输入寄存器的值(如传感器数据) |
05 | 写单个线圈 | 强制单个线圈为ON/OFF |
06 | 写单个保持寄存器 | 写入单个保持寄存器的值 |
15 | 写多个线圈 | 写入多个线圈状态 |
16 | 写多个保持寄存器 | 写入多个保持寄存器的值 |
4. 数据模型
Modbus定义四类数据存储区:
数据类型 | 读写权限 | 地址范围 |
线圈(Coils) | 读/写 | 00001 ~ 09999 |
离散输入(Inputs) | 只读 | 10001 ~ 19999 |
保持寄存器(Holding Registers) | 读/写 | 40001 ~ 49999 |
输入寄存器(Input Registers) | 只读 | 30001 ~ 39999 |
5. CRC校验
计算范围:从地址字段到数据字段的所有字节。
算法:使用多项式 0xA001(CRC-16)。
工具:可通过查表法或在线工具生成校验码。
6. 异常响应
从站返回异常时,功能码最高位置1(原功能码 + 0x80),并附加错误码:
示例:01 83 02 C0 F1
83:功能码0x03的异常(03 + 80 = 0x83)
02:错误码(02表示“无效地址”)
7. 物理层配置
RS-485:需终端电阻(120Ω)防止信号反射,支持最多32个设备。
接线:A/B线(差分信号)需正确连接,避免干扰。
波特率/数据位/校验位:常见配置为 8-N-1(8数据位、无校验、1停止位)。
8. 优缺点
优点:
简单易实现,资源占用低。
实时性强,适合低速串行通信。
缺点:
无加密或身份验证机制,安全性低。
主从架构不支持多主站。
9. 应用场景
PLC与传感器、仪表、变频器等设备通信。
工业控制系统(SCADA、DCS等)中的数据采集与控制。
10. 写入单个线圈实例
功能码:05(写单个线圈)
作用:强制线圈(Coil)的值为 ON(0xFF00)或 OFF(0x0000)。
10.1 请求帧格式
字段 | 字节数 | 示例值 | 说明 |
从站地址 | 1字节 | 01 | 目标从站地址(1~247) |
功能码 | 1字节 | 05 | 写单个线圈 |
线圈地址 | 2字节 | 00 13 | 线圈地址(0x0013 = 十进制19,对应协议地址 00020) |
写入值 | 2字节 | FF 00 或 00 00 | FF00 表示ON,0000 表示OFF |
CRC校验 | 2字节 | 8C 3A | 校验地址、功能码、地址、数据的CRC |
10.2 示例请求帧(将线圈20设为ON):
01 05 00 13 FF 00 8C 3A
解析:
01:从站地址1。
05:功能码(写线圈)。
00 13:线圈地址为0x0013(十进制19),对应Modbus协议地址 00020(地址从1开始)。
FF 00:强制线圈为ON。
8C 3A:CRC校验码。
10.3 响应帧格式
成功写入后,从站返回与请求相同的帧(确认写入值):
01 05 00 13 FF 00 8C 3A
字段含义与请求帧完全一致。
10.4 异常响应
若写入失败(如地址无效),从站返回异常帧:
01 85 02 80 71
解析:
01:从站地址。
85:异常功能码(0x05 + 0x80 = 0x85)。
02:错误码(02表示“无效地址”)。
80 71:CRC校验。
10.5 关键注意事项
线圈地址偏移:
协议中线圈地址范围为 00001-09999,但在数据帧中需转换为 0-based 地址(减1)。
例如:协议地址 00020 → 数据帧地址 0x0013(十进制19)。
写入值固定:
FF 00 表示ON,00 00 表示OFF,其他值无效。
CRC计算工具:
使用在线工具或代码库(如Python的 crcmod)生成校验码,避免手动计算错误。
10.6 实际应用场景
控制继电器、电磁阀等设备的开关。
示例代码(Python伪代码):
python
import serialimport crcmod
# 构造请求帧
slave_address = 0x01
function_code = 0x05
coil_address = 0x0013 # 对应协议地址00020
value = 0xFF00 # ON
frame = bytes([slave_address, function_code]) + coil_address.to_bytes(2, ‘big’) + value.to_bytes(2, ‘big’)
# 计算CRC
crc_func = crcmod.mkCrcFun(0x18005, rev=True, initCrc=0xFFFF)
crc = crc_func(frame)
frame += crc.to_bytes(2, ‘little’) # CRC以小端字节序附加
# 通过串口发送
ser = serial.Serial(‘/dev/ttyUSB0’, baudrate=9600, timeout=1)
ser.write(frame)
总结来说,用户需要明确的步骤说明、实际例子以及常见问题的解决方法。确保回答结构清晰,覆盖所有关键点,特别是数据打包和地址转换,这些通常是容易出错的地方。
以下是 Modbus RTU 写多个线圈(功能码 15,十六进制 0x0F) 的详细指令示例和解析:
11. 写入多个线圈实例
功能码:15(0x0F,写多个线圈)
作用:一次性写入多个线圈(Coils)的ON/OFF状态。
11.1 请求帧格式
字段 | 字节数 | 示例值 | 说明 |
从站地址 | 1字节 | 01 | 目标从站地址(1~247) |
功能码 | 1字节 | 0F | 写多个线圈 |
起始线圈地址 | 2字节 | 00 13 | 起始线圈地址(0x0013 = 十进制19,对应协议地址 00020) |
线圈数量 | 2字节 | 00 06 | 要写入的线圈数量(6个线圈) |
字节数 | 1字节 | 01 | 数据字段的字节数(6线圈需1字节,见下文说明) |
数据(线圈状态) | N字节 | B2 | 线圈状态的二进制位组合(B2 = 1011 0010,见解析) |
CRC校验 | 2字节 | 4E 8B | 校验所有字段的CRC值 |
示例请求帧(写入线圈20~25的6个线圈):
01 0F 00 13 00 06 01 B2 4E 8B
解析:
01:从站地址1。
0F:功能码(写多个线圈)。
00 13:起始线圈地址0x0013(十进制19,对应协议地址 00020)。
00 06:写入6个线圈(地址20~25)。
01:数据字段占1字节(6个线圈需1字节,8位中仅使用低6位)。
B2:二进制 1011 0010,解析为线圈状态(见下表)。
4E 8B:CRC校验。
线圈状态解析(按地址顺序):
线圈地址 | 20 | 21 | 22 | 23 | 24 | 25 | 未使用 | 未使用 |
位值 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 0 |
状态 | ON | OFF | ON | ON | OFF | OFF | – | – |
注意:
数据字段的每个字节表示8个线圈状态,按位打包,低位在前(如字节 B2 的二进制为 10110010,但实际解析顺序为从右到左):
第1位(最低位):地址20 → 0(对应二进制 10110010 的第0位是 0,但此处示例可能需要修正位顺序)。
修正说明:Modbus协议规定,数据字段的每个字节中,最低位对应起始地址,后续位依次递增。例如:
字节 B2(二进制 10110010)解析为:
位0(最低位)→ 地址20:0(OFF)
位1 → 地址21:1(ON)
位2 → 地址22:0(OFF)
位3 → 地址23:0(OFF)
位4 → 地址24:1(ON)
位5 → 地址25:1(ON)
位6和7未使用(填充0)。
实际应根据具体需求调整位顺序。
11.3 响应帧格式
成功写入后,从站返回确认帧:
01 0F 00 13 00 06 3D 8C
解析:
01:从站地址。
0F:功能码。
00 13:起始地址。
00 06:写入的线圈数量。
3D 8C:CRC校验。
11.4 异常响应
若写入失败(如地址越界),返回异常帧:
01 8F 02 80 71
解析:
01:从站地址。
8F:异常功能码(0x0F + 0x80 = 0x8F)。
02:错误码(02表示“无效地址”)。
80 71:CRC校验。
11.5 关键注意事项
数据字节数计算:
线圈数量为 N,所需字节数为 ceil(N / 8)。
例如:6个线圈 → 1字节,10个线圈 → 2字节。
位顺序:
每个字节中,最低位(LSB)对应起始地址,后续位依次递增。
例如:数据字节 0x01(二进制 00000001)表示仅第一个线圈为ON。
未使用位填充:
若线圈数量不足8的倍数,最后一个字节的高位补0(例如6个线圈时,最后2位无意义)。
11.6 实际应用场景
批量控制多个继电器、指示灯或电磁阀。
Python伪代码示例:
python
import serialimport crcmod
def write_multiple_coils(slave_addr, start_addr, coil_states):
# 计算字节数和数据
coil_count = len(coil_states)
byte_count = (coil_count + 7) // 8
data_bytes = bytearray(byte_count)
# 将线圈状态打包为字节
for i in range(coil_count):
if coil_states[i]:
byte_index = i // 8
bit_index = i % 8
data_bytes[byte_index] |= (1 << bit_index)
# 构造请求帧
frame = bytes([slave_addr, 0x0F])
frame += start_addr.to_bytes(2, ‘big’)
frame += coil_count.to_bytes(2, ‘big’)
frame += bytes([byte_count])
frame += data_bytes
# 计算CRC
crc_func = crcmod.mkCrcFun(0x18005, rev=True, initCrc=0xFFFF)
crc = crc_func(frame)
frame += crc.to_bytes(2, ‘little’)
# 发送帧
ser = serial.Serial(‘/dev/ttyUSB0’, baudrate=9600)
ser.write(frame)
# 示例:写入线圈20~25为 [ON, OFF, ON, ON, OFF, OFF]
write_multiple_coils(1, 19, [1, 0, 1, 1, 0, 0])
11.7 常见问题
位顺序错误:确保数据字节的位顺序与线圈地址顺序一致。
字节数不足:未正确计算 ceil(N/8) 会导致数据截断。
地址偏移:协议地址(如00020)需转换为0-based地址(19)。
12. 写单个保持寄存器(功能码 0x06)
作用:向从站设备的单个保持寄存器写入一个16位值。
请求帧格式:
[从站地址] [功能码] [寄存器地址高] [寄存器地址低] [数据高] [数据低] [CRC低] [CRC高]
示例:
假设:
从站地址:0x01
寄存器地址:0x0002(对应MODBUS地址40003,因为地址从40001开始)
写入数据:0x00C8(十进制200)
请求帧(十六进制):
01 06 00 02 00 C8 08 03
分解:
01:从站地址
06:功能码(写单个寄存器)
00 02:寄存器地址(大端格式)
00 C8:写入的数据(大端格式)
08 03:CRC校验值(低字节在前)
13. 写多个保持寄存器(功能码 0x10)
作用:向从站设备的连续多个保持寄存器写入多个16位值。
请求帧格式:
[从站地址] [功能码] [起始地址高] [起始地址低] [寄存器数量高] [寄存器数量低] [字节数] [数据1高] [数据1低] [数据2高] [数据2低] … [CRC低] [CRC高]
示例:
假设:
从站地址:0x01
起始寄存器地址:0x0000(对应MODBUS地址40001)
写入2个寄存器:
寄存器1数据:0x1234
寄存器2数据:0x5678
请求帧(十六进制):
01 10 00 00 00 02 04 12 34 56 78 7C 9A
分解:
01:从站地址
10:功能码(写多个寄存器)
00 00:起始地址(大端格式)
00 02:寄存器数量(写入2个寄存器)
04:后续数据的总字节数(2寄存器 × 2字节 = 4字节)
12 34:第一个寄存器的数据(0x1234)
56 78:第二个寄存器的数据(0x5678)
7C 9A:CRC校验值(低字节在前)
关键注意事项:
寄存器地址与MODBUS地址的映射:
MODBUS地址40001对应协议中的寄存器地址0x0000,40002对应0x0001,依此类推。
数据格式:
所有数值均为大端格式(高字节在前),但CRC校验值为小端格式(低字节在前)。
CRC计算:
使用标准MODBUS CRC16算法,可借助工具(如在线CRC计算器)生成。
从站响应:
成功写入后,从站会返回与请求相同的帧(写单个寄存器)或返回起始地址和寄存器数量(写多个寄存器)。
14. 读取离散输入(Inputs,功能码 02)
在 Modbus 协议中,读取离散输入(Inputs,功能码 02)和读取线圈(Coils,功能码 01)的返回数据格式完全相同,均以 按位打包的字节(bit-packed bytes) 形式表示每个输入或线圈的状态。以下是具体分析:
1. 返回数据格式
无论是读取线圈(01)还是离散输入(02),响应报文的数据部分均为 位(bit)的紧凑打包形式:
每个字节(Byte)表示 8 个输入/线圈的状态(从低地址到高地址依次排列)。
字节内位的顺序:最低有效位(LSB)对应第一个地址,最高有效位(MSB)对应第 8 个地址。
例如,若读取地址 0~7 的状态为 [1,0,1,0,1,0,1,0],则返回字节为 `0x55`(二进制 01010101)。
剩余位填充:如果请求的输入/线圈数量不是 8 的倍数,最后一个字节的高位部分补零。
2. 响应报文结构
以功能码 02(读离散输入) 为例:
响应报文格式:
[事务ID] [协议ID] [长度] [单元ID] [功能码] [字节数] [数据字节1] [数据字节2] …
字节数(Byte Count):表示后续数据的总字节数。
数据字节(Data Bytes):按位打包的输入状态。
3. 示例对比
假设读取 10 个离散输入(地址 0~9),响应报文如下:
功能码(02) | 字节数(2) | 数据字节1(0xCD) | 数据字节2(0x01)
数据解析:
字节1 0xCD → 二进制 11001101,表示地址 0~7 的状态:1,0,1,1,0,0,1,1。
字节2 0x01 → 二进制 00000001,表示地址 8~9 的状态:1,0(高位补零)。
4. 与线圈(Coils,功能码 01)的区别
功能码不同:线圈(01)与离散输入(02)的功能码不同,但数据格式一致。
读写权限:
线圈(Coils):可读可写(功能码 01 读,05 写单个,15 写多个)。
离散输入(Inputs):只读(功能码 02)。
5. 解析方法
无论是线圈还是离散输入,均需按以下步骤解析:
提取 字节数 字段,确定数据长度。
将数据字节转换为二进制,按位拆解。
按顺序映射到具体地址。
代码示例(Lua):
— 假设收到数据字节为 {0xCD, 0x01},读取 10 个输入local bytes = {0xCD, 0x01}local bits = {}
for i, byte in ipairs(bytes) do
for bit_pos = 0, 7 do
local bit = (byte >> bit_pos) & 1
bits[#bits + 1] = bit
endend
— 输出地址 0~9 的状态for addr = 0, 9 do
print(“地址”, addr, “状态:”, bits[addr + 1]) — Lua 索引从 1 开始end
6. 总结
是,返回包格式相同:离散输入(02)和线圈(01)的响应数据均以 位打包的字节 形式返回。
唯一区别:功能码和寄存器类型(线圈可写,离散输入只读)。
解析代码可复用,只需注意功能码对应的寄存器类型。
在 MODBUS RTU 协议中,功能码 0x10(写多个保持寄存器) 只能操作连续的寄存器地址。如果需要对不连续地址写入数据,必须分多次发送不同的请求,或依赖设备支持自定义扩展协议。以下是针对你的需求的详细分析和解决方案:
15. 是否可以用10 写入非连续地址的数据:比如
用指令 16 写多个保持寄存器 可以是不连续地址吗, 比如开始地址0000,我要写00,04,06 这三个地址,00,04写入4字节浮点数(ABCD序),06写入2个字节的整数(AB需要)
标准MODBUS协议的限制
功能码 0x10 的规则:必须指定一个连续的寄存器块,起始地址 + 寄存器数量,且数据按顺序填充。
你的需求矛盾:
要写入 0000, 0004, 0006 三个不连续地址。
每个浮点数占用 2 个连续寄存器(4 字节,ABCD 序),整数占用 1 个寄存器(2 字节,AB 序)。
无法通过单次功能码 0x10 实现。
在 Modbus RTU 协议中,功能码 0x04(读输入寄存器) 支持一次性读取多个连续的输入寄存器。以下是具体说明和示例:
14. 读取一个或多个输入寄存器
功能码:0x04(读输入寄存器)
用途:从从站设备读取只读输入寄存器的数据(例如传感器数值、设备状态等)。
支持范围:可一次性读取 1~125 个连续寄存器(每个寄存器为 16 位)。
1. 请求帧格式
[从站地址] [功能码] [起始地址高] [起始地址低] [寄存器数量高] [寄存器数量低] [CRC低] [CRC高]
起始地址:输入寄存器的起始地址(大端格式,即高字节在前)。
寄存器数量:要读取的寄存器个数(大端格式)。
示例:
从站地址:0x01
起始地址:0x0000(对应 Modbus 地址 30001)
读取数量:3 个寄存器
请求帧(十六进制):
01 04 00 00 00 03 [CRC]
分解:
01:从站地址
04:功能码(读输入寄存器)
00 00:起始地址(大端格式)
00 03:读取 3 个寄存器(大端格式)
[CRC]:CRC 校验值(低字节在前)。
2. 响应帧格式
[从站地址] [功能码] [字节数] [数据1高] [数据1低] [数据2高] [数据2低] … [CRC低] [CRC高]
字节数:后续数据的字节总数(寄存器数量 × 2)。
数据格式:每个寄存器数据为大端格式(高字节在前)。
示例响应(假设读取到 3 个寄存器的值为 0x1234, 0x5678, 0x9ABC):
01 04 06 12 34 56 78 9A BC [CRC]
分解:
01:从站地址
04:功能码
06:数据总字节数(3 寄存器 × 2 字节 = 6 字节)
12 34:第一个寄存器的值(0x1234)
56 78:第二个寄存器的值(0x5678)
9A BC:第三个寄存器的值(0x9ABC)
[CRC]:CRC 校验值。
3. 关键注意事项
(1) 地址范围限制
Modbus 输入寄存器的逻辑地址范围为 30001~39999(协议中的寄存器地址为 0x0000~0x270F)。
确保起始地址和寄存器数量不超出设备支持的范围(参考从站设备文档)。
(2) 数据解析
若数据为 32 位浮点数或 32 位整数,需根据设备定义的字节序(如 ABCD、CDAB 等)组合两个寄存器的值。
例如,浮点数占用地址 0000 和 0001,需将两个寄存器的值拼接后转换。
(3) 错误响应
如果请求非法(如地址无效或数量超限),从站返回错误帧:
[从站地址] [功能码 + 0x80] [异常码] [CRC]
异常码:0x02(非法地址)、0x03(非法数据值)等。
4. 实际应用场景
(1) 读取传感器数据
假设从站的输入寄存器存储了以下数据:
地址 30001(0x0000):温度值(16 位整数)
地址 30002(0x0001):湿度值(16 位整数)
地址 30003(0x0002):压力值(32 位浮点数,占用 0x0002 和 0x0003)
请求帧(读取 4 个寄存器):
01 04 00 00 00 04 [CRC]
响应帧:
01 04 08 00 64 00 32 43 1C 00 00 [CRC]
00 64 → 温度值(0x0064 = 100)
00 32 → 湿度值(0x0032 = 50)
43 1C 00 00 → 浮点数(需按 ABCD 序解析为 0x431C0000 = 156.0)。
6. 总结
功能码 0x04 支持一次性读取多个连续的输入寄存器,最大数量为 125 个。
需确保请求的地址和数量在设备允许范围内,并正确解析数据格式(如浮点数、整数、字节序)。
发表回复