OpenPLC v3 runtime源码解析:从软PLC原理到Linux部署与Modbus通信实践
发布时间:2026/10/22 15:24:34 锦皓数字建站

简介OpenPLC_v3-master 是一套开放源代码的 PLC 运行时环境面向工业自动化开发者、工程师及 PLC 编程学习者旨在摆脱对特定品牌硬件的依赖让用户在 PC、服务器等通用平台上编写、调试和运行控制程序。项目遵循 IEC 61131-3 标准支持梯形图、结构化文本、顺序功能图等多种编程方式适合从入门实验到复杂控制逻辑验证的不同场景。压缩包共 2769 个文件大小约 11.71MB主要包含 C/C 核心源码、Java/C# 扩展模块、Scala 组件及说明文档与脚本从文件构成看C/C 部分对应运行时和驱动Java/C#、Scala 部分偏向配套工具与平台适配便于按模块检索阅读。已有 501 人浏览学习。这份开源资源既可用作 PLC 原理学习样本也可作为二次开发基础配合编译器、调试器和模拟器等配套能力读者可低成本完成逻辑验证与功能定制是一份了解开放 PLC 技术路线的完整参考资料。1. 先别把它当玩具OpenPLC_v3-master 这个源码包究竟是个什么东西拿到一份 OpenPLC_v3-master 的源码包最容易产生的误会是这是个能画梯形图、跑仿真的 PLC 软件。实际上标题里真正的主角是 runtime——OpenPLC_v3 的运行时内核它的职责是把一台普通计算机变成可编程逻辑控制器。你用 OpenPLC Editor 按 IEC 61131-3 标准编写 ST、梯形图或功能块图程序编译之后上传给这个 runtime 去周期执行一套软 PLC 就成立了而且自带 Modbus TCP 从站能力SCADA 和组态软件可以直接连上来读写。它能解决什么产线改造预算紧张时用旧 PC 顶替硬件 PLC、边缘节点做设备数据采集和顺序控制、教学场景里让学生人手一个可编程控制器。适合谁自动化工程师、做 IIoT 边缘接入的开发者、想学 PLC 编程但不想买硬件的学生。但有一句要放在前面它适合逻辑控制不适合运动控制和带安全认证的安全回路这个边界先划清楚后面才不会踩出大坑。2. 拆开 OpenPLC_v3-masterruntime 的模块边界与启动链路2.1 源码里必须认识的四个目录core、lib、utils、webserver拿到源码包先别急着跑 install.sh花十分钟把根目录布局读一遍后面排查问题会省很多时间。以 master 分支的常见布局来说真正决定“这是一台 PLC 还是普通程序”的代码全在 core 目录里PLC 主循环、IEC 61131-3 的数据类型定义、标准函数库TON、TOF、CTU 这些定时器和计数器、以及上传程序的加载入口都在这里。core 之外的 lib 目录放的是协议栈Modbus TCP/RTU、DNP3 这些工业协议与 PLC 核心交换数据靠的是共享的 I/O 映象区而不是进程间通信这也是它的响应能跟上扫描周期的原因。webserver 目录是另一个不能忽视的部分OpenPLC runtime 自带一个用 C 实现的嵌入式 Web 管理台默认监听 8080 端口上传程序、查看运行状态、改配置都在这个页面上完成不需要在电脑上再装任何客户端。utils 目录主要是一些辅助脚本和配置文件模板日常用得不多但 openplc.conf 这类配置文件的参考写法往往就在里面。用一个命令把目录结构摊开看会更直观cd OpenPLC_v3-master find . -maxdepth 2 -type d | sort这个命令会列出根目录下两层以内的所有子目录输出里能看到 core、lib、webserver、utils 以及各自的内层结构。第一次跑 install.sh 之前建议先把输出存一份因为安装脚本可能向源码目录写入编译中间文件后面对比“哪些文件是源码自带、哪些是构建产物”时这份清单就是后悔药。把这几个主要目录的职责和你要关心的重点列出来读源码时对号入座目录职责你会改动或需要阅读的部分corePLC 核心引擎、主循环、IEC 数据类型与标准库plc_main.c主循环、iec_std_lib.h标准函数库libModbus、DNP3 等协议栈modbus 从站驱动、串口与网络读写接口webserverWeb 管理台与上传接口静态页面资源、上传处理逻辑、会话鉴权utils辅助脚本、配置模板openplc.conf 的参考写法、调试工具2.2 runtime 从入口函数到任务线程一个 PLC 程序是这么转起来的runtime 的启动过程可以拆成三步。第一步读 openplc.conf确定 Web 端口、Modbus 端口、PLC 名称这些基本参数第二步初始化硬件抽象层HAL把物理输入输出接口准备好第三步拉起一组线程主任务线程负责周期扫描协议栈线程负责对外通信Web 服务器线程独立运行。三个线程共享一份 I/O 映象区主任务每扫一個周期就刷新一次映象区协议栈线程则随时把外部的读写请求作用到这个映象区上。主任务线程的扫描模型和硬件 PLC 完全一致先读输入映象再执行用户程序最后写输出映象。用一段伪代码来示意这个循环的骨架理解它比背配置项重要得多/* 主任务线程的核心循环示意代码非仓库原样 */ while (running) { update_inputs(); /* 把物理输入读到 I/O 映象区 */ run_user_logic(); /* 执行编译后的用户 PLC 程序 */ update_outputs(); /* 把输出映象写到物理输出 */ sync_wait(scan_cycle); /* 按扫描周期节拍等待 */ }一个周期的时间就是扫描周期定时器函数TON、TOF依赖系统时钟累加所以宿主机的时钟同步很重要跳时会导致定时器漂移。这里有个很多新手会误判的点上传到 runtime 的程序不是被解释执行的而是先被转成 C 代码再用系统里的 gcc 编译成动态库运行时通过动态加载方式挂进主任务。这意味着它没有解释器的性能损耗但任何运行时错误数组越界、空指针都会直接体现在进程崩溃上而不是报一条优雅的 PLC 错误。我对运行时行为的判断习惯是看一眼核心线程数量和扫描周期配置就能估出这台机器的实时性能底限。OpenPLC 的线程调度走的是普通实线程调度而非硬实时扫描周期调到几毫秒级别时抖动会明显变大这时候不是程序写得不好而是宿主机系统根本没给它硬实时的能力。想在一个项目里压榨性能就先把这个认知立住。注意扫描周期调得越小对宿主机 CPU 频率和调度稳定性要求越高。先跑默认值等逻辑验证无误后再往下压才是稳的路线。3. 在 Linux 上把 OpenPLC v3 runtime 编译起来最小可跑流程3.1 依赖安装与一键脚本install.sh 做了什么拿到 master 源码包后最直接的上手路径是运行根目录下的 install.sh。这个脚本做的事情按我的理解可以分成三块第一块是用系统包管理器把编译链补齐gcc、make、flex、bison 都在清单里其中 flex 和 bison 是 ST 语句解析生成器的依赖缺了它们后面的编译必挂第二块是把 Modbus 这类协议栈依赖装好第三块是检查当前架构并生成对应的构建配置。运行方式很简单# 进入源码根目录以解压后的文件夹名为准 cd OpenPLC_v3-master chmod x install.sh sudo ./install.sh这里有两个容易翻车的点。一是 install.sh 执行到中途会提示选择目标平台常见选项包括通用 x86 Linux 和各类 ARM 单板机选错平台会导致后面编译出的硬件驱动与机器架构不匹配表现出来就是启动时发现设备相关依赖缺失报错往往藏在启动日志深处不好定位。二是不建议在精简安装的发行版上直接跑系统里如果连基础编译工具都没有脚本会先花很长时间装依赖非常考验软件源的速度和稳定性。脚本跑完不代表一切就绪验证构建结果比看脚本输出更可靠。我一般会看两个地方源码目录下有没有生成可执行的 runtime 主程序以及启动脚本能否执行而不报“找不到文件”类错误。如果 install.sh 执行过程中出现 gcc 相关报错先别急着换脚本把系统里的 gcc 整体卸载后重装很多时候是机器上存在多个版本导致链接阶段选错了路径。3.2 首次启动与三个必改配置Web 端口、Modbus 端口与监听地址依赖装好后启动 runtime 本身很简单但启动前先改配置。打开源码根目录下的 openplc.conf里面是以键值对形式写的配置。三个键必须确认Web 管理台的监听端口、Modbus TCP 从站的监听端口、以及服务绑定的网卡地址。前两个有默认值兜底第三个才是重灾区——如果配置里绑定了所有网卡而机器又恰好被端口映射暴露到公网等于把一个可读写的控制接口直接暴露在网络上这不是危言耸听。# 前台启动便于看日志 sudo ./start_openplc.sh # 或后台运行日志重定向到文件 nohup sudo ./start_openplc.sh /tmp/openplc.log 21 启动后用一句话验证三个东西进程在不在、端口有没有监听、页面开不开。命令合在一起写ps aux | grep -i openplc | grep -v grep ss -tlnp | grep -E :(8080|502)\b如果看到 502 端口没有监听最常见的原因是当前用户没有权限绑定低端口——Linux 下 1024 以下的端口需要特权用户所以很多人会把端口改成 1502、15020 这类高位数字。Web 登录页打开后用默认账号口令登录多数版本默认都是 openplc登录后第一件事是在配置里把口令换掉。这个口令如果保持默认局域网里任何人都能给你上传新程序等于把设备控制权交了出去。配置项的关键信息放在一起看更清楚配置键常见命名默认值作用典型坑Web 管理端口8080管理台与程序上传入口与既有 Web 服务冲突时改成 8081Modbus 端口502Modbus TCP 从站监听低端口需特权用户冲突时改 1502监听地址所有网卡决定服务暴露范围生产环境务必绑内网网卡3.3 用 Web 管理台把第一个 ST 程序跑起来配置改完、runtime 起来后第一个实验程序建议写得足够简单。先用一段延时导通逻辑验证“程序上传—编译—运行—输出”这条链路通不通而不是一上来就跑完整项目。下面这段 ST 程序是典型的接通延时逻辑输入一触发两秒后输出置位。ST 语法接近 Pascal关键字大写是 IEC 61131-3 的习惯写法。PROGRAM delay_on VAR trigger : BOOL; (* 输入可映射到数字量输入 *) busy : BOOL; (* 输出可映射到数字量输出 *) t_work : TON; (* 接通延时定时器实例 *) END_VAR t_work(IN : trigger, PT : T#2s); busy : t_work.Q; END_PROGRAM这段程序在 OpenPLC Editor 里写好并编译生成上传用的程序文件然后在 Web 管理台的程序页面上传。runtime 收到后会走一遍“生成 C 代码—编译—动态加载”的流程页面状态会从编译中变成运行中。这里留个心眼上传这个动作本身会让 runtime 重启当前任务正在对外通信的 Modbus 连接会瞬间断开再重连产线调试时要避开正在跑料的时段。上传成功后在管理台的概览页能看到程序的运行状态和扫描周期。如果这一步卡住优先看启动日志里有没有编译错误。大多数上传失败都是程序本身有语法问题或变量类型不匹配在编辑器里先做软仿真验证比直接往线上 runtime 传要稳妥得多。我见过的返工案例里十有八九是编译期的低级错误编辑器里多花一分钟现场少折腾一小时。4. 程序上传、Modbus 通信与硬件层适配OpenPLC 的 ST 是怎么走到物理世界的4.1 上传的 ST 是怎么变成 C 代码的OpenPLC 的编译链路值得理解清楚因为很多怪异现象最后都能回到这条链路上解释。你在 Editor 里写的 ST 或梯形图Editor 端负责语法检查和初步编译生成的上传文件里携带的是编辑器的编译产物。runtime 收到上传内容后调用内置的代码生成器把它展开成 C 源文件放到运行时的工作目录下然后调用系统 gcc 编译成共享库最后通过动态加载方式装进主任务。也就是说每次上传程序这台机子上都会发生一次真实的编译。这条链路带来两个直接后果。第一宿主机上必须留着完整的 gcc 工具链install.sh 装过的依赖不能为了省空间删掉。第二程序里的问题分两类编译期错误会在上传时当场报出来运行期错误比如访问了非法内存则会表现为 runtime 进程崩溃或任务反复重启这类问题在日志里往往只剩一条段错误信息排查成本高得多。所以我的习惯是任何程序上传前先在 Editor 里做软仿真把逻辑跑一遍再往线上传。上传的动作在 Web 页面里是拖拽文件走的是 HTTP 上传通道。如果要脚本化部署常见做法是用命令行模拟同样的 multipart 表单提交具体的接口路径以页面抓包的 POST 请求为准核心是文件字段名和登录态保持一致。我不建议绕过多鉴权直接打端口管理服务不是为裸调而设计的绕过去只会给自己埋雷。4.2 Modbus TCP 从站配置与 SCADA 接线runtime 默认把自己当一个 Modbus TCP 从站来用这是它作为软 PLC 最有价值的一点SCADA、组态软件、上位机都可以直接按 Modbus 协议读写它的 I/O。配置入口在 Web 管理台的 Modbus 页面需要确认的选项包括从站 ID、协议使能开关和端口号端口号要与 openplc.conf 里保持一致。Modbus 对象与 IEC 61131-3 变量的对应关系理解成一张映射表就好Modbus 对象地址区域IEC 变量线圈Coil0x%QX 数字量输出离散输入Discrete Input1x%IX 数字量输入输入寄存器Input Register3x%IW 模拟量输入保持寄存器Holding Register4x%QW / %MW 输出与内部字不同小版本在地址偏置上偶尔差 10 偏置和 1 偏置之争调试时先用一个已知值写进寄存器再从客户端读回来验证这一步能省下大量“明明连上了却读不到”的排查时间。验证脚本用 Python 很顺手下面是一段用 pymodbus 读写保持寄存器的示例from pymodbus.client import ModbusTcpClient client ModbusTcpClient(192.168.1.20, port1502) if client.connect(): # 读保持寄存器从地址 0 开始读 8 个 result client.read_holding_registers(0, count8, slave1) if not result.isError(): print(registers:, result.registers) # 回写一个已知值验证写路径 client.write_register(0, 1234, slave1) client.close()这段脚本要注意三个参数连接地址写运行时所在机器的实际 IP端口与 runtime 的 Modbus 端口一致slave 参数对应配置的从站 ID。连不上时按“先 ping 通 IP、再确认端口监听、最后查从站 ID”的顺序排查能避开一半的通信玄学。协议栈线程与主任务线程是并行运行的Modbus 读写不阻塞主扫描但频繁无节制的轮询会抬高 CPU 占用。对一台还要跑其他任务的边缘盒子来说状态监控的轮询周期放到 500ms 以上通常就够用了。4.3 硬件驱动层ARM 单板机引脚与自定义 HAL 驱动如果只想做纯逻辑仿真OpenPLC 默认的硬件配置就能满足——它不带任何物理 I/O所有输入输出都停留在内存映象里配合 Modbus 从站对外通信。但一旦要接真实的按钮、继电器、传感器就必须在 Web 管理台的硬件页面选择对应的驱动选好了之后runtime 会把 %IX0.0、%QX0.0 这些地址映射到物理引脚上。这一步最常见的坑是权限操作 GPIO 通常需要特权用户runtime 必须用 root 启动或者给设备节点配好权限否则表现为程序正常运行、映象区数值也在变但引脚电平纹丝不动。自写驱动是 OpenPLC 扩展性的核心。驱动的本质是实现一组接口函数初始化函数负责申请资源和配置引脚方向输入读取函数把物理电平搬到 I/O 映象区输出写入函数把映象区数值落到物理输出上。把这三个函数放进 lib 层对应的驱动目录再在构建配置里把新驱动加进去重新编译后就能在硬件选择列表里看到它。对大多数项目来说自定义串口设备、脉冲计数这类非标输入都值得用这个方式接入而不是在外面再包一层协议转换。写驱动时注意一点输入读取函数是在主任务线程里被调用的它应该尽快返回把耗时操作等串口响应、轮询外部设备放到单独线程里否则会直接拉长扫描周期。很多人自定义驱动翻车九成是栽在这一条上。5. OpenPLC_v3 runtime 避坑与排查从编译失败到通信闪断这一整章是实际使用中最高频的五个问题按“现象—原因—解决”的记录方式整理。每一条都是在不同项目里反复见过的共性规律不涉及具体项目细节照着排查即可。5.1 编译链接失败缺依赖、选错平台与多版本 gcc现象install.sh 执行到一半报错或者在上传程序时出现编译错误日志里能看到 gcc、flex、bison 相关的报错信息。原因分两类。一类是系统里缺基础依赖精简安装的发行版没有完整编译链另一类是机器上存在多个 gcc 版本编译时选错了头文件路径。还有一个低概率但隐蔽的原因是安装脚本交互式选择平台时选错导致构建配置指向了错误的架构。解决先把编译链整体清理重装确保 gcc、make、flex、bison 版本匹配再理顺多版本 gcc 的优先级最后确认构建配置里的架构标识与当前机器一致。三步做完还报错就把报错日志的前二十行贴出来比对真正的错误往往在中间位置而不是最后一行。5.2 程序上传成功但输出寄存器怎么改都不动现象Web 管理台显示程序处于运行状态Modbus 也能连上但写入输出线圈或保持寄存器后对应变量没有任何反应读回来还是原值。原因最常见的是程序里根本没有把输出地址放进逻辑链路变量声明了但没被赋值或者赋值被后续逻辑覆盖。另一个常见原因是硬件驱动选了空白模式输出只写在内存映象区没有落到物理设备让人误以为“输出不动”。解决先在程序的变量映射里确认输出变量确实被赋值语句引用再用 Modbus 写一个已知值回到 Web 管理台的变量监控里看映象区数值是否变化。映象区变了而物理量不变问题在硬件驱动层映象区都没变问题在程序逻辑层。这个二分法能直接定位九成以上的“输出不动”。5.3 Modbus 能连上但读写频繁超时、连接闪断现象用上位机轮询时连接建立成功但读几次就超时过一会儿又能连上周而复始runtime 侧能看到大量连接断开重建的记录。原因高频次的连接与断开触发了半开连接堆积服务端连接表被占满或者轮询周期小于 runtime 的处理能力请求排队积压导致超时。低端口被防火墙策略拦截的情况也偶有发生。解决把轮询周期放宽到合理区间避免每个变量单独建连接尽量用一次请求批量读取连续寄存器在系统层调整半开连接的回收参数缩短等待时间排查防火墙对目标端口的策略。稳定通信的前提是轮询方别太“急性子”。5.4 断开 SSH 后整个 runtime 跟着退出现象通过 SSH 登录服务器用前台方式启动 runtime调试一切正常一旦断开 SSH 会话runtime 进程也跟着消失下次还得重新登录拉起。原因前台进程挂在 SSH 会话的终端上会话断开时系统向进程发送挂断信号进程没有处理这个信号默认行为就是退出。这是 Linux 下最常见的后台进程管理误解不是 OpenPLC 特有的问题。解决要么用 nohup 把进程与终端会话剥离要么注册成系统服务由守护进程接管。生产环境我强烈建议用后者开机自启、异常退出自动拉起都比 nohup 可靠。下面是一段 systemd 服务配置替换路径后即可使用# /etc/systemd/system/openplc.service [Unit] DescriptionOpenPLC Runtime Afternetwork.target [Service] ExecStart/opt/OpenPLC_v3-master/start_openplc.sh WorkingDirectory/opt/OpenPLC_v3-master Restarton-failure Userroot [Install] WantedBymulti-user.target配置完成后依次执行 systemctl daemon-reload 和 systemctl enable openplc 使其开机自启。注意 ExecStart 建议写成启动脚本的绝对路径WorkingDirectory 保证脚本里所有相对路径都指向源码根目录这两处配错是服务起不来的头号原因。5.5 程序状态正常物理输出却纹丝不动现象ARM 单板机上跑 runtimeWeb 管理台里变量状态完全正常程序也在运行但接在 GPIO 引脚上的继电器或指示灯没有任何反应。原因硬件驱动没有选对或没有启用runtime 默认跑在无物理 I/O 的模式下或者 GPIO 设备节点权限不足驱动初始化时静默失败没有明显报错。解决先到硬件配置页确认当前驱动是对应板卡型号的那一个再确认 runtime 是以特权用户运行的。这两步都无误时用命令行直接读写 GPIO 引脚验证系统层权限和引脚编号是否与驱动文档一致。如果命令行能控制而 runtime 不能问题就在驱动初始化逻辑重点查设备节点在系统里的实际路径是否与驱动代码里写死的路径不一致。6. 进阶玩法把 OpenPLC v3 runtime 容器化与轻量协议扩展6.1 把 runtime 塞进容器网络模式与数据卷的取舍OpenPLC runtime 依赖 gcc 工具链和系统级设备访问容器化不是开箱即用的但有两条路值得走。第一条是保留完整 runtime 镜像用 host 网络模式运行让 Modbus 端口直接暴露在宿主机网络上。host 模式的好处是端口不用做映射、没有 NAT 转发延迟坏处是隔离性弱一些镜像体积也小不了。第二条是把 runtime 当作纯逻辑内核跑在容器里对外通信全部走 Modbus 端口映射配合数据卷挂载程序目录这样升级 runtime 版本不会丢程序和配置。我推荐先在宿主机上把这个流程完全跑通再容器化否则很难分清是容器网络问题还是 runtime 本身的配置问题。容器化可以把部署动作收敛成一条命令对边缘节点批量交付特别有价值。健康检查直接探测 Web 管理端口服务没有响应就触发重启策略让异常自愈。有个细节容易被忽略容器内绑定 502 这类特权端口同样需要 root 权限所以在容器里我一般直接用高位端口再把宿主端口映射到标准的 1502两边都省心。6.2 不写 C 也能扩展协议外部读写与插件思路自定义协议不一定非要写进 lib 层。更轻量的做法是把 runtime 当作一个带 Modbus 从站接口的软 PLC外部程序Python、Node.js 都行用 Modbus TCP 读写变量再以你自己的协议格式与上层系统通信。这种双层结构把“PLC 做的事”和“边缘网关做的事”解耦开逻辑清晰也方便单独调试。如果确实需要低延迟、高吞吐的协议定制再考虑写驱动插件挂到 lib 层仿照已有协议栈的读写接口实现编译进 runtime 后重启生效。插件方式需要你理解 I/O 映象区的读写生命周期把自定义协议的数据进出做成与扫描周期同步的节奏而不是在插件里自起线程疯狂读写。与扫描周期同步的优势是数据一致性有保证不会出现读到一半映象区被刷新的撕裂问题。说到最后我这些年在 OpenPLC 相关项目里养成的习惯是任何一台新机器落地第一件事改默认口令第二件事确认监听地址只绑定内网网卡第三件事把扫描周期调到满足任务需求的最大值再慢慢往下压。顺序反过来的人多半在后面某个深夜为安全问题或性能抖动付出过代价。这个顺序也送给你希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。