ModbusRtu通信协议

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 00FF00 表示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校验。

线圈状态解析(按地址顺序):

线圈地址202122232425未使用未使用
位值10110010
状态ONOFFONONOFFOFF

注意

数据字段的每个字节表示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 的规则:必须指定一个连续的寄存器块,起始地址 + 寄存器数量,且数据按顺序填充。

你的需求矛盾

要写入 000000040006 三个不连续地址。

每个浮点数占用 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 个寄存器的值为 0x12340x56780x9ABC):

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 个

需确保请求的地址和数量在设备允许范围内,并正确解析数据格式(如浮点数、整数、字节序)。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注