树莓派遇上pymodbus:用异步通信打破工业数据采集的“卡顿”困局

你有没有遇到过这样的场景?在做一个多设备监控项目时,树莓派连着十几个Modbus传感器,每次轮询一圈要好几秒——明明每个设备响应很快,但串行读取下来就是慢得让人抓狂。更糟的是,一旦某个老式仪表通信超时,整个程序就卡住不动了。

这不是代码写得差,而是传统同步通信模型本身的硬伤。好消息是,这个问题早有解法: 把 pymodbus 的异步模式和树莓派的计算能力结合起来,实现真正的并发读取 。今天我们就来拆解这套方案,不讲虚的,只说实战中怎么干、为什么有效、踩过哪些坑。


为什么你的Modbus轮询总那么“拖沓”?

先别急着优化,咱们得搞清楚问题出在哪。

假设现场有10个Modbus从站设备,平均每个通信耗时200ms(这其实已经算快了)。如果采用传统的同步方式逐个访问:

总耗时 ≈ 10 × 200ms = 2秒

也就是说,你想每秒更新一次数据?做不到。最快也只能每两秒拿到一轮完整数据。而且只要其中一个设备掉线或响应慢,比如花了1.5秒才返回超时错误,那这一整轮就得等1.5秒以上。

这就是典型的 I/O等待阻塞 —— CPU大部分时间都在空转,等着串口或者网卡传来数据。对于资源本就不宽裕的嵌入式设备来说,简直是浪费生命。

而解决办法也很直接: 让这些请求并行出去,谁先回来处理谁 。

Python里的 asyncio 加上支持异步的 pymodbus v3+ ,正好能干这事。它不像多线程那样吃内存、难调试,而是基于事件循环的协程机制,在单线程内高效调度多个I/O任务,特别适合这种高并发低计算负载的场景。


pymodbus 异步模式到底强在哪?

pymodbus 是一个纯Python写的Modbus协议栈,不用编译、跨平台、安装简单( pip install pymodbus 就完事),最关键的是从v3版本开始全面拥抱 asyncio 。

它能干什么?

  • 支持 Modbus TCP 和 RTU(RS485)
  • 可作为客户端(Master)发起请求,也能当服务器(Slave)响应
  • 覆盖所有常用功能码:读线圈、读保持寄存器、写单寄存器等等
  • 内置日志系统,DEBUG级别下连发送的十六进制报文都能看到

同步 vs 异步:差别不只是“快一点”

特性 同步模式 异步模式
并发能力 一次只能发一个请求 多个请求同时发出
阻塞行为 等待期间CPU干不了别的 请求发出后立即释放控制权
响应效率 受最慢设备拖累 快速设备优先返回结果
编程复杂度 直接调用函数即可 需理解await/async机制

举个例子:同样是读5台设备,同步模式像服务员挨桌点菜;异步模式则是同时给五张桌子递上菜单,哪桌填好了就先收哪张。

所以如果你面对的是混合速度设备(比如新PLC + 老温控表),异步的优势会非常明显。


树莓派真是工业网关的好选择吗?

有人可能会质疑:树莓派不是玩具吗?能扛得住工业环境?

坦率说,早期版本确实不太稳定,但现在 Pi 4B / Pi 5 已经完全不一样了。

我们来看看它的实际表现:

  • 处理器 :四核A72 @ 1.5GHz,跑几十个协程毫无压力
  • 内存 :最低2GB起,运行Python脚本绰绰有余
  • 网络 :千兆以太网 + 双频Wi-Fi,Modbus TCP轻松应对
  • 串口支持 :可通过USB转RS485模块接入总线,也可启用GPIO上的硬件UART
  • 系统稳定性 :Raspberry Pi OS 基于Debian,长期运行无压力

更重要的是,它成本低(一套带壳电源不到500元)、功耗小(满载也就5W左右)、扩展性强,还能跑Docker、Node-RED、InfluxDB这些工具链。

换句话说, 它既不像工控机那么贵,又比单片机强大得多 ,拿来当边缘网关再合适不过。


实战代码:如何真正实现“并发”读取?

下面这段代码,是我在线上项目里打磨过的精简版核心逻辑。重点不是炫技,而是告诉你每一行背后的考量。

import asyncio
from pymodbus.client import AsyncModbusTcpClient, AsyncModbusSerialClient
from pymodbus.exceptions import ModbusIOException
import logging

# 日志配置:方便排查通信问题
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
log = logging.getLogger(__name__)

设备列表定义:灵活适配混合拓扑

DEVICES = [
    # Modbus TCP设备
    {"type": "tcp", "host": "192.168.1.101", "port": 502, "slave_id": 1, "name": "PLC_A"},
    {"type": "tcp", "host": "192.168.1.102", "port": 502, "slave_id": 2, "name": "HMI_B"},

    # Modbus RTU设备(通过USB-RS485转换器)
    {
        "type": "rtu",
        "port": "/dev/ttyUSB0",
        "baudrate": 9600,
        "parity": 'N',
        "stopbits": 1,
        "bytesize": 8,
        "slave_id": 3,
        "name": "Sensor_C"
    },
]

✅ 提示 :你可以混用TCP和RTU设备,系统自动识别创建对应客户端。


单设备读取协程:连接 → 请求 → 解析 → 清理

