Modbus RTU/TCP 原理与生态
协议简介
Modbus 是 1979 年由 Modicon 公司发布的工业通信协议,是目前工业自动化领域使用最广泛的串行通信协议之一。其简单性、开放性和可靠性使其在 40 余年后仍是工业现场的主流选择。
Modbus 协议族:
├── Modbus RTU → 串行通信(RS-232/RS-485),二进制编码,最常用
├── Modbus ASCII → 串行通信,ASCII 编码,可读性好但效率低
├── Modbus TCP → 基于 TCP/IP,端口 502,工业以太网
└── Modbus Plus → Modicon 专有,已基本淘汰网络拓扑
Modbus RTU(RS-485 总线)
主站 (Master)
│
├── RS-485 总线(最长 1200m,最多 32 个节点)
│
├── 从站 01:变频器
├── 从站 02:PLC
├── 从站 03:电表
├── 从站 04:温控仪
└── 从站 05:BMS
接线规范:
- A(+) 和 B(-) 差分信号线
- 总线两端各接 120Ω 终端电阻
- 屏蔽层单端接地(主站侧)
- 线缆:双绞屏蔽线,推荐 RVSP 0.5mm²Modbus TCP(工业以太网)
SCADA 服务器 (Master)
│
├── 工业以太网交换机
│
├── 变频器(IP: 192.168.1.11, Port: 502)
├── PLC(IP: 192.168.1.12, Port: 502)
└── 智能电表(IP: 192.168.1.13, Port: 502)帧结构
Modbus RTU 帧
┌──────────┬──────────┬──────────┬──────────────────┬──────────┐
│ 从站地址 │ 功能码 │ 数据域 │ 数据 │ CRC │
│ 1 byte │ 1 byte │ N bytes │ │ 2 bytes │
└──────────┴──────────┴──────────┴──────────────────┴──────────┘
示例:读取从站 01 的保持寄存器 40001-40010
请求帧:01 03 00 00 00 0A C5 CD
│ │ │ │ │ └─ CRC 低字节
│ │ │ │ └─── CRC 高字节
│ │ │ └───────── 读取数量:10 个寄存器
│ │ └─────────────── 起始地址:0x0000(对应 40001)
│ └────────────────── 功能码:03(读保持寄存器)
└───────────────────── 从站地址:01
响应帧:01 03 14 00 01 00 02 ... CRC
│ │ │ └─────────── 数据(10个寄存器 = 20字节)
│ │ └────────────── 字节数:20(0x14)
│ └───────────────── 功能码:03
└──────────────────── 从站地址:01Modbus TCP 帧(MBAP Header)
┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────────────┐
│ 事务标识符│ 协议标识符│ 长度 │ 单元标识符│ 功能码 │ 数据 │
│ 2 bytes │ 2 bytes │ 2 bytes │ 1 byte │ 1 byte │ N bytes │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────────────┘
事务标识符:请求/响应匹配(客户端递增)
协议标识符:固定 0x0000(Modbus 协议)
长度:后续字节数(单元标识符 + PDU)
单元标识符:从站地址(TCP 直连时通常为 0xFF 或 0x01)功能码详解
| 功能码 | 名称 | 操作 | 数据类型 |
|---|---|---|---|
| 0x01 | Read Coils | 读 | 线圈(位,可读写) |
| 0x02 | Read Discrete Inputs | 读 | 离散输入(位,只读) |
| 0x03 | Read Holding Registers | 读 | 保持寄存器(16位,可读写) |
| 0x04 | Read Input Registers | 读 | 输入寄存器(16位,只读) |
| 0x05 | Write Single Coil | 写 | 单个线圈 |
| 0x06 | Write Single Register | 写 | 单个保持寄存器 |
| 0x0F | Write Multiple Coils | 写 | 多个线圈 |
| 0x10 | Write Multiple Registers | 写 | 多个保持寄存器 |
| 0x16 | Mask Write Register | 写 | 寄存器位掩码操作 |
| 0x17 | Read/Write Multiple Registers | 读写 | 同时读写 |
寄存器地址映射
Modbus 地址空间:
线圈(Coil): 00001 - 09999 → 功能码 01/05/0F
离散输入(DI): 10001 - 19999 → 功能码 02
输入寄存器(IR): 30001 - 39999 → 功能码 04
保持寄存器(HR): 40001 - 49999 → 功能码 03/06/10
注意:协议地址 = 寄存器地址 - 1(偏移量)
例:读取 40001 → 功能码 03,起始地址 0x0000
读取 40100 → 功能码 03,起始地址 0x0063数据类型编码
python
import struct
# 16位无符号整数(单寄存器)
def decode_uint16(registers, index=0):
return registers[index]
# 16位有符号整数
def decode_int16(registers, index=0):
value = registers[index]
return value if value < 32768 else value - 65536
# 32位浮点数(两个寄存器,大端序)
def decode_float32_be(registers, index=0):
raw = struct.pack('>HH', registers[index], registers[index+1])
return struct.unpack('>f', raw)[0]
# 32位浮点数(两个寄存器,小端序 - 部分设备使用)
def decode_float32_le(registers, index=0):
raw = struct.pack('>HH', registers[index+1], registers[index])
return struct.unpack('>f', raw)[0]
# 32位无符号整数
def decode_uint32_be(registers, index=0):
return (registers[index] << 16) | registers[index+1]
# 字符串(ASCII)
def decode_string(registers, length):
raw = b''
for reg in registers[:length//2]:
raw += struct.pack('>H', reg)
return raw.decode('ascii').rstrip('\x00')Python 实现示例
pymodbus 库
python
from pymodbus.client import ModbusTcpClient, ModbusSerialClient
from pymodbus.exceptions import ModbusException
import logging
# Modbus TCP 客户端
def read_bms_data_tcp(host: str, port: int = 502, unit_id: int = 1):
client = ModbusTcpClient(host=host, port=port, timeout=3)
try:
if not client.connect():
raise ConnectionError(f"Cannot connect to {host}:{port}")
# 读取 BMS 数据(假设寄存器映射)
# 40001-40010: 电芯电压(0.1mV 精度)
result = client.read_holding_registers(
address=0, # 40001 → 地址 0
count=10,
slave=unit_id
)
if result.isError():
raise ModbusException(f"Read error: {result}")
voltages = [reg * 0.1 for reg in result.registers] # 转换为 mV
return voltages
finally:
client.close()
# Modbus RTU 客户端
def read_meter_data_rtu(port: str = '/dev/ttyS1', unit_id: int = 1):
client = ModbusSerialClient(
port=port,
baudrate=9600,
bytesize=8,
parity='N',
stopbits=1,
timeout=1
)
try:
client.connect()
# 读取电表数据
result = client.read_input_registers(
address=0,
count=4,
slave=unit_id
)
if result.isError():
return None
voltage = decode_float32_be(result.registers, 0)
current = decode_float32_be(result.registers, 2)
return {"voltage": voltage, "current": current}
finally:
client.close()生态工具
调试工具:
├── Modbus Poll → Windows GUI 主站模拟器
├── Modbus Slave → Windows GUI 从站模拟器
├── ModRSsim2 → 免费从站模拟器
├── QModMaster → 跨平台开源工具
└── Wireshark → 抓包分析(Modbus TCP)
开发库:
├── Python: pymodbus, minimalmodbus
├── Java: j2mod, jamod
├── C/C++: libmodbus
├── Go: goburrow/modbus
└── .NET: NModbus4, EasyModbus
网关/转换器:
├── Neuron → 工业协议网关(支持 MQTT 桥接)
├── Kepware → 商业 OPC 服务器
├── Node-RED → node-red-contrib-modbus
└── Ignition → 工业 SCADA 平台