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获取具体异常码。建议根据异常码做针对性处理,如地址错误应检查配置,设备故障应触发告警并重试。

相关推荐
西柚研究生1234569 分钟前
论文分析17:YOLOv11_UAVNet:无人机航拍图像专用目标检测算法
人工智能·python·深度学习·算法·目标检测
RAN114267600818 分钟前
1.3第一个程序的介绍
开发语言·qt
沧海一笑-dj42 分钟前
【Python】Python学习笔记-初识 Python
python·python安装·python解释器
打工仔折腾 AI1 小时前
FastAPI 从本机到生产服务器:Nginx+Gunicorn+Uvicorn 完整部署实录
人工智能·后端·python·nginx·fastapi·gunicorn
blue_ice .1 小时前
Verilog DDS 数字信号发生器:DAC 输出模拟波形与 ADC 回采观测
开发语言·单片机·嵌入式硬件·fpga开发
hanchenxing1 小时前
AI 数据管道重构:从函数堆到链式管道的可读性实践
python
hhzz1 小时前
【OpenCV 入门到精通 10】视频分析与光流跟踪:背景减除与运动检测
人工智能·python·opencv·性能优化
蓝胖的四次元口袋1 小时前
Python基础语法入门
python
远翔调光芯片^138287988721 小时前
ECP5702能芯科技PD取电芯片在市场上的优势有哪些?
开发语言·人工智能·单片机·嵌入式硬件·智能家居
Patrick在香港1 小时前
Python 抓 0.91 GB 香港法例:Agent 的进度该写进磁盘,不是写进上下文
爬虫·python·api·claude·香港