Modbus RTU协议在工业物联网中的完全解析与实战
前言
工业现场90%以上的PLC、传感器、仪表还在跑Modbus RTU。搞工业物联网,Modbus是绕不过去的坎。本文从协议原理到代码实战,结合有人物联网USR-G780 DTU,彻底讲透。
一、Modbus RTU协议帧结构
┌─────────┬───────────┬──────────┬──────────┬───────┬───────┐ │ 地址码 │ 功能码 │ 数据区 │ CRC校验 │ │ │ │ 1 Byte │ 1 Byte │ N Bytes │ 2 Bytes │ │ │ └─────────┴───────────┴──────────┴──────────┴───────┴───────┘
地址码(Slave Address)
-
范围:1~247
-
每个从机唯一地址
-
0为广播地址(所有从机执行但不回复)
功能码(Function Code)
|
功能码 |
名称 |
操作对象 |
|---|---|---|
|
01 |
读线圈 |
开关量输出 |
|
02 |
读离散输入 |
开关量输入 |
|
03 |
读保持寄存器 |
模拟量(最常用) |
|
04 |
读输入寄存器 |
模拟量输入 |
|
05 |
写单线圈 |
单路控制 |
|
06 |
写单寄存器 |
单参数设置 |
|
15 |
写多线圈 |
批量控制 |
|
16 |
写多寄存器 |
批量参数 |
CRC校验
Modbus RTU用CRC-16校验,多项式 0xA001。这保证了工业环境强干扰下的数据完整性。
def modbus_crc(data): """Modbus CRC-16校验""" crc = 0xFFFF for byte in data: crc ^= byte for _ in range(8): if crc & 0x0001: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc.to_bytes(2, 'little')
二、实战一:Python读取Modbus温湿度传感器
""" 使用Python + RS485转USB读取Modbus RTU传感器数据 硬件:有人物联网USR-G780 DTU(透传模式)或USB转RS485模块 """ import serial import struct import time class ModbusRTUMaster: def __init__(self, port='COM3', baudrate=9600, timeout=1): self.ser = serial.Serial( port=port, baudrate=baudrate, bytesize=8, parity='N', stopbits=1, timeout=timeout ) def read_holding_registers(self, slave_id, start_addr, count): """功能码03:读保持寄存器""" # 构造请求帧 request = bytearray([ slave_id, # 地址码 0x03, # 功能码 (start_addr >> 8) & 0xFF, # 起始地址高字节 start_addr & 0xFF, # 起始地址低字节 (count >> 8) & 0xFF, # 寄存器数量高字节 count & 0xFF # 寄存器数量低字节 ]) # 追加CRC request += modbus_crc(request) # 发送 self.ser.write(request) time.sleep(0.1) # 接收响应 response = self.ser.read(5 + count * 2) if len(response) < 5: raise TimeoutError("从机无响应") # 验证CRC if modbus_crc(response[:-2]) != response[-2:]: raise ValueError("CRC校验失败") # 解析数据 data_bytes = response[3:-2] registers = [] for i in range(0, len(data_bytes), 2): reg = (data_bytes[i] << 8) | data_bytes[i+1] registers.append(reg) return registers def write_single_register(self, slave_id, addr, value): """功能码06:写单个寄存器""" request = bytearray([ slave_id, 0x06, (addr >> 8) & 0xFF, addr & 0xFF, (value >> 8) & 0xFF, value & 0xFF ]) request += modbus_crc(request) self.ser.write(request) time.sleep(0.1) response = self.ser.read(8) return len(response) == 8 def registers_to_float(high_reg, low_reg): """两个16位寄存器 → 32位浮点数(大端序)""" raw = (high_reg << 16) | low_reg return struct.unpack('>f', struct.pack('>I', raw))[0] # ===== 使用示例 ===== if __name__ == '__main__': master = ModbusRTUMaster(port='COM3', baudrate=9600) # 读取从机地址1的保持寄存器,从0x0000开始读4个 regs = master.read_holding_registers(slave_id=1, start_addr=0, count=4) # 解析数据(假设前两个存温度,后两个存湿度) temperature = registers_to_float(regs[0], regs[1]) humidity = registers_to_float(regs[2], regs[3]) print(f"温度: {temperature:.1f}°C") print(f"湿度: {humidity:.1f}%RH")
三、实战二:Modbus RTU → MQTT JSON 转换(DTU场景)
当你用4G DTU(如有人USR-G780)把Modbus数据发到云端时,需要做格式转换:
""" modbus_to_mqtt.py 运行在边缘网关或服务器上,将Modbus RTU数据转换为MQTT JSON """ import json import paho.mqtt.client as mqtt from datetime import datetime # Modbus点位映射表 POINT_MAP = { "温度传感器_01": { "slave_id": 1, "start_addr": 0, "count": 4, "fields": [ {"name": "temperature", "reg_index": 0, "type": "float32"}, {"name": "humidity", "reg_index": 2, "type": "float32"} ] }, "电表_01": { "slave_id": 2, "start_addr": 100, "count": 6, "fields": [ {"name": "voltage", "reg_index": 0, "type": "float32"}, {"name": "current", "reg_index": 2, "type": "float32"}, {"name": "power", "reg_index": 4, "type": "float32"} ] } } def collect_and_publish(master, mqtt_client): """采集所有设备数据并发布MQTT""" for device_name, config in POINT_MAP.items(): try: regs = master.read_holding_registers( slave_id=config['slave_id'], start_addr=config['start_addr'], count=config['count'] ) # 解析字段 payload = { "device": device_name, "timestamp": datetime.now().isoformat(), "data": {} } for field in config['fields']: if field['type'] == 'float32': value = registers_to_float( regs[field['reg_index']], regs[field['reg_index'] + 1] ) else: value = regs[field['reg_index']] payload['data'][field['name']] = round(value, 2) # 发布MQTT topic = f"/iot/{device_name}/data" mqtt_client.publish(topic, json.dumps(payload), qos=1) print(f"[OK] {device_name}: {payload['data']}") except Exception as e: print(f"[ERR] {device_name}: {e}") # MQTT连接 mqttc = mqtt.Client(client_id="modbus_gateway") mqttc.connect("broker.emqx.io", 1883, 60) mqttc.loop_start()
四、常见排查方法
问题1:读不到数据
# 用最基础的指令测试从机是否存在 test_frame = bytearray([0x01, 0x03, 0x00, 0x00, 0x00, 0x01]) test_frame += modbus_crc(test_frame) ser.write(test_frame) response = ser.read(7) print(response.hex()) # 有回复说明通信正常,没回复检查接线/波特率/地址
问题2:A/B线接反
-
万用表测量:A线通常3-5V,B线通常0-1V
-
交换A/B线试试是最快的方法
问题3:波特率不对
-
常见波特率:9600、19200、38400、115200
-
用串口助手逐个试,直到读到不乱码的数据
问题4:32位浮点数数值异常
-
注意字节序:大端/小端、ABCD/CDAB等排列
-
用已知值反推(比如发个25.5,看寄存器原始值)
五、有人物联网DTU的Modbus优化
有人物联网USR-G780等DTU内置了Modbus自动采集功能: 轮询间隔可配**:1-3600秒
-
批量读取:一次配置读取多个从机的多个寄存器
-
自动上报:采集后自动转MQTT JSON上报
-
无需写代码:通过配置工具即可完成
更多推荐


所有评论(0)