Skip to content

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
         └──────────────────── 从站地址:01

Modbus TCP 帧(MBAP Header)

┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────────────┐
│ 事务标识符│ 协议标识符│  长度    │ 单元标识符│ 功能码   │  数据            │
│  2 bytes │  2 bytes │  2 bytes │  1 byte  │  1 byte  │  N bytes         │
└──────────┴──────────┴──────────┴──────────┴──────────┴──────────────────┘

事务标识符:请求/响应匹配(客户端递增)
协议标识符:固定 0x0000(Modbus 协议)
长度:后续字节数(单元标识符 + PDU)
单元标识符:从站地址(TCP 直连时通常为 0xFF 或 0x01)

功能码详解

功能码名称操作数据类型
0x01Read Coils线圈(位,可读写)
0x02Read Discrete Inputs离散输入(位,只读)
0x03Read Holding Registers保持寄存器(16位,可读写)
0x04Read Input Registers输入寄存器(16位,只读)
0x05Write Single Coil单个线圈
0x06Write Single Register单个保持寄存器
0x0FWrite Multiple Coils多个线圈
0x10Write Multiple Registers多个保持寄存器
0x16Mask Write Register寄存器位掩码操作
0x17Read/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 平台

褚成志的IoT笔记