用Arduino Uno打造你的第一台开源PLC:从零开始的OpenPLC移植实战
你有没有想过,只花不到10美元,就能拥有一台符合工业标准、支持梯形图编程、还能联网监控的可编程逻辑控制器(PLC)?听起来像天方夜谭?其实它已经实现了——通过将OpenPLC成功移植到Arduino Uno上。
这不是实验室里的概念验证,而是一条真实可行的技术路径。对于自动化初学者、电子爱好者、高校师生甚至小型产线开发者来说,这不仅降低了学习门槛,更打开了一扇通往“开源工业控制”的大门。
本文不讲空话套话,也不堆砌术语,而是带你一步步走完从环境搭建到实际运行的完整流程,重点解决资源受限下的关键问题:内存不够怎么办?没有操作系统怎么跑?Modbus如何精简?I/O怎么映射?
准备好了吗?我们从一个最现实的问题开始。
为什么要在Arduino上跑OpenPLC?
传统PLC贵、封闭、难调试。一台入门级西门子S7-1200动辄上千元,开发软件还要授权,对学生和创客极不友好。而单片机开发虽然便宜灵活,但缺乏标准化编程方式,写出来的代码难以维护,更别提让电气工程师看懂了。
OpenPLC的出现,恰好填补了这个空白。它是全球首个开源的IEC 61131-3兼容PLC平台,支持梯形图(LD)、功能块图(FBD)、结构化文本(ST)等工业标准语言。原本运行在Linux或Windows上,但现在,有人把它“塞”进了只有32KB闪存、2KB内存的ATmega328P芯片里——也就是你手边那块几块钱买的Arduino Uno。
这背后的意义远不止“炫技”。
这意味着你可以:
- 用图形化IDE画梯形图,一键生成能在Arduino上运行的控制逻辑;
- 保留工业级编程范式的同时,享受极低硬件成本;
- 自由修改底层驱动,接入任何传感器或执行器;
- 搭建微型自动化系统,用于教学实验、原型验证甚至小批量产线控制。
当然,挑战也很明显:RAM仅2KB,Flash仅32KB,没有RTOS,没有TCP/IP协议栈……这些都得靠“裁剪+重构”来解决。
接下来我们就看看,这条路到底该怎么走。
OpenPLC是个什么玩意儿?
先别急着烧录程序,搞清楚它的本质才能少走弯路。
OpenPLC不是一个完整的PLC硬件,而是一个开源的PLC运行时引擎。你可以把它理解为“PLC的操作系统”——它负责解析用户编写的控制逻辑(比如梯形图),然后按照PLC典型的扫描周期去执行。
它是怎么工作的?
典型的PLC每毫秒都在重复这三个步骤:
- 输入采样:读取所有外部输入状态(如按钮是否按下);
- 程序执行:根据用户逻辑计算输出结果;
- 输出刷新:把结果写回继电器、指示灯等设备。
整个过程在一个无限循环中进行,称为“扫描周期”,通常在几毫秒到几十毫秒之间。
OpenPLC的核心文件主要包括:
-plc_program.cpp:存放由梯形图编译出的C++逻辑;
-main.cpp:主循环调度器;
-modbus.h/.cpp:实现Modbus通信;
-vios.h:虚拟I/O系统,用来连接软件变量和物理引脚。
这套架构本是为PC或嵌入式Linux设计的,直接搬到Arduino上会“水土不服”。所以我们需要做的是:剥离多余组件,重写底层接口,让它适应裸机AVR环境。
Arduino Uno能扛得住吗?
很多人看到这里都会问一句:“就这?”
毕竟Arduino Uno的配置摆在那儿:
| 参数 | 数值 |
|---|---|
| MCU | ATmega328P |
| 主频 | 16 MHz |
| Flash | 32 KB(可用约31.5KB) |
| SRAM | 2 KB |
| EEPROM | 1 KB |
| 数字IO | 14个(6个PWM) |
| 模拟输入 | 6路(10位ADC) |
看起来确实寒酸。原始OpenPLC项目依赖C++ STL、动态内存分配、POSIX线程、socket网络……随便一项都能让Uno当场罢工。
但我们不是要运行全功能版本,而是构建一个轻量化的OpenPLC运行时子集,只保留最核心的功能:
- ✅ 梯形图逻辑执行
- ✅ 数字/模拟I/O控制
- ✅ Modbus RTU串行通信(RS485)
- ✅ 固定扫描周期调度
其他统统砍掉:
- ❌ 不要TCP/IP
- ❌ 不要动态new/delete
- ❌ 不要文件系统
- ❌ 不要加密认证
这样下来,代码体积可以压缩到25KB以内,RAM使用控制在1.5KB左右,完全可行。
移植四步走:动手前必看的关键改造
别一上来就往IDE里塞代码。真正的难点不在“能不能编译”,而在“能不能稳定运行”。以下是经过验证的四步法,每一步都是坑,但也都有解。
第一步:搭好开发环境
推荐使用PlatformIO + VS Code,比Arduino IDE更适合管理复杂项目。
安装步骤如下:
# 创建项目 pio project init --board uno # 添加必要的库(假设社区已发布适配版) pio lib install "OpenPLC_Arduino_Core"同时你需要下载OpenPLC Studio(原名OpenPLC Editor),这是官方图形化IDE,支持绘制梯形图并导出为.cpp文件。
⚠️ 注意:默认导出的是Linux平台代码,不能直接用!必须配合一个专为AVR优化的OpenPLC内核,比如GitHub上的
thiagoralves/OpenPLC_Arduino分支。
第二步:重写I/O抽象层(vios)
这是移植中最关键的一环。OpenPLC通过一组全局指针数组来访问I/O变量:
uint8_t* discreteInputs[8]; // 数字输入 uint8_t* discreteOutputs[8]; // 数字输出 uint16_t* analogInputs[8]; // 模拟输入 uint16_t* analogOutputs[8]; // 模拟输出这些指针最终要指向具体的GPIO引脚。我们需要自己实现updateBuffersIn()和updateBuffersOut()函数。
示例代码:vios_arduino.h
#define MAX_DISCRETE_INPUT 8 #define MAX_DISCRETE_OUTPUT 8 #define MAX_ANALOG_INPUT 4 #define MAX_ANALOG_OUTPUT 2 // 缓冲区(静态分配,避免malloc) static uint8_t di_buf[MAX_DISCRETE_INPUT]; static uint8_t do_buf[MAX_DISCRETE_OUTPUT]; static uint16_t ai_buf[MAX_ANALOG_INPUT]; static uint16_t ao_buf[MAX_ANALOG_OUTPUT]; // 引脚映射表(可自定义) const int discreteInputPins[] = {2, 3, 4, 5, 6, 7, 8, 9}; const int discreteOutputPins[] = {10, 11, 12, 13, A0, A1, A2, A3}; const int analogInputPins[] = {A0, A1, A2, A3}; const int analogOutputPins[] = {3, 5, 6, 9}; // 支持PWM的引脚 void setupVios() { for (int i = 0; i < MAX_DISCRETE_OUTPUT; i++) { pinMode(discreteOutputPins[i], OUTPUT); digitalWrite(discreteOutputPins[i], LOW); } } void updateBuffersIn() { for (int i = 0; i < MAX_DISCRETE_INPUT; i++) { if (discreteInputs[i]) { *discreteInputs[i] = digitalRead(discreteInputPins[i]); } } for (int i = 0; i < MAX_ANALOG_INPUT; i++) { if (analogInputs[i]) { *analogInputs[i] = analogRead(analogInputPins[i]); } } } void updateBuffersOut() { for (int i = 0; i < MAX_DISCRETE_OUTPUT; i++) { if (discreteOutputs[i]) { digitalWrite(discreteOutputPins[i], *discreteOutputs[i]); } } for (int i = 0; i < MAX_ANALOG_OUTPUT; i++) { if (analogOutputs[i]) { analogWrite(analogOutputPins[i], *analogOutputs[i] >> 2); // 映射0-1023 → 0-255 } } }🔍 关键点:所有缓冲区必须静态分配,禁用
new和malloc。否则RAM很快耗尽。
第三步:精简Modbus协议栈
原版OpenPLC内置完整的Modbus TCP/RTU协议栈,但我们只需要Modbus RTU从站模式,通过串口与HMI通信。
删除所有TCP相关代码,保留以下功能码即可:
- 功能码0x01:读线圈状态(DO)
- 功能码0x02:读离散输入(DI)
- 功能码0x05:写单个线圈
- 功能码0x03 / 0x04:读保持寄存器 / 输入寄存器(AI/AO)
精简版Modbus轮询函数
void modbus_update() { static uint8_t frame[64]; if (Serial1.available()) { int len = Serial1.readBytes(frame, sizeof(frame)); if (validateModbusRTUFrame(frame, len)) { processModbusRequest(frame); } } }接一个MAX485模块,就可以连上触摸屏、Node-RED或者SCADA系统了。
第四步:整合主循环
Arduino的标准模型是setup()+loop()。我们要把OpenPLC的扫描周期嵌入进去。
主循环设计要点:
- 使用
millis()控制固定扫描周期(如50ms); - 顺序执行:输入→逻辑→输出→通信;
- 可选启用看门狗防止死锁。
#include <avr/wdt.h> void setup() { wdt_disable(); Serial.begin(9600); Serial1.begin(19200); // Modbus RTU波特率 setupVios(); wdt_enable(WDTO_2S); // 启用看门狗 } void loop() { static unsigned long last_scan = 0; unsigned long now = millis(); if (now - last_scan >= 50) { // 每50ms一次扫描 updateBuffersIn(); executeLogic(); // 来自OpenPLC Studio生成的逻辑 updateBuffersOut(); modbus_update(); last_scan = now; } wdt_reset(); // 喂狗 }✅ 这样做既保证了周期性,又不会阻塞通信响应。
实战案例:做个带远程监控的电机启停箱
纸上谈兵终觉浅。我们来看一个真实应用场景。
需求描述
某小型传送带需要一个控制箱,要求:
- 本地有启动/停止按钮;
- 电机具备自锁和互锁保护;
- 能被上位机读取状态、远程启停;
- 成本尽可能低。
解决方案
| 组件 | 作用 |
|---|---|
| Arduino Uno | 核心控制器 |
| 按钮×2 | 启动/停止输入(接D2/D3) |
| 继电器模块 | 控制电机电源(接D10) |
| MAX485模块 | 接入Modbus网络 |
| HMI或Node-RED | 远程监控界面 |
步骤流程
在OpenPLC Studio中画一个“启动-保持”电路:
- I0.0 = 启动按钮
- I0.1 = 停止按钮
- Q0.0 = 继电器输出
- 加入互锁逻辑防误动作导出为
main_program.cpp,复制到Arduino工程中。修改
variables.csv映射关系:
Name,Location,Type,Initial Value,Comment %IX0.0,%IX0.0,BOOL,0,"Start Button" %IX0.1,%IX0.1,BOOL,0,"Stop Button" %QX0.0,%QX0.0,BOOL,0,"Motor Relay"
编译烧录,接线测试。
上位机通过Modbus读取Q0.0状态,也可强制写入实现远程控制。
✅ 结果:总物料成本不足50元,开发时间不到一天,逻辑清晰可维护。
踩过的坑与避坑指南
别以为编译通过就万事大吉。我在调试过程中踩过不少雷,总结几个高频问题:
❌ 问题1:程序跑着跑着就卡死了
原因:未启用看门狗,或逻辑中有死循环。
解决:务必开启wdt,并在主循环中定期wdt_reset()。
❌ 问题2:模拟量读数跳变严重
原因:电源干扰或ADC参考电压不稳定。
解决:使用外部基准电压(如LM336),加滤波电容。
❌ 问题3:Modbus通信超时
原因:串口波特率不匹配,或帧间隔太短。
解决:确保主从设备波特率一致;RTU模式要求帧间≥3.5字符时间。
❌ 问题4:变量无法正确映射
原因:variables.csv索引与C数组不对应。
解决:手动检查discreteOutputs[0]是否真的指向Q0.0。
✅ 最佳实践建议
- 禁用浮点运算:ATmega328P无FPU,用整型或定点数代替。
- 关闭日志输出:原版OpenPLC有很多
printf,全部注释掉。 - 电源去耦:每个IC旁加0.1μF陶瓷电容,抗干扰。
- 固件完整性校验:加入CRC检测,防止程序损坏。
- 预留调试接口:留一个LED闪烁标志运行状态。
这只是开始:下一步能做什么?
Arduino Uno只是起点。一旦你掌握了这套移植方法论,就可以轻松扩展到更强平台:
- ESP32版OpenPLC:支持Wi-Fi/蓝牙,实现Modbus TCP、MQTT上传;
- 树莓派Pico + FreeRTOS:双核Cortex-M0+,跑更复杂的控制算法;
- 多节点协同:多个OpenPLC通过CAN或RS485组网,实现分布式控制;
- 集成OPC UA:迈向现代工业互联标准;
- 边缘智能融合:结合TinyML,在端侧做简单预测性维护。
更重要的是,这种“标准PLC逻辑 + 开源硬件”的组合,正在重塑教育和小型工业场景的开发模式。
如果你是一名自动化专业的学生,现在可以用十分之一的成本完成课程设计;
如果你是工厂的设备工程师,可以用它快速搭建临时控制系统;
如果你是创客,那你已经拥有了一个真正意义上的“工业级大脑”。
OpenPLC + Arduino 的结合,不只是技术的嫁接,更是一种理念的解放:
让每一个人都有机会亲手构建属于自己的工业控制系统。
你准备好动手了吗?