简介: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-master:runtime 的模块边界与启动链路
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 之前,建议先把输出存一份,因为安装脚本可能向源码目录写入编译中间文件,后面对比“哪些文件是源码自带、哪些是构建产物”时,这份清单就是后悔药。
把这几个主要目录的职责和你要关心的重点列出来,读源码时对号入座:
| 目录 | 职责 | 你会改动或需要阅读的部分 |
|---|---|---|
| core | PLC 核心引擎、主循环、IEC 数据类型与标准库 | plc_main.c(主循环)、iec_std_lib.h(标准函数库) |
| lib | Modbus、DNP3 等协议栈 | modbus 从站驱动、串口与网络读写接口 |
| webserver | Web 管理台与上传接口 | 静态页面资源、上传处理逻辑、会话鉴权 |
| 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 2>&1 &启动后用一句话验证三个东西:进程在不在、端口有没有监听、页面开不开。命令合在一起写:
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 服务冲突时改成 8081 |
| Modbus 端口 | 502 | Modbus 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 变量 |
|---|---|---|
| 线圈(Coil) | 0x | %QX 数字量输出 |
| 离散输入(Discrete Input) | 1x | %IX 数字量输入 |
| 输入寄存器(Input Register) | 3x | %IW 模拟量输入 |
| 保持寄存器(Holding Register) | 4x | %QW / %MW 输出与内部字 |
不同小版本在地址偏置上偶尔差 1(0 偏置和 1 偏置之争),调试时先用一个已知值写进寄存器,再从客户端读回来验证,这一步能省下大量“明明连上了却读不到”的排查时间。验证脚本用 Python 很顺手,下面是一段用 pymodbus 读写保持寄存器的示例:
from pymodbus.client import ModbusTcpClient client = ModbusTcpClient("192.168.1.20", port=1502) if client.connect(): # 读保持寄存器,从地址 0 开始,读 8 个 result = client.read_holding_registers(0, count=8, slave=1) if not result.isError(): print("registers:", result.registers) # 回写一个已知值,验证写路径 client.write_register(0, 1234, slave=1) 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] Description=OpenPLC Runtime After=network.target [Service] ExecStart=/opt/OpenPLC_v3-master/start_openplc.sh WorkingDirectory=/opt/OpenPLC_v3-master Restart=on-failure User=root [Install] WantedBy=multi-user.target配置完成后依次执行 systemctl daemon-reload 和 systemctl enable openplc 使其开机自启。注意 ExecStart 建议写成启动脚本的绝对路径,WorkingDirectory 保证脚本里所有相对路径都指向源码根目录,这两处配错是服务起不来的头号原因。
5.5 程序状态正常,物理输出却纹丝不动
现象:ARM 单板机上跑 runtime,Web 管理台里变量状态完全正常,程序也在运行,但接在 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 相关项目里养成的习惯是:任何一台新机器落地,第一件事改默认口令,第二件事确认监听地址只绑定内网网卡,第三件事把扫描周期调到满足任务需求的最大值再慢慢往下压。顺序反过来的人,多半在后面某个深夜为安全问题或性能抖动付出过代价。这个顺序也送给你,希望帮到你。
本文还有配套的精品资源,点击获取