理解Modbus地址:设备手册地址 (40001) vs. 协议地址 (0)
理解Modbus地址:设备手册地址 (40001) vs. 协议地址 (0)
你提出的问题“协议中使用的地址是从0开始的,如何理解40001号寄存器在协议帧中地址为0x0000”是理解 Modbus 通信的关键。
简单来说,Modbus 存在两套地址系统:
设备地址 (或叫“人机界面地址”): 这是你在设备手册、上位机软件界面上看到的地址,例如
40001
、30005
。这套地址是给人看的,它的首位数字有特殊含义,方便工程师快速识别数据区。4
xxxx: 保持寄存器 (Holding Register)3
xxxx: 输入寄存器 (Input Register)1
xxxx: 离散量输入 (Discrete Input)0
xxxx: 线圈 (Coil)
协议地址 (或叫“PDU地址”/“偏移量地址”): 这是在实际的 Modbus 通信报文(数据帧)中使用的地址。这套地址是给机器通信用的,它是一个从 0 开始的、连续的索引(偏移量)。计算机处理从0开始的索引效率最高。
核心换算规则:
协议地址 = 设备地址 - 地址偏移量
数据类型 | 设备地址范围 | 地址偏移量 | 示例 (设备地址 -> 协议地址) |
---|---|---|---|
线圈 (Coil) | 00001 - 09999 | 1 |
|
离散量输入 | 10001 - 19999 | 10001 |
|
输入寄存器 | 30001 - 39999 | 30001 |
|
保持寄存器 | 40001 - 49999 | 40001 |
|
所以,对于你的问题: 要访问设备手册上的 40001 号保持寄存器,根据规则,它的协议地址就是 40001 - 40001 = 0
。在16位的协议地址中,0
就表示为 0x0000
。
带入具体开发场景中讲解
假设我们有一个智能电表,需要通过 Modbus RTU 读取它的当前电压值。
第一步:查阅设备手册
我们翻开电表的技术手册,找到 Modbus 通信地址表,可能会看到类似这样的内容:
功能 | 设备地址 | 数据类型 | 长度 | 读/写 | 单位 | 描述 |
---|---|---|---|---|---|---|
A相电压 | 40101 | 16位无符号整型 | 1 | R | 0.1V | 读取A相电压,值为2205代表220.5V |
B相电压 | 40102 | 16位无符号整型 | 1 | R | 0.1V | 读取B相电压 |
C相电压 | 40103 | 16位无符号整型 | 1 | R | 0.1V | 读取C相电压 |
我们要读取的是 A相电压,其设备地址为 40101
。
第二步:计算协议地址
根据我们查阅手册得到的信息:
数据类型: 保持寄存器 (因为地址是
4
xxxx 开头)设备地址:
40101
地址偏移量:
40001
现在我们进行计算: 协议地址 = 40101 - 40001 = 100
所以,在我们的代码或者通信协议帧中,需要使用的地址是 100 (十进制),转换成十六进制就是 0x0064。
第三步:编写代码 (以 Python 和 pymodbus
库为例)
很多 Modbus 库已经为我们处理了部分底层细节,但地址参数仍然需要我们传入计算后的协议地址。
from pymodbus.client import ModbusSerialClient as ModbusClient# 假设电表串口连接在 COM3, 波特率 9600, 设备从站地址为 1
client = ModbusClient(method='rtu', port='COM3', baudrate=9600, stopbits=1, bytesize=8, parity='N')
client.connect()# 电表的从站ID
SLAVE_ID = 1# --- 关键在这里 ---
# 我们在手册上看到的地址是 40101
# 我们计算出的协议地址是 100
# 我们要读取1个寄存器
try:# 调用 "read_holding_registers" 函数# 第一个参数是起始的协议地址 (100)# 第二个参数是要读取的寄存器数量 (1)# 第三个参数是从站IDresponse = client.read_holding_registers(address=100, count=1, slave=SLAVE_ID)if not response.isError():# response.registers 是一个列表,包含读取到的值voltage_raw = response.registers[0]# 根据手册,单位是 0.1V,所以需要除以10voltage_real = voltage_raw / 10.0print(f"成功读取到A相电压: {voltage_real} V") # 例如: 成功读取到A相电压: 220.5 Velse:print(f"读取失败: {response}")except Exception as e:print(f"发生错误: {e}")finally:client.close()
在上面的代码中,我们传入的 address
参数是 100
,而不是 40101
。Modbus 库会把 100
转换为 0x0064
并构建协议帧。
第四步:分析实际的 Modbus RTU 协议帧 (底层视角)
当上述 Python 代码运行时,它通过串口发送出去的原始数据帧 (Request Frame) 会是这样的 (十六进制表示):
01 03 00 64 00 01 C5 D5
我们来解析一下这串数据:
01
: 从站地址 - 我们要访问的设备ID,即SLAVE_ID = 1
。03
: 功能码 -0x03
代表 "读取保持寄存器"。00 64
: 起始地址 (协议地址) - 这就是我们计算出的协议地址100
的十六进制表示 (0x0064
)。这直接回答了你的问题,帧里面用的是偏移量地址。00 01
: 寄存器数量 - 我们要读取1
个寄存器。C5 D5
: CRC校验 - 用于验证数据完整性的校验码。
总结
分清两个角色:设备手册地址是给人看的“路标”(如
40101
),协议地址是邮差(机器)实际使用的“内部编号”(如100
)。记住换算:在编程或配置工具时,你看到的是设备手册地址,但输入到软件或代码里的一定是减去偏移量 (
1
,10001
,30001
,40001
) 之后的协议地址。库的封装:现代的很多 Modbus 库和软件工具,为了方便用户,可能会允许你直接输入
40101
,它在内部自动帮你计算。但理解这个转换原理,能让你在遇到问题时,知道如何排查,并能看懂抓包工具捕获的原始数据。
希望这个结合场景的解释能让你彻底明白这个机制!