async def read_from_device(device):
    client = None
    try:
        # 根据类型创建客户端
        if device["type"] == "tcp":
            client = AsyncModbusTcpClient(
                host=device["host"],
                port=device.get("port", 502),
                timeout=5  # 设置合理超时
            )
        elif device["type"] == "rtu":
            client = AsyncModbusSerialClient(
                port=device["port"],
                baudrate=device["baudrate"],
                parity=device["parity"],
                stopbits=device["stopbits"],
                bytesize=device["bytesize"],
                timeout=5
            )
        else:
            raise ValueError(f"不支持的设备类型: {device['type']}")

        # 连接
        await client.connect()
        if not client.connected:
            raise ConnectionError("连接失败")

        log.info(f"已连接 {device['name']}")

        # 发起读取(功能码0x03,地址0开始,读10个寄存器)
        result = await client.read_holding_registers(
            address=0,
            count=10,
            slave=device["slave_id"]
        )

        # 成功则提取数据
        if hasattr(result, 'registers'):
            return {
                "device": device["name"],
                "status": "success",
                "data": result.registers
            }
        else:
            return {
                "device": device["name"],
                "status": "error",
                "message": str(result)
            }

    except ModbusIOException as e:
        return {
            "device": device["name"],
            "status": "exception",
            "message": f"通信异常: {str(e)}"
        }
    except Exception as e:
        return {
            "device": device["name"],
            "status": "failure",
            "message": str(e)
        }
    finally:
        # 关闭连接(RTU需要显式关闭)
        if client and hasattr(client, 'close'):
            client.close()

🔍 关键点解析 :
- 每个设备独立连接,避免共用连接导致冲突;
- 使用 hasattr(result, 'registers') 判断是否成功,防止解析异常;
- finally 中确保连接关闭,防止资源泄露。


主协程:并发执行所有任务

async def main():
    # 创建所有设备的读取任务
    tasks = [read_from_device(dev) for dev in DEVICES]

    # 并发执行,并收集结果(即使某任务出错也不中断其他)
    results = await asyncio.gather(*tasks, return_exceptions=True)

    # 输出结果
    for res in results:
        if isinstance(res, dict):
            print(f"[{res['device']}] 状态: {res['status']}")
            if res['status'] == 'success':
                print(f"  数据: {res['data']}")
            elif 'message' in res:
                print(f"  错误: {res['message']}")
        else:
            print(f"任务异常: {res}")

if __name__ == "__main__":
    asyncio.run(main())

⚡️ 性能对比实测 :
- 同步方式读10台设备:平均耗时 1.8~2.2秒
- 异步方式并发读取:平均耗时 ~220ms (受限于最慢设备)
→ 效率提升接近 9倍


生产环境中必须注意的几个“坑”

上面的代码可以在开发环境跑通,但真要部署到车间,还得考虑更多现实因素。

1. 超时设置不能一刀切

timeout=5  # 太短?误判断线;太长?整体延迟上升

建议根据现场情况调整:
- 新型PLC/TCP设备:2~3秒足够
- 老式仪表/长距离RS485:可设为5~10秒

2. 不要频繁重连

每次 .connect() 都涉及TCP握手或串口初始化,开销不小。如果采样频率高(如每秒一次),应该考虑 长连接复用 ,而不是每次读完就断。

💡 方案思路:将client实例缓存起来,定期检测连接状态,断开后再重建。

3. 控制并发数量

虽然理论上可以并发上百个任务,但树莓派毕竟不是服务器。建议:
- 小规模系统(<20设备):全量并发没问题
- 大系统:分组轮询,每组5~10个,并搭配定时器(如APScheduler)

4. 加个心跳机制

有些设备看似在线,其实已失去响应。建议额外加一个“心跳位”读取(比如固定地址0x0000),用于判断设备活跃度。

5. 用systemd守护进程

别手动运行脚本!用systemd管理服务,崩溃后自动重启:

# /etc/systemd/system/modbus-agent.service
[Unit]
Description=Modbus Async Reader
After=network.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/modbus-app
ExecStart=/usr/bin/python3 main.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

然后启用服务:

sudo systemctl enable modbus-agent
sudo systemctl start modbus-agent

它适合哪些应用场景?

这套组合拳最适合以下几类项目:

✅ 工业边缘网关

  • 连接PLC、变频器、电表等Modbus设备
  • 本地预处理数据后上传云平台(如ThingsBoard、阿里云IoT)

✅ 能源管理系统

  • 实时采集各回路电压、电流、功率
  • 异常波动触发告警(微信/短信通知)

✅ 楼宇自动化

  • 读取空调、照明、门禁系统的状态
  • 结合时间策略自动控制

✅ 农业物联网

  • 多节点土壤温湿度传感器采集
  • 低功耗+无线传输+周期上报

最后一点思考:异步真的值得学吗?

我知道很多人看到 async/await 就头大,觉得“我又不是做Web的,干嘛搞这么复杂”。

但现实是: 现代嵌入式系统越来越需要处理多源I/O任务 。无论是串口、网络、定时器还是MQTT消息,都逃不开并发问题。

而 asyncio 提供了一种轻量级、高效的解决方案。比起多线程的锁竞争和上下文切换开销,协程更贴近嵌入式场景的需求。

更何况,你现在写的每一行异步代码,未来都可以无缝迁移到ESP32-S3、RP2040这类支持MicroPython的高性能MCU上。


如果你正在为数据采集延迟发愁,不妨试试这个组合:
👉 树莓派 + pymodbus + asyncio

你会发现,原来那个“卡卡的”系统,也可以变得飞快。

对了,文中的完整代码我已经整理好放在GitHub,欢迎 clone 下来直接跑。你在实际部署中遇到什么问题?欢迎留言讨论。

更多推荐