Linux上搭建Modbus TCP服务端:pymodbus 3.x实战与避坑指南

发布时间:2026/10/23 17:53:24
Linux上搭建Modbus TCP服务端:pymodbus 3.x实战与避坑指南 简介针对Linux平台的TCPModbus服务端实现面向工业自动化开发者、嵌入式工程师及Linux网络编程人员解决工业现场中Modbus设备通过TCP/IP网络接入上位机或网关的问题。压缩包共4个文件约10KB包含C语言主程序tcpmodbusserver.c、线程封装头文件linux_thread.inc、Makefile构建脚本及编译好的可执行程序结构精简既可直接编译运行学习也可作为模块集成到自有项目中。已有971人学习下载。代码实现了Modbus协议常用的三类寄存器操作输入寄存器0x04和离散输入0x02为只读保持寄存器支持读0x03、单写0x06和多写0x10并通过多线程并发处理客户端请求其中输入寄存器对应传感器测量值离散输入对应开关状态保持寄存器用于设定参数完整展示了从协议解析、地址映射到响应组装的工业通信流程。配合源代码和构建脚本对理解Modbus TCP从站开发、并发服务设计及工业数据采集均有实际帮助适合作为入门学习、课程设计或原型验证的基础。1. Linux 上搭 Modbus TCP 服务端版本坑比你想的多先解决装哪个某天我接到一个 PLC 联调任务需要在 Linux 上起一个 Modbus TCP 从站服务端模拟一批温湿度传感器让上位机那头的 MES 系统先把流程跑起来。真正动手时才发现最坑的不是能不能装而是照着网上旧教程装完直接报错同一个 pymodbus 库2.x 和 3.x 的导入路径、服务端写法完全两码事复制下来没一个能跑。这篇记录我最终落地的一套方案Ubuntu 22.04 LTS、Python 3.10 虚拟环境、pymodbus 3.7.x从环境准备、服务端脚本、systemd 托管到寄存器映射和字节序那些容易翻车的细节一直写到用客户端工具做读写回归验证。适合刚接触 Modbus 联调的新手也适合被老教程坑过、想直接拿到能跑版本的老手。2. 环境准备与版本选型Ubuntu 22.04 Python 3.10 pymodbus 3.72.1 为什么我最后选了 pymodbus 而非自写 C 服务端Linux 上实现 Modbus TCP 服务端常见的路子有三条用 C/C 直接调 libmodbus 自己写用 libmodbus 自带的 diagslave 命令行工具模拟或者用 Python 的 pymodbus 库写一个服务端进程。C 方案在性能和生产环境确实有优势但要处理交叉编译、链接参数、内存释放这些杂事为了一次模拟设备和联调任务去写 C投入产出比不高。diagslave 虽然零代码就能跑缺点是可编程能力太弱——寄存器初始化逻辑、按周期更新数值、多从站场景都不好扩展。最后我选了 pymodbus理由很实际部署快数据块模型直观改寄存器映射就是改一个 Python 列表而且 3.x 版本支持同步和异步两种服务端能覆盖大多数模拟和设备联调场景。方案上手成本扩展能力适合场景C/C libmodbus高要编译强生产环境嵌入式、高并发采集diagslave 模拟器低装完即用弱难写逻辑临时测试连通性Python pymodbus低pip 安装中代码可扩展联调模拟、数据中台测试、教学 Demo版本选型这里要单独说一句pymodbus 2.x 已经停止维护网上大量教程都是 2.x 时代的写法比如from pymodbus.server.sync import StartTcpServer这个路径在 3.x 里直接被删掉了。我装的时候 pip 默认就给装 3.7.x所以除非你项目里有存量代码锁定在 2.x否则新项目直接上 3.x别再去看老教程的导入路径。3.x 里同步和异步的服务端入口统一收敛到了pymodbus.server包后面写代码会用到。2.2 创建虚拟环境并安装 pymodbus环境准备的第一步是确认系统自带的 Python 版本。Ubuntu 22.04 默认带 Python 3.10对 pymodbus 3.7.x 完全足够。先用 apt 把虚拟环境和 pip 组件补齐sudo apt update sudo apt install -y python3 python3-venv python3-pip python3 --version这段命令先把发行版自带的 Python 三件套装好。Ubuntu 的 base 系统里默认有 python3但python3-venv经常缺席不装的话下一步创建虚拟环境会直接报ensurepip is not available。版本检查输出的应该是 Python 3.10.x这个版本号后面排障时可以用来排除Python 太老或太新的问题。接下来在统一工作目录里建虚拟环境安装依赖sudo mkdir -p /opt/modbus-server sudo chown $USER:$USER /opt/modbus-server cd /opt/modbus-server python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install pymodbus3.7.4我把服务端放进/opt/modbus-server这是 Linux 上放第三方自研服务的常用位置和用户自己的家目录分开后面配 systemd 也更整洁。python3 -m venv venv创建虚拟环境source venv/bin/activate激活它激活后命令行提示符前会出现(venv)前缀这说明接下来的 pip 都装进隔离环境了。固定pymodbus3.7.4是为了可复现——如果直接pip install pymodbus装到 3.8 或更高版本时接口又有可能变。3.7.4 是我验证过 API 稳定的一代。2.3 锁定依赖版本pymodbus 安装时会自动拉入几个运行依赖比如 pyserial 和 typing-extensions。多人协作或者换服务器重新部署时光记一个主版本号是不够的最好把所有依赖版本锁定进文件pip list pip freeze requirements.txt cat requirements.txtpip freeze输出的文件里应该能看到pymodbus3.7.4、pyserial3.5、typing-extensions4.9.0这几项。以后换机器的时候一行pip install -r requirements.txt就能装回完全一致的环境。我在实际部署时还会顺手把pip list的完整输出存一份万一现场环境装不了内网源拿着版本清单去手工找包也方便。2.4 快速验证库能否正常导入安装完先别急着写业务代码用一行命令验证服务端入口能不能导入python -c import pymodbus; print(pymodbus.__version__) python -c from pymodbus.server import StartTcpServer; print(server import ok)如果第一行打印出版本号但第二行报ModuleNotFoundError说明你装的 pymodbus 版本不对或者当前激活的 Python 解释器不是虚拟环境里的。这时候检查一下which python确认在/opt/modbus-server/venv/bin/python下面。这一步是我每次装 Python 库之后都会做的冒烟测试十秒不到能拦下大半个环境问题。3. 部署最小服务端一份能跑的 modbus_server.py3.1 代码结构和四类数据块Modbus 从站的核心是数据模型。一个完整的从站要维护四块数据区离散输入 DI、线圈 CO、保持寄存器 HR、输入寄存器 IR。其中线圈和保持寄存器支持上位机读写离散输入和输入寄存器是只读的。pymodbus 里这四块对应ModbusSequentialDataBlock这个类只关心起始地址和初始值列表协议层解析交给框架处理。服务端进程要做的就是把四块数据准备好塞进ModbusSlaveContext再包一层ModbusServerContext传给 TCP Server 启动函数。这种分层结构和真实 PLC 内部的数据区模型是一致的。做传感器模拟时采集侧的瞬时值放到 HR 或 IR上位机通过功能码 0x03 读保持寄存器就能拿到需要控制设备的场景就用线圈 CO 接收上位机写来的开关命令。理解了这个模型后面改寄存器映射就是改初始值列表的事不动框架代码。3.2 完整的服务端示例代码#!/usr/bin/env python3 # -*- coding: utf-8 -*- Linux Modbus TCP 从站服务端 依赖: pymodbus 3.7.x 运行: python modbus_server.py from pymodbus.server import StartTcpServer from pymodbus.datastore import ( ModbusSequentialDataBlock, ModbusSlaveContext, ModbusServerContext, ) def build_context(): # 离散输入 DI: 从地址 0 开始, 10 个只读开关量 di ModbusSequentialDataBlock(0, [0] * 10) # 线圈 CO: 从地址 0 开始, 10 个可读写开关量 co ModbusSequentialDataBlock(0, [0] * 10) # 保持寄存器 HR: 从地址 0 开始, 100 个 16 位寄存器 hr ModbusSequentialDataBlock(0, [0] * 100) # 输入寄存器 IR: 从地址 0 开始, 50 个只读寄存器 ir ModbusSequentialDataBlock(0, [0] * 50) slave_store ModbusSlaveContext( didi, coco, hrhr, irir, zero_modeTrue ) # singleTrue 表示只模拟一个从站, 从站 ID 固定为 1 return ModbusServerContext(slavesslave_store, singleTrue) if __name__ __main__: context build_context() # 监听 0.0.0.0:5020; 换到标准端口 502 需要 root 权限或 setcap StartTcpServer(contextcontext, address(0.0.0.0, 5020))ModbusSequentialDataBlock(0, [0] * 100)的第一个参数是数据块起始地址第二个参数是初始化值列表长度决定了这个数据块有多少个点。我这里把 DI 和 CO 都开了 10 个点HR 开了 100 个寄存器IR 开了 50 个足够模拟一台小型设备。zero_modeTrue表示数据块内部地址从 0 开始这是 pymodbus 的默认行为保持默认就不会出现地址错位。singleTrue是单从站模式上位机不管发什么从站 ID服务端都响应联调阶段最省事。3.3 启动并确认监听端口source /opt/modbus-server/venv/bin/activate cd /opt/modbus-server python modbus_server.py ss -lntp | grep 5020python modbus_server.py 让进程在后台跑输出会打到当前终端。紧接的ss -lntp | grep 5020用来确认端口真的被监听看到0.0.0.0:5020且进程名带python就说明服务端已经起来了。这里必须强调看一眼监听地址如果显示127.0.0.1:5020说明绑到了回环地址只等于本机能访问外部的 PLC 和上位机全都会连接失败这一点后面避坑章节还会展开。后台用跑只是临时验证下一步立刻换 systemd 托管。3.4 用 systemd 托管开机自启手动后台跑的服务一关终端就没了生产联调时不能这么干。我一般用 systemd 把服务托管起来保证崩了自动拉起、开机自启。先创建专用运行用户再写 service 文件sudo useradd -r -s /usr/sbin/nologin modbus sudo chown -R modbus:modbus /opt/modbus-server[Unit] DescriptionModbus TCP Server Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Usermodbus Groupmodbus WorkingDirectory/opt/modbus-server ExecStart/opt/modbus-server/venv/bin/python /opt/modbus-server/modbus_server.py Restartalways RestartSec5 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target这个 service 文件里有几个参数是踩坑总结出来的。Usermodbus用普通用户跑服务避免 Python 进程拿到 root 权限万一被渗透也不至于直接控制整台机器。ExecStart必须写虚拟环境里 Python 解释器的绝对路径写成python modbus_server.py的话 systemd 会在自己的 PATH 里找 python大概率找到系统 Python然后 import 不到 pymodbus。Restartalways配合RestartSec5让服务在崩溃后 5 秒自动拉起联调现场没人盯着服务器时这个参数能救命。PYTHONUNBUFFERED1让日志实时刷到 stdout否则 systemctl 看日志会有几秒延迟。把文件放进 systemd 并启动sudo tee /etc/systemd/system/modbus-server.service /dev/null EOF 粘贴上面的 service 内容 EOF sudo systemctl daemon-reload sudo systemctl enable --now modbus-server systemctl status modbus-serverdaemon-reload必须执行systemd 才会重新读取新增的 service 文件。enable --now是开机自启 立即启动的合并写法。systemctl status输出里有Active: active (running)就说明托管成功。这个时候服务端已经是一个标准系统服务了配合下一节的防火墙放行外部设备就能连进来。4. 配置与扩展寄存器映射、从站 ID 与字节序4.1 地址、端口和从站 ID 的改法最小服务端跑起来后下一步就是按实际设备改配置。地址元组里的(0.0.0.0, 5020)第一个元素是监听地址第二个是端口。联调场景我无脑写0.0.0.0让服务端接受任意网卡上的请求如果服务端要同时跑多个实例做隔离可以换成具体网卡 IP。端口这里有个矛盾点Modbus TCP 标准端口是 502但 502 属于特权端口小于 1024非 root 进程没权限监听。两个选择要么端口继续用 5020 这类高位端口让上位机连接时把端口改掉要么用 root 跑服务或者给 Python 解释器加cap_net_bind_service能力。我的习惯是测试阶段用 5020进生产环境前再改成 502 并给解释器加能力位避免服务以 root 身份长期运行。多从站场景是联调里最常见的需求比如同时模拟 4 台温湿度传感器每台是一个从站 ID。把singleTrue换成多从站字典就能实现slaves { 0x01: ModbusSlaveContext(didi1, coco1, hrhr1, irir1), 0x02: ModbusSlaveContext(didi2, coco2, hrhr2, irir2), } context ModbusServerContext(slavesslaves, singleFalse)这里 1 号从站和 2 号从站的数据块要各自建一份不能共用一个 DataBlock否则上位机写 1 号从站时 2 号从站的数据也被改掉。多从站模式下上位机发给哪个单元 ID服务端就路由到哪个从站的存储区。实际联调时我会把设备地址表打印出来贴在工位上例如 1 号传感器地址 0x01、2 号 0x02省得现场猜地址。4.2 四类数据区对照表配置寄存器映射前先把四类数据区对应的功能码和读写特性列清楚照着表填数据不容易出幺蛾子数据区功能码读写方向典型用途pymodbus 参数线圈 CO0x01 读、0x05 写单、0x0F 写多可读写开关命令、启停控制co离散输入 DI0x02 读只读限位开关、按钮状态di保持寄存器 HR0x03 读、0x06 写单、0x10 写多可读写设定值、参数配置、浮点数据hr输入寄存器 IR0x04 读只读传感器实时值、采集结果ir联调时最常见的组合是传感器实时值放 IR主站用功能码 0x04 去读设备参数和阈值放 HR主站既能读也能写启停信号放 CO。如果只需要单向上报数据IR 就够用还能避免误写。4.3 模拟真实设备温湿度传感器寄存器单纯的全 0 寄存器只能验证连通性模拟真实设备必须把传感器特征值填进数据块。以 4 路温湿度传感器为例我通常约定每路占 2 个保持寄存器前一个存温度乘以 10 后取整后一个存湿度乘以 10 后取整全部从地址 0 开始排布hr_initial [0] * 20 # 1 号通道: 温度 25.5℃ - 255, 湿度 60.0% - 600 hr_initial[0] 255 hr_initial[1] 600 # 2 号通道: 温度 26.2℃ - 262, 湿度 58.5% - 585 hr_initial[2] 262 hr_initial[3] 585 hr ModbusSequentialDataBlock(0, hr_initial)这里有个关键技术取舍Modbus 寄存器是 16 位无符号整数没有浮点原生类型所以温度 25.5℃ 不能直接存必须放大 10 倍变成 255 再存客户端读到 255 后自己除以 10 还原。放大系数是协议的一部分必须写进接口文档和上位机研发对齐——我见过好几次因为倍率没对齐现场温度读出来是 2550℃ 的重大事故其实是 255 和 2550 的差距。4.4 字节序同一个寄存器谁和谁看到的不一样如果设备数据涉及 32 位浮点或者 32 位整数字节序就是绕不过去的坑。比如温度值用 IEEE 754 单精度浮点表示上位机要从连续两个 16 位寄存器里拼出这个浮点数拼接顺序决定了最终数值。常见顺序有三种大端序 ABCD高字在前、小端序 CDAB低字在前、还有 BADC 这种寄存器内部字节交换的变体。我的经验是先约定寄存器协议为H寄存器存高 16 位、L寄存器存低 16 位即大端序服务端按这个约定填值客户端按同样顺序解析。import struct def float_to_regs(value): # 把 float 按大端序拆成两个 16 位寄存器值 raw struct.pack(f, value) return struct.unpack(HH, raw) def regs_to_float(regs): # 两个 16 位寄存器按大端序拼回 float raw struct.pack(HH, regs[0], regs[1]) return struct.unpack(f, raw)[0]f里的表示大端序f表示单精度浮点。服务端把值拆进寄存器时用float_to_regs客户端解析回来用regs_to_float两边只要都是大端序就不会出问题。如果现场设备是国产小端序的把这个函数里的改成就行。字节序问题最磨人之处在于它不报错——数值看似正常但在小数点后几个零上差着量级等到联调阶段才暴露就晚了。我一般会在写数据块时打印一份十六进制原始值跟上位机返回值做对照一眼就能看出顺序对不对。5. 常见问题排查与避坑五条真实踩坑记录5.1 现象import 直接报 ModuleNotFoundError照着老教程写from pymodbus.server.sync import StartTcpServer一运行就报ModuleNotFoundError: No module named pymodbus.server.sync。原因是 pymodbus 3.x 重构后删掉了pymodbus.server.sync和pymodbus.server.async这两个子模块同步和异步入口统一放到了pymodbus.server下同样的迁移还发生在客户端2.x 的from pymodbus.client.sync import ModbusTcpClient在 3.x 里改成了from pymodbus.client import ModbusTcpClient。解决方式是直接改用 3.x 的统一导入路径老代码要么迁移要么锁回 2.x不存在兼容层。这个坑的根源是搜到的教程年代太久装库之前先确认版本比什么都重要。5.2 现象外部设备连不上本机自己却正常现象很典型服务器本机telnet 127.0.0.1 5020秒开上位机通过网络访问192.168.x.x:5020就是连接超时。原因通常是两个叠加服务端启动时监听地址写成了127.0.0.1只对回环接口生效或者监听地址是0.0.0.0但防火墙把 5020 端口挡了。排查顺序是先用ss -lntp | grep 5020看监听地址再看防火墙状态和规则# Ubuntu / Debian 系 sudo ufw allow 5020/tcp sudo ufw status # Rocky Linux / CentOS 系 sudo firewall-cmd --add-port5020/tcp --permanent sudo firewall-cmd --reload sudo firewall-cmd --list-ports解决之后一定要从外部机器再测一次本机测试通过不代表网络路径通。我踩这个坑最狠的一次是监听地址和防火墙都没问题最后发现是云平台安全组没放行所以外部设备连不上时按服务端监听 - 本机防火墙 - 云平台安全组/物理网络ACL这个顺序逐层排查。5.3 现象能连上一读就超时TCP 连接建立成功但发请求后一直等到 timeout。这个现象大概率是从站 ID 不匹配。pymodbus 的singleTrue模式不校验从站 ID谁来都响应但如果改成singleFalse的多从站模式上位机用从站 ID 3 去读而服务端字典里只配了 1 和 2请求就会被丢弃表现就是连接正常但一轮询就超时。排查时先把日志级别调到 DEBUGimport logging logging.basicConfig(levellogging.DEBUG)服务端控制台或者 journalctl 里会打出收到的请求帧和单元 ID看到unit3而服务端没有对应从站就能锁定问题。解决方式是让上位机把单元 ID 改成服务端配置的从站 ID或者在服务端把缺失的从站补上去。联调现场这种情况极多我习惯把从站 ID 表写死在启动日志里第一行就打出来。5.4 现象寄存器数值翻倍或者减半写入一个浮点温度上位机读回来数值是预期值的一半或者干脆是一个天文数字。原因一是倍率没对上服务端按 ×10 存储上位机按 ×1 解析原因二是前面第 4.4 节说的字节序问题32 位浮点高低字拼接顺序不对。解决方式是把协议文档拿出来逐行核对倍率和字节序最快的方法是写一个已知值给服务端 HR 地址 0 写入一个特殊浮点数如 1.0十六进制 0x3F800000看上位机读回来是多少。如果读出来是一个数量级离谱的数不用猜了字节序反了。确定方案后统一改协议约定改完重新跑读写回归。5.5 现象systemd 启动失败journalctl 显示找不到模块服务手动在终端跑没问题systemctl start modbus-server却启动失败journalctl -u modbus-server里报ModuleNotFoundError: No module named pymodbus。最常见原因是 ExecStart 里写的是python modbus_server.py而不是虚拟环境解释器的绝对路径systemd 调用时 PATH 里没有/opt/modbus-server/venv/bin自然找到了系统 Python。还有一种隐蔽情况WorkingDirectory写错了Python 脚本里如果有相对路径读文件的逻辑会跟着一起炸。解决方式是把 ExecStart 改成/opt/modbus-server/venv/bin/python /opt/modbus-server/modbus_server.py然后systemctl daemon-reload systemctl restart modbus-server。看日志的时候用journalctl -u modbus-server -n 50只看最近 50 行比翻老日志省时间。6. 装完怎么验证mbpoll 和 Python 客户端读写回归6.1 安装 mbpoll 并读寄存器服务端和配置都就位后最后一步是验证。我习惯用 mbpoll 这个命令行工具做第一轮连通性验证它相当于一个极简 Modbus 主站sudo apt install -y mbpoll mbpoll -a 1 -r 1 -c 10 -t 3 127.0.0.1 -p 5020参数含义-a 1指定从站 ID 为 1-r 1是从起始地址 1 开始读mbpoll 显示地址从 1 开始对应协议地址 0-c 10读 10 个寄存器-t 3表示读保持寄存器127.0.0.1 -p 5020是目标和端口。正常输出会列出地址和数值如果读到的全是 0 而服务端初始化时填过数优先检查地址偏移。用 mbpoll 而不是自己写客户端来验证的另一个好处是它不带缓存每次发送的都是标准协议帧能真实反映服务端响应情况。6.2 自写 Python 客户端写读回读mbpoll 测完读还要测写。Modbus 服务端如果只支持读、写返回异常联调时同样会翻车。我一般用 pymodbus 的客户端模式做写读回归from pymodbus.client import ModbusTcpClient client ModbusTcpClient(127.0.0.1, port5020, timeout3) assert client.connect() # 写单个线圈: 地址 0, 置为 True client.write_coil(0, True, unit1) # 回读线圈 rl client.read_coils(0, 4, unit1) print(coil bits:, rl.bits[:4]) # 写保持寄存器: 地址 0, 写入 1234 client.write_register(0, 1234, unit1) # 回读保持寄存器 rr client.read_holding_registers(0, 2, unit1) print(hr registers:, rr.registers[:2]) client.close()这段代码的验证逻辑是写线圈地址 0 为 True再读 4 个线圈预期第一个是 True 其余 False写保持寄存器地址 0 为 1234再读 2 个寄存器预期第一个是 1234。如果读写都满足预期说明 CO 和 HR 两大数据区的写入通道是通的。我在实际联调任务里最后还会从外部上位机再发一次真实业务报文比如让 MES 系统读一次温湿度并确认界面刷新整条链路才算真正闭环。从那以后我每次搭完一个 Modbus TCP 从站都会强制走一遍 mbpoll 读寄存器加自写客户端写读回读的回归流程确认四类数据区、从站 ID、字节序全部对得上才把地址和协议文档交给现场同事这也让我躲开后续联调里大量的沟通成本。这篇安装笔记从环境选型、服务端脚本、systemd 托管一路写到这里加上避坑那几条覆盖了我自己在 Linux 上搭 Modbus TCP 服务端遇到的大部分问题希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询