基于ModbusRTU和WebServer的PLC数据采集与监控系统

发布时间:2026/9/7 2:16:02
基于ModbusRTU和WebServer的PLC数据采集与监控系统 在工厂车间里PLC 控制柜每天都在产生大量设备数据但大多数时候这些数据只停留在触摸屏或者 PLC 程序内部。当你想在办公室实时看到设备温度、压力、产量和启停状态时往往只能跑到现场看。即使能远程看也常常依赖厂家私有协议或商业组态软件价格高、定制难、数据封闭。如果有一套成本低、方案开放、能快速搭建的数据采集系统把这些设备数据送到 Web 上展示很多中小型产线就能摆脱“数据孤岛”的尴尬。本文要讲的就是基于 ModbusRTU 和 WebServer 的工业数据采集项目。这个项目以 S7-200 SMART PLC 作为 ModbusRTU 从站设备使用 Python 作为采集端主站通过 RS485 串口读取 PLC 寄存器数据再交给自己实现的 WebServer 服务进行展示。整个过程不依赖商业组态软件从协议原理、PLC 配置、上位机采写到 Web 页面展示全部走开源和通用技术路线适合学生做课设、工程师做产线数据可视化也适合作为工业物联网平台的前置采集示例。1. 项目背景与整体架构1.1 为什么要做这个项目很多工业现场的设备并不具备以太网接口或者以太网模块成本较高。相比之下RS485 总线应用非常广泛几乎每台 PLC 都支持串口通信。基于 RS485 的 ModbusRTU 协议是工业领域事实上的标准协议之一几乎所有上位机、触摸屏、物联网网关都兼容它。把 ModbusRTU 数据采集上来之后下一步自然就是“数据上云”或“数据上 Web”。通过 WebServer 提供 HTTP 接口和页面数据就能被 MES 系统、可视化大屏、手机浏览器等更上层的系统访问。这个链路并不复杂但涉及串口通信、协议解析、并发访问、数据存储等多个知识点非常适合作为工业数据采集入门到进阶的综合项目。1.2 整体架构系统按数据流向可以分成四层如下所示[ S7-200 SMART PLC ] ---RS485/ModbusRTU--- [ Python 采集服务 ] ---HTTP/JSON--- [ WebServer ] ---浏览器 ModbusRTU Slave ModbusRTU Master Flask/HTTP服务PLC 侧ModbusRTU 从站通过指令库开放若干保持寄存器。采集端Python 程序周期性读取 PLC 保持寄存器解析成温度、压力、状态等业务数据。存储端采集的数据先写入内存缓存或数据库供 Web 服务读取。Web 端提供 REST API 和简单可视化页面支持浏览器查看实时数据。这种方式的好处是采集服务和 Web 服务可以拆分部署。比如采集服务可以放在靠近 PLC 的工控机上Web 服务可以部署在公司内网服务器两边通过 HTTP 接口通信耦合度低便于后续扩展。2. ModbusRTU 协议核心概念2.1 ModbusRTU 是什么Modbus 是 Modicon 公司 1979 年提出的工业通信协议后来成为工业领域应用最广泛的通信标准之一。Modbus 协议有两种常见传输模式ASCII 模式和 RTU 模式。RTURemote Terminal Unit模式下数据以二进制方式传输报文紧凑传输效率高。一帧 ModbusRTU 报文的基本格式如下字段长度说明从站地址1 字节1~2470 为广播地址功能码1 字节如 03 读保持寄存器、06 写单个寄存器数据N 字节寄存器地址、数量、或写入值等CRC 校验2 字节低字节在前循环冗余校验例如主站请求从站地址 1 的 PLC读取起始地址 0 开始的 3 个保持寄存器报文大致是01 03 00 00 00 03 05 CB其中01是从站地址03是对应功能码00 00是寄存器起始地址00 03是读取数量05 CB是 CRC 校验值。实际开发中 CRC 校验由通信库自动完成不需要我们自己手算但理解报文结构对排查问题很有帮助。2.2 保持寄存器与功能码Modbus 协议把设备数据分为几类区域最常用的是保持寄存器Holding Register和输入寄存器Input Register。保持寄存器可读可写功能码 03 读取、06 写单个、16 写多个。输入寄存器只读功能码 04 读取。每个寄存器是 16 位2 字节一个寄存器可以存 0~65535 的整数或者拆成两个寄存器存 32 位浮点数。S7-200 SMART 常见的整数、浮点数、开关量状态都可以映射到保持寄存器中。理解这一点之后采集程序的逻辑就很清晰了。我们需要知道PLC 从站地址是多少。要读哪些寄存器、起始地址是什么、数量是多少。读回来的 16 位整数在业务层代表什么物理量、什么单位。2.3 WebServer 在采集链路的角色WebServer 不直接和 PLC 通信它的作用是把采集端已经处理好的数据以 HTTP 接口方式暴露出去。这样上层系统不需要关心底层协议只需要请求一个 JSON 接口。这种“设备层-采集层-服务层-展示层”的分层设计是工业互联网项目最常见的参考架构。3. 环境准备与硬件接线3.1 硬件清单本项目使用 S7-200 SMART PLC 作为从站在开始采集之前需要准备以下硬件和工具S7-200 SMART CPU如 SR20、ST20、SR30 等型号。RS485 转 USB 适配器用于连接 PLC 串口和电脑。24V DC 电源为 PLC 供电。两根双绞线作为 RS485 通信线。螺丝刀、万用表等接线工具。西门子编程线或网线用于给 PLC 下载程序。不同型号的 S7-200 SMART 串口定义基本一致RS485 接口通常有 A、B 两个接线端子。通信时要注意 A 接 A、B 接 B不能接反否则会出现数据超时或乱码。3.2 软件环境软件方面主要分为 PLC 编程软件和上位机开发环境两部分。PLC 侧编程软件STEP 7-MicroWIN SMART。PLC 固件和软件版本以你手上设备实际为准。上位机侧操作系统Windows 10/11或者 Linux。Python 3.8 及以上版本。Python 依赖库pymodbus、Flask、requests、pyserial。测试工具ModbusPoll 或 Modbus Slave用于验证 PLC 从站功能和串口链路。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。pymodbus 库在不同版本之间 API 有一定差异如果你使用的版本较新需要以官方文档或库内提示为准。3.3 通信参数约定RS485 是一种电气标准通信双方必须约定完全一致的串口参数否则数据链路无法建立。本项目的通信参数约定如下参数数值波特率9600数据位8停止位1校验位无N从站地址1超时时间1 秒这个参数可以修改但必须保证 PLC 端 MBUS_INIT 指令中的设置与上位机完全一致。“9600-8-N-1”是工业现场最常用的串口参数组合也最容易排查。4. S7-200 SMART 配置为 ModbusRTU 从站4.1 调用 Modbus RTU 指令库S7-200 SMART 本身并不直接支持 ModbusRTU 协议需要通过西门子提供的指令库实现。打开 STEP 7-MicroWIN SMART 软件后在指令树中找到“库”菜单展开后可以看到 Modbus RTU Slave 相关指令核心指令有两个MBUS_INIT 和 MBUS_SLAVE。MBUS_INIT 指令用于初始化从站参数包括波特率、站号、寄存器范围等。MBUS_SLAVE 指令必须在每个扫描周期调用用于响应主站请求。如果忘记调用 MBUS_SLAVE从站是不会回复任何请求的。4.2 初始化 MBUS_INIT在子程序中调用 MBUS_INIT并在首次扫描时触发一次。为了简化可以直接在主程序中使用 SM0.1 作为触发条件。SM0.1 是 PLC 首次扫描周期为 ON 的特殊寄存器非常适合做初始化操作。MBUS_INIT 参数示例如下参数示例值说明Mode11 启用 Modbus 协议0 恢复 PPI 协议Addr1从站地址范围 1~247Baud9600波特率Parity00 无校验1 奇校验2 偶校验Delay0协议转换延时通常设为 0MaxIQ128Modbus 地址 0xxxx/1xxxx 映射的 I/Q 点数量MaxAI32Modbus 地址 3xxxx 映射的 AI 字数量MaxHold200保持寄存器数量HoldStartVB0保持寄存器起始地址这里最关键的是 HoldStart。它表示从 VB0 开始的一段 V 存储区会被映射为保持寄存器。如果上位机读取地址 0 的保持寄存器实际上读取的是 VB0、VB1 组成的 16 位数据对应 Modbus 地址 40001。4.3 主循环调用 MBUS_SLAVE初始化完成后在主程序中用 SM0.0常 ON调用 MBUS_SLAVE每个扫描周期执行一次。MBUS_SLAVE 有 Done 和 Error 两个输出参数Done 为 TRUE 表示本次响应完成Error 返回错误代码。为了在 PLC 中留下可视化数据可以在 V 存储区中写入一些测试值。例如在 PLC 程序里把 VW0 赋值为 100表示当前温度 100数值可以后续在程序内随意改变VW2 赋值为 50表示压力 50VW4 赋值为 1表示设备启动状态。这样上位机采集后就能看到具体数值。下载程序到 PLC 后把 PLC 运行模式切换到 RUN。此时 S7-200 SMART 已经作为 ModbusRTU 从站运行等待主站读取数据。4.4 用 ModbusPoll 验证从站在写 Python 代码之前建议先用 ModbusPoll 软件验证一下从站配置是否正确。ModbusPoll 是一个常用的 Modbus 主站模拟工具。打开 ModbusPoll 后选择 “Connection” - “Connect”。连接方式选择 Serial串口选择 USB 转 RS485 对应的 COM 口。波特率设置为 9600数据位 8停止位 1校验位 NONE。Slave ID 填写 1。功能码选择 03 Holding Register。地址 0数量 3。如果连接成功ModbusPoll 会显示三个寄存器的值比如100、50、1。如果显示超时或错误先检查串口、波特率和接线不要急着写代码。5. 编写 Python ModbusRTU 采集服务5.1 安装 Python 依赖打开命令行创建一个 Python 虚拟环境并安装依赖mkdir modbus-web cd modbus-web python -m venv venv venv\Scripts\activate pip install pymodbus flask requests pyserial如果网络环境下载慢可以切换国内镜像源。到这里我们先不会用到 requests后面扩展时会用到。5.2 实现 ModbusRTU 主站读取这一小节编写一个最基础的采集脚本用于连接 PLC 并读取保持寄存器。完整代码如下# modbus_client.py from pymodbus.client import ModbusSerialClient # 串口和协议参数 PORT COM3 # 根据实际情况修改Linux 下通常是 /dev/ttyUSB0 BAUDRATE 9600 SLAVE_ID 1 client ModbusSerialClient( methodrtu, portPORT, baudrateBAUDRATE, bytesize8, parityN, stopbits1, timeout1, ) def read_plc_data(): if not client.connect(): print(串口连接失败请检查 COM 口号和 USB 转 485 设备) return None try: # 读取从站地址 1起始寄存器地址 0读取 3 个保持寄存器 response client.read_holding_registers(address0, count3, slaveSLAVE_ID) if response.isError(): print(读取失败错误信息, response) return None temperature response.registers[0] pressure response.registers[1] device_status response.registers[2] return { temperature: temperature, pressure: pressure, device_status: device_status, } finally: client.close() if __name__ __main__: data read_plc_data() print(data)运行该脚本后如果通信正常会输出类似下面的结果{temperature: 100, pressure: 50, device_status: 1}这里要注意read_holding_registers的第一个参数 address 是协议内的寄存器索引0 对应 Modbus 地址 40001。S7-200 SMART 的 VB0 被映射到这一地址所以 PLC 里修改 VW0 的值这里读回来的 temperature 就会跟着变化。5.3 增加超时、重试与异常处理工业现场通信链路偶尔会出现瞬时干扰一两次读取失败很正常。如果失败一次就停止采集系统稳定性会很差。在实际项目中需要增加重试机制和异常隔离。改进后的采集函数如下# modbus_client_retry.py import time from pymodbus.client import ModbusSerialClient class PlcModbusClient: def __init__(self, port, baudrate9600, slave_id1, timeout1): self.port port self.baudrate baudrate self.slave_id slave_id self.timeout timeout self.client None def connect(self): if self.client is not None and self.client.is_socket_open(): return True self.client ModbusSerialClient( methodrtu, portself.port, baudrateself.baudrate, bytesize8, parityN, stopbits1, timeoutself.timeout, ) return self.client.connect() def read_with_retry(self, address0, count3, retries3): for attempt in range(1, retries 1): try: if not self.connect(): time.sleep(0.5) continue response self.client.read_holding_registers( addressaddress, countcount, slaveself.slave_id ) if response.isError(): raise RuntimeError(f读取错误: {response}) return response.registers except Exception as e: print(f第 {attempt} 次读取失败: {e}) self.client.close() time.sleep(1) return None def close(self): if self.client is not None: self.client.close()通过类的封装串口连接、读取、重试、释放资源都有统一入口。后续编写采集循环时不需要再关心底层连接细节。5.4 周期性采集与数据存储实际的采集服务需要持续运行每隔一定时间读一次 PLC。采集到的数据不能只打印到屏幕上还要存起来供 WebServer 使用。为了简单起见本项目使用 SQLite 作为本地存储文件名为industrial_data.db。采集线程每隔 2 秒读一次数据并插入到数据库。这里使用 SQLite 是因为它无需额外安装服务、单文件即可运行适合教学和轻量场景。生产环境建议使用 InfluxDB、TimescaleDB 或 MySQL 等更专业的存储。下面是完整的采集服务示例# collector.py import sqlite3 import time from datetime import datetime from modbus_client_retry import PlcModbusClient DB_PATH industrial_data.db def init_db(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS plc_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, temperature REAL, pressure REAL, device_status INTEGER ) ) conn.commit() conn.close() def insert_data(data): conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute( INSERT INTO plc_data (timestamp, temperature, pressure, device_status) VALUES (?, ?, ?, ?), (datetime.now().strftime(%Y-%m-%d %H:%M:%S), data[0], data[1], data[2]), ) conn.commit() conn.close() if __name__ __main__: init_db() plc PlcModbusClient(portCOM3, baudrate9600, slave_id1, timeout1) while True: registers plc.read_with_retry(address0, count3, retries3) if registers is not None: insert_data(registers) print(采集成功, registers) else: print(采集失败等待下一轮) time.sleep(2)数据库表的字段中timestamp记录采集时刻temperature和pressure保存模拟量数值device_status保存设备启停状态。这里所有字段都是按可扩展的思路设计的后续增加新寄存器时只需要调整 count 和插入字段。6. 实现 WebServer 数据展示服务6.1 WebServer 和 Flask 的作用WebServer 模块的核心任务有三个对外提供实时数据接口方便其他系统调用。展示当前最新数据让用户在浏览器中查看。可选地提供历史数据查询接口或图表。Flask 是一个轻量级 Python Web 框架非常适合这个场景。它不需要复杂的工程结构一个 Python 文件就能启动一个 Web 服务。6.2 提供实时数据 API在项目目录下创建webserver.py编写一个实时数据接口。# webserver.py import sqlite3 from flask import Flask, jsonify, render_template app Flask(__name__) DB_PATH industrial_data.db def get_latest_data(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() cursor.execute(SELECT timestamp, temperature, pressure, device_status FROM plc_data ORDER BY id DESC LIMIT 1) row cursor.fetchone() conn.close() if row is None: return {timestamp: None, temperature: None, pressure: None, device_status: None} return { timestamp: row[0], temperature: row[1], pressure: row[2], device_status: row[3], } app.route(/api/realtime, methods[GET]) def realtime_api(): data get_latest_data() return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)接口返回示例如下{ timestamp: 2024-01-15 14:30:22, temperature: 100, pressure: 50, device_status: 1 }其他系统、大屏或者前端页面只需要请求这个 API 就能拿到最新数据。采集服务与 Web 服务通过数据库共享数据二者互不阻塞。6.3 编写简单前端页面为了更直观地展示效果这里实现一个简单的 HTML 页面。页面通过 JavaScript 定时请求/api/realtime接口每 2 秒刷新一次数据。在项目目录下创建templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleS7-200 SMART 数据采集监控/title style body { font-family: Microsoft YaHei, sans-serif; background: #f5f7fa; padding: 40px; } .card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding: 24px; margin: 20px auto; max-width: 700px; } .card h2 { margin-top: 0; } .data-item { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #eee; } /style /head body div classcard h2工业数据实时监控/h2 div classdata-item span采集时间/span span idtimestamp--/span /div div classdata-item span温度/span span idtemperature--/span /div div classdata-item span压力/span span idpressure--/span /div div classdata-item span设备状态/span span iddevice_status--/span /div /div script function fetchData() { fetch(/api/realtime) .then(response response.json()) .then(data { document.getElementById(timestamp).textContent data.timestamp || --; document.getElementById(temperature).textContent data.temperature ! null ? data.temperature ℃ : --; document.getElementById(pressure).textContent data.pressure ! null ? data.pressure kPa : --; document.getElementById(device_status).textContent data.device_status ! null ? (data.device_status 1 ? 运行中 : 停止) : --; }) .catch(error console.error(请求失败, error)); } // 首次加载立即获取之后每 2 秒刷新一次 fetchData(); setInterval(fetchData, 2000); /script /body /html然后在 Flask 中添加页面路由app.route(/, methods[GET]) def home(): return render_template(index.html)6.4 启动 WebServer 验证启动 WebServerpython webserver.py浏览器访问http://localhost:5000页面每隔 2 秒会请求一次后端接口显示最新的 PLC 数据。如果数据能随着 PLC 程序里 VW0、VW2、VW4 的修改而变化说明整条链路已经完全打通。7. 整体联调与预期效果7.1 启动顺序为了保证系统稳定运行建议按照以下顺序启动相关服务给 PLC 下载包含 ModbusRTU Slave 指令的程序并切换到 RUN 模式。启动 Python 采集服务collector.py观察控制台是否输出了正确的寄存器值。启动 Web 服务webserver.py浏览器访问页面。采集服务是持续运行的进程如果中途退出Web 页面只能显示最后一次写入数据库的数据而不会继续更新。实际部署时可以用 systemd、supervisor 或 Windows 服务方式把采集服务配置成开机自启。7.2 模拟故障与环境干扰为了验证系统的健壮性可以手动拔掉 RS485 通信线观察采集服务的重试日志和 Web 页面表现。在通信正常恢复后采集服务应该能自动继续采集。这种情况下数据库里会存在一小段时间的数据空缺。这是工业数据采集的常态不建议在采集端尝试“补数据”覆盖因为补出来的数据可能并不是真实数据。更合理的做法是记录数据的质量状态比如为空、超时、错误让上层系统区分真实数据和异常数据。7.3 扩展点当前示例中只读取了 3 个保持寄存器。实际项目中可能需要读取几十个甚至上百个变量包括不同类型的模拟量、数字量和累计量。扩展时可以做一个数据点配置文件把每个变量的名称、寄存器地址、数据类型、单位、换算系数放在一个 JSON 文件里采集程序根据配置动态采集和解析。这样当设备点位变化时只需要修改配置不需要重写代码。8. 常见问题与排查思路下面是本类项目最高频的问题按现象、原因、处理思路整理如下问题现象常见原因解决思路串口连接失败COM 口选择错误USB 驱动未安装在设备管理器中确认 USB 转 485 设备的 COM 编号读取超时波特率、站号、数据位等参数不一致核对 PLC 的 MBUS_INIT 参数与 Python 参数是否一致读取数据全为 0从站寄存器没有写入值或寄存器地址偏移检查 PLC 程序 VW 赋值确认寄存器地址映射关系偶发读取失败485 总线干扰、接线过长、接地不良使用屏蔽双绞线增加终端电阻检查接地数据一直不更新PLC 处于 STOP 模式子程序未调用把 PLC 切到 RUN确认 MBUS_SLAVE 被 SM0.0 每个周期调用pymodbus API 报错pymodbus 版本差异查看已安装版本按官方文档调整参数名和调用方式Web 页面无数据采集服务未启动或数据库无记录先确认采集控制台输出再用浏览器访问 /api/realtime8.1 典型问题ModbusPoll 能读Python 读不到这种情况最常见的原因是 COM 口被 ModbusPoll 占用。Windows 下同一串口同一时刻只能被一个程序打开ModbusPoll 保持连接时Python 脚本无法打开同一个串口。解决方法是先关闭 ModbusPoll再运行 Python 脚本。8.2 典型问题寄存器数值和 PLC 程序不一致Modbus 保持寄存器地址从 0 开始对应 Modbus 地址 40001。S7-200 SMART 的 HoldStart 如果设置为 VB0那么上位机读取地址 0 的寄存器读取的是 VB0、VB1 两个字。如果在 PLC 中使用 VW2 存放压力上位机就应该读取地址 1。地址偏移一位是一个高频错误排查时先确认映射关系。9. 生产环境避坑与最佳实践9.1 不要用 sleep 硬等采集周期很多初学者会在采集循环里写死time.sleep(2)。这在演示场景没什么问题但在生产环境中如果某次读取因为重试耗时较长会导致采集周期漂移。更稳妥的做法是计算下一次执行时间用相对时间差进行调度或者使用schedule、APScheduler这类调度库。9.2 数据入库采用批量写入本示例每条数据单独 INSERT 一次数据量少时没问题。如果每 2 秒采集一次一天会产生 43200 条记录单条插入也能扛住。但如果采集频率提高到 100ms或者设备数量增加到几十台就必须使用批量插入。SQLite 可以使用executemany批量插入MySQL 可以使用多值 INSERT。9.3 增加日志而不是只 print生产环境不可能一直盯着控制台看。建议使用 Python 的logging模块记录关键事件包括采集成功、采集失败、重连等。日志内容包含时间戳、设备地址、错误码方便事后分析。9.4 关注并发和线程安全如果采集服务和 WebServer 在同一个进程内运行需要注意 SQLite 的并发访问。SQLite 默认同一时刻只允许一个线程写数据库多线程同时写会出现锁错误。最简单的办法是把采集服务和 WebServer 拆成两个进程各自独立连接数据库。也可以在 Flask 中为每个请求创建短连接连接对象在使用完毕后及时关闭。9.5 安全边界WebServer 如果部署在公司内网风险相对可控。但如果需要跨网络访问一定要加认证。Flask 可以用登录页面、Token 或 Basic Auth 做简单鉴权不要直接把接口裸奔到公网。同时所有 Python 代码涉及到串口操作时要确认操作环境是合法授权的测试环境避免在生产设备上造成意外停机或参数写入错误。本示例只读取保持寄存器没有写入操作。如果需要写寄存器建议先在实验室设备上验证并设置严格的操作权限和数据范围校验。9.6 从“能跑”到“可用”这个项目本身是一个最小可行系统。从“能跑”到“可用”还有几件事值得做增加设备在线判断超过 N 次采集失败自动标记设备离线。增加点位配置表用 JSON 或数据库表管理所有数据点。增加历史数据查询接口用于趋势图和报表。用图表库如 ECharts 替换静态 HTML展示折线图、柱状图。如果 PLC 数量多考虑使用工业网关代替电脑串口采集端使用网络 ModbusTCP 协议。10. 总结与下一步学习方向通过这个项目你应该已经掌握了 ModbusRTU 通信的核心流程从硬件接线、PLC 从站配置、Python 主站采集到 WebServer 数据展示。整套链路虽然简单却涵盖了工业数据采集最常见的技术栈很多商业项目的雏形也是这么搭建出来的。下一步可以考虑两条进阶路线。一条是横向扩展把多台 PLC、多种协议ModbusTCP、OPC UA、S7 协议接入到同一个采集平台做一个统一的设备接入层。另一条是纵向深化研究数据存储优化、边缘计算处理、数据清洗和设备云边协同。如果你是从零开始接触工控通信建议直接用一台 S7-200 SMART按照本文步骤走完一遍。遇到问题时优先检查串口参数和寄存器映射关系这是 90% 通信异常的根本原因。把这个最小闭环跑通后后续增加设备、增加 Web 功能、对接 MES 系统都会顺畅很多。