Python实现工业一体机Modbus-TCP通信-从协议解析到多设备轮询实战

工业一体机在产线数据采集场景中,经常需要通过Modbus TCP协议与PLC、仪表和传感器等设备通信。本文基于pymodbus库,从协议基础到多设备轮询实战,完整记录在Linux工业一体机上部署Modbus TCP客户端的全过程。

Modbus TCP协议基础

Modbus TCP是Modbus协议的以太网实现版本,协议规范由Modbus组织维护(MB/TCP规范文档)。相比Modbus RTU的串行通信,TCP版本去掉了CRC校验(由TCP层保证可靠性),增加了MBAP报文头(Modbus Application Protocol Header),固定7字节长度。

一个完整的Modbus TCP请求帧结构如下:

```

事务ID(2B)\] \[协议ID(2B)\] \[长度(2B)\] \[单元ID(1B)\] \[功能码(1B)\] \[数据(NB)

```

常用功能码:

  • 0x03 读保持寄存器

  • 0x04 读输入寄存器

  • 0x06 写单个寄存器

  • 0x10 写多个寄存器

环境搭建

工业一体机运行Debian 12,Python 3.11。先装依赖:

```bash

pip install pymodbus==3.6.3

pip install pyserial # 如果还需要Modbus RTU over TCP

```

确认网络连通性,假设PLC的IP是192.168.1.50,Modbus TCP端口默认502:

```bash

ping 192.168.1.50

确认能通之后测试端口

nc -zv 192.168.1.50 502

```

单设备通信实现

先写一个最基本的单设备读取:

```python

from pymodbus.client import ModbusTcpClient

class ModbusDevice:

def init(self, ip, port=502, timeout=3, unit_id=1):

self.client = ModbusTcpClient(

ip, port=port, timeout=timeout

)

self.unit_id = unit_id

def connect(self):

if not self.client.connect():

raise ConnectionError(f"无法连接到设备 {self.client.host}:{self.client.port}")

return self

def read_holding(self, address, count=1):

resp = self.client.read_holding_registers(

address=address,

count=count,

slave=self.unit_id

)

if resp.isError():

raise IOError(f"读取保持寄存器失败: {resp}")

return resp.registers

def write_register(self, address, value):

resp = self.client.write_register(

address=address,

value=value,

slave=self.unit_id

)

if resp.isError():

raise IOError(f"写入寄存器失败: {resp}")

return True

def close(self):

self.client.close()

```

用起来很简单:

```python

try:

dev = ModbusDevice("192.168.1.50").connect()

读地址0开始的4个保持寄存器

values = dev.read_holding(address=0, count=4)

print(f"寄存器值: {values}")

写单个寄存器

dev.write_register(address=10, value=100)

dev.close()

except Exception as e:

print(f"通信异常: {e}")

```

多设备轮询的坑

单设备通信跑通了就上多设备。产线上一台工业一体机通常要接好几台PLC或仪表,轮询的时候有几个坑我是踩过的。

**第一个坑:连接复用 vs 每次新建连接**

Modbus TCP的连接是长连接,按理说建一次连接后可以反复用。但实际在工业现场,网络偶发抖动会导致TCP连接半断开状态,你的程序以为连着实际已经断了。我的做法是每轮完整采集后断开重连,牺牲一点延迟换稳定性。

**第二个坑:轮询间隔太短导致PLC响应不过来**

有些低端PLC处理Modbus请求的能力有限,你刚发完上一个请求还没等它处理完就发下一个,它会直接丢包。我测过某国产PLC,连续请求间隔小于50ms就丢数据。

**第三个坑:异常不处理导致整个轮询卡死**

一台设备通信超时了,如果没有超时控制,整个轮询线程就卡在那里。后面的设备都读不到了。

针对这几个坑,对比一下串行和并行两种轮询方案的差异:

| 轮询方案 | 4台设备单轮耗时 | 稳定性 | 实现复杂度 | 适用场景 |

| --- | --- | --- | --- | --- |

| 串行轮询 | 约2.4秒 | 较高 | 低 | 设备少、实时性要求不高 |

| 并行轮询 | 约0.8秒 | 需处理线程异常 | 中 | 设备多、要求快速采集 |

| 并行+每次新建连接 | 约1.0秒 | 高 | 中高 | 网络不稳定的车间 |

| 串行+连接复用 | 约0.6秒 | 网络抖动时低 | 低 | 网络稳定的洁净环境 |

下面是我改进后的多设备轮询实现:

```python

import time

import threading

from dataclasses import dataclass, field

from concurrent.futures import ThreadPoolExecutor, as_completed

@dataclass

class DeviceConfig:

name: str

ip: str

port: int = 502

unit_id: int = 1

registers: list = field(default_factory=list) # (地址, 数量), ...

interval_ms: int = 100 # 轮询间隔

class MultiDevicePoller:

def init(self, configs, max_workers=4):

self.configs = configs

self.pool = ThreadPoolExecutor(max_workers=max_workers)

self.results = {}

self._running = False

def _poll_single(self, config):

"""单设备采集,每次新建连接"""

result = {"name": config.name, "data": {}, "error": None}

try:

dev = ModbusDevice(

config.ip, config.port,

timeout=2, unit_id=config.unit_id

).connect()

for addr, count in config.registers:

values = dev.read_holding(address=addr, count=count)

result"data"addr = values

寄存器间留间隔,避免PLC处理不过来

time.sleep(config.interval_ms / 1000.0)

dev.close()

except Exception as e:

result"error" = str(e)

return result

def poll_once(self):

"""并行轮询所有设备"""

futures = \[\]

for cfg in self.configs:

f = self.pool.submit(self._poll_single, cfg)

futures.append(f)

for f in as_completed(futures, timeout=10):

r = f.result()

self.resultsr\["name"] = r

return dict(self.results)

def start_loop(self, interval_s=1.0):

"""持续轮询循环"""

self._running = True

while self._running:

results = self.poll_once()

for name, r in results.items():

if r"error":

print(f"{name} 采集失败: {r'error'}")

else:

print(f"{name} 数据: {r'data'}")

time.sleep(interval_s)

def stop(self):

self._running = False

self.pool.shutdown(wait=False)

```

配置和使用

```python

if name == "main":

configs = [

DeviceConfig(

name="主PLC",

ip="192.168.1.50",

unit_id=1,

registers=(0, 10), (100, 4),

interval_ms=80

),

DeviceConfig(

name="温度仪表",

ip="192.168.1.51",

unit_id=2,

registers=(0, 2),

interval_ms=100

),

DeviceConfig(

name="电能表",

ip="192.168.1.52",

unit_id=3,

registers=(0x200, 8),

interval_ms=120

),

]

poller = MultiDevicePoller(configs, max_workers=3)

poller.start_loop(interval_s=1.0)

```

性能优化建议

实测4台设备并行轮询,单轮采集耗时从串行的2.4秒降到并行的0.8秒左右。几个优化点记录一下:

  1. **调整max_workers**:不要超过工业一体机的CPU核心数。RK3568是4核,max_workers设3到4个比较合适,留一个核给系统。

  2. **超时设短**:timeout=2秒比默认的3秒好。正常响应在100ms内,2秒还没响应基本就是有问题了。

  3. **指数退避重连**:设备偶尔超时不用立刻报错,重试一次再报。我在_poll_single里加了简单重试逻辑:

```python

for attempt in range(2):

try:

dev = ModbusDevice(config.ip, ...).connect()

... 读取逻辑

dev.close()

break

except Exception as e:

if attempt == 0:

time.sleep(0.5)

continue

result"error" = str(e)

```

  1. **日志落盘**:异常频率做统计比实时打印有用。把error写入CSV,定期分析哪台设备通信最不稳定。

常见问题(FAQ)

**Q:Modbus TCP和Modbus RTU在寄存器地址映射上有什么区别?**

寄存器地址映射本身没有区别,两者的地址空间定义一致(保持寄存器0x类、输入寄存器3x/4x等)。主要区别在于传输层和报文格式。Modbus TCP通过MBAP头中的Unit ID实现多设备寻址,而RTU通过总线上的从站地址寻址。代码层面使用pymodbus时,TCP用slave参数对应Unit ID,RTU用slave参数对应从站地址。

**Q:工业一体机上同时跑Modbus TCP和Modbus RTU(串口)需要注意什么?**

需要在代码中分别创建TCP客户端和RTU客户端实例,互不干扰。RTU通信使用串口(如/dev/ttyS0),TCP使用网络套接字。需要注意的是两者不要在同一个线程中交替调用,否则串口的阻塞等待可能影响TCP的轮询节奏。建议将RTU和TCP分别放在独立线程或使用异步框架(如asyncio)管理。

**Q:如何处理Modbus TCP通信中PLC返回的异常码?**

Modbus协议定义了5种标准异常码:01非法功能码、02非法数据地址、03非法数据值、04从站设备故障、05确认。pymodbus在resp.isError()为True时可以通过resp.exception_code获取具体异常码。建议根据异常码做针对性处理,如地址错误应检查配置,设备故障应触发告警并重试。

相关推荐
fpcc24 分钟前
跟我学C++中级篇—static_assert和assert
开发语言·c++
2401_8858850425 分钟前
国际语音php接口代码示例:PHP使用cURL快速调用语音发送API
android·开发语言·前端·人工智能·python·php·语音识别
萌动的小火苗33 分钟前
Linux进程线程面试题【无答案】
linux·运维·服务器·c语言·开发语言
EW Frontier35 分钟前
基于 Python + NumPy 的雷达信号处理仿真平台设计与实现——从 LFM 波形到 CFAR 检测的完整信号链
python·雷达信号处理·cfar\]·mti·mtd
axinawang44 分钟前
Tesseract入门--把图片里印刷文字提取成文本
python
青 春 记 忆1 小时前
LeetCode 283. 移动零|Python 解法详解
python·算法·leetcode
Zenova EdgeOS1 小时前
C++ 工业边缘 Boost.Asio 高级实战
开发语言·c++·边缘计算·工业网关
mqiqe1 小时前
AgentScope Java 2.0 技能仓库(Skill Repository)完全实战指南
java·开发语言·elasticsearch
人工干智能1 小时前
科普:Python中的生成器——带`yield`的函数
开发语言·python