> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 没有硬件也能练 PLC：Factory I/O + OpenPLC 虚拟实验台
- URL: https://www.hubwiz.com/blog/virtual-plc-lab-factory-io-openplc-modbus-tcp/
- Published: 2026-10-16T04:09:37.000Z
- Updated: 2026-10-16T04:22:13.000Z
- Description: PLC 不在乎水箱是钢还是像素，只在乎 I/O 映像。用 Factory I/O + OpenPLC + Modbus 搭实验台：14 级梯形图、互锁与倒计时。
- Author: admin
- Tags: PLC, 梯形图, OpenPLC, Factory I/O, Modbus TCP

**一句话结论**：想学 PLC，不必先买一台控制器、一个水箱和一套按钮。**PLC 根本不在乎水箱是钢的还是像素的，它只在乎自己在网络上读写的那个 I/O 映像**。这篇要复现的虚拟实验台由三部分组成：**Factory I/O 扮演被控过程（Modbus 服务器）**，**OpenPLC 扮演 PLC（Modbus 主站，同时自带从站服务器）**，两者之间跑**真实的 Modbus TCP**——不是进程内的假连接。最终的梯形图程序是 **14 级**：两个带定时器的锁存流程、一条相互排斥的互锁、两盏面板指示灯和一个由 PLC 计算、经总线传到面板的倒计时。跑通之后，两个数字就能说明一切：灌水计时器走到 **2.2 秒**、剩余 **7.8 秒**时，面板显示 **8**（10 − 2.2 截断取整）——它同时证明了定时器在计数、算术链正确、数据真的走完了 Modbus。

本文完整编译自 dev.to @almahmudkhalif《Virtual PLC Lab: Ladder Logic Control of a Filling Tank with Factory I/O and OpenPLC over Modbus TCP》（2026-10-09），截图为原文作者实机捕获，做中文整理。

![Factory I/O 的 Filling Tank（Timers）场景：水箱、进出水阀、Fill/Discharge 按钮与计时显示面板](https://www.hubwiz.com/blog/content/images/2026/10/img_056ca1da1e9b18f576acad84e972b549.jpg)

---

## 一、要练什么：一个带真实协议的教学目标

作者给自己定的目标很具体：**用 PLC 程序控制一个水箱，而且过程要像真的设备**。

- 按一次按钮开始灌水——操作员**不需要按住**按钮；
- 灌水在固定时间后**自动停止**（没有液位传感器，时间就是唯一的控制参数）；
- 第二个按钮让水箱排水，同样是定时；
- **灌水和排水绝不允许同时发生**——这是真互锁，不是"最好别这样"；
- 面板指示灯显示当前处于哪个阶段；
- 面板显示**实时倒计时**，让操作员看得到还剩多久。

用的场景是 Factory I/O 的 **3 - Filling Tank (Timers)**：水箱、进水阀、出水阀、两个带灯按钮（Fill、Discharge）和一个数字计时显示屏。注意：**按钮里的灯由 PLC 驱动，不是手指按亮的**——所以这块面板同时是状态指示器。

## 二、为什么用 Modbus TCP，而不是"假连接"

这是整个实验台最值得学的一个设计决定。作者说得很清楚：**如果把两个程序用进程内的假链接粘在一起，就什么都学不到**——因为你绕过了"现场 I/O 到底怎么到达控制器"这件事。

改用 Modbus TCP 之后，两半程序就是**两个真正独立的设备，通过 socket 对话**，跟真实系统的方式一致。

| 角色   | 用什么                                   | 扮演什么                                        |
| ---- | ------------------------------------- | ------------------------------------------- |
| 被控过程 | Factory I/O v2.5.10（Ultimate Edition） | 水箱、阀门、按钮、灯、显示；跑一个 Modbus TCP **服务器**        |
| PLC  | OpenPLC Runtime v4                    | 执行程序；自带的 Modbus **主站（客户端）** \+ 自己的**从站服务器** |
| 工程站  | OpenPLC Editor v4                     | 画梯形图、下载到运行时、在线监视                            |
| 现场总线 | Modbus TCP                            | 在两半之间搬运 I/O                                 |

有个容易绕晕的地方要提前讲清：**Factory I/O 是 Modbus 服务器（从站），PLC 才是 Modbus 主站（客户端）**，由 PLC 每 100 ms 轮询一次过程。控制器当客户端听起来别扭，但这恰恰是现实中很多"PLC 对远程 I/O"的通信方式。

![OpenPLC Runtime 控制台：从站监听 0.0.0.0:502，主站连上模拟器，PLC 处于 RUNNING](https://www.hubwiz.com/blog/content/images/2026/10/img_09b41bb05e475140af0212703857d7f4.jpg)

## 三、步骤 1：配置过程模拟器，先数清楚 I/O

安装 Factory I/O，打开场景 **3 - Filling Tank (Timers)**。花几分钟逐个点击场景里的元件，读它们的**标签名**——后面写地址映射时要用到这些**完全一致**的名字。

在动手接线之前，先看清楚这个场景**没有什么**。作者最终确定的驱动配置是：

- 数字输入 **3 个**（Fill 按钮、Discharge 按钮、"FACTORY I/O (Running)"）；
- 数字输出 **4 个**（进水阀、出水阀、灌水指示灯、排水指示灯）；
- 寄存器输入 **0 个**；
- 寄存器输出 **1 个**（计时显示屏）。

**那个 0 就是整个设计的起点**：没有任何模拟量液位值被传过来，所以想按液位控制也不可能——灌水和排水时长成了仅有的控制参数，解法只能用定时器。这是一个很好的习惯：**先看清接口边界，再决定控制策略。**

## 四、步骤 2：启动 Runtime，并在控制台自查三件事

先启动 OpenPLC Runtime——编辑器需要一个正在运行的目标才能下载，在线监视也依赖它。

检查控制台输出，作者那次长这样：

```
MODBUS_SLAVE  Server listening on 0.0.0.0:502
MODBUS_SLAVE  Plugin modbus_slave started successfully
MODBUS_MASTER (PASS) Connected to TCP 192.168.56.1:502 (attempt 1)
PLC base tick: 100000000 ns across 1 task(s)
PLC State: RUNNING

```

要确认的三件事：

1. **从站插件在 502 端口监听**（这是 Runtime 自己的 I/O 映像，后面验证阶段要用）；
2. **主站连上了模拟器的 IP 与端口**；
3. **PLC 确实处于 RUNNING**，而不是 stopped。

你还会看到 `Memory locking not available`、`SCHED_FIFO(99)` 调度失败之类的告警。在虚拟机或 Windows 上这是预期现象——只说明运行时不是硬实时。对一个 100 ms tick 的模拟过程来说完全够用。

## 五、步骤 3：建项目，把两半接起来

在 OpenPLC Editor 里新建项目（作者的叫 `Tank_Filling`），一个程序 `main`。打开 **Device Configuration** 指向目标运行时；当按钮从 Connect 变成 Disconnect、状态显示 **PLC: RUNNING**，说明编辑器已接上，之后可以在线看逻辑执行。

然后是两个通信对象：

**PLC 自己的从站服务器**（项目树 → Servers → modbus）：启用；传输方式 Modbus TCP；网络接口 `All Interfaces (0.0.0.0)`；端口 **502**；从站 ID **1**。它让外部客户端在不碰程序的情况下读 PLC 的 I/O 映像——第十节验证要用。

**远程设备**（项目树 → Remote Devices → modbus1）：这是 PLC 的客户端侧，指向 Factory I/O：

```
Protocol   : Modbus (TCP/IP)
IP address : 192.168.56.1      # Factory I/O 服务器绑定的网卡
Port       : 502
Timeout    : 1000 ms
Slave ID   : 1

```

然后把过程信号和 PLC 地址绑起来：

| 方向       | Factory I/O 标签 | Modbus 引用                  | PLC 地址 |
| -------- | -------------- | -------------------------- | ------ |
| 过程 → PLC | Fill（按钮）       | Discrete Input 0 — FC 02   | %IX0.0 |
| 过程 → PLC | Discharge（按钮）  | Discrete Input 1 — FC 02   | %IX0.1 |
| PLC → 过程 | 进水阀            | Coil 0 — FC 15             | %QX0.0 |
| PLC → 过程 | 出水阀            | Coil 1 — FC 15             | %QX0.1 |
| PLC → 过程 | 灌水指示灯          | Coil 2 — FC 15             | %QX0.2 |
| PLC → 过程 | 排水指示灯          | Coil 3 — FC 15             | %QX0.3 |
| PLC → 过程 | 计时显示           | Holding Register 0 — FC 16 | %QW0   |

**IEC 地址和 Modbus 引用是怎么对上的**，值得单独理解一次：IEC 61131-3 里用 `%IX`（输入位）、`%QX`（输出位）、`%QW`（输出字）描述过程映像；Modbus 侧则用离散输入（Discrete Input）、线圈（Coil）、保持寄存器（Holding Register）这些数据区描述对象。编辑器在中间做映射，功能码决定读写的语义——**FC 02 读离散输入、FC 15 写多个线圈、FC 16 写多个保持寄存器**。搞错方向（比如把"过程→PLC"接成写线圈），症状往往不是报错，而是"值不动"或"逻辑行为怪异"，这是新手最费时的一类问题。

两个"照抄会踩"的点：

- **PLC 只读两个离散输入（length 2）**，所以模拟器的第三个输入 "FACTORY I/O (Running)" 根本到不了程序里。想拿它当"仿真运行中"的总闸，得先把 length 改成 3。
- **两端的从站 ID 必须一致。** ID 不匹配的后果是**静默超时**，而不是有用的报错。

![OpenPLC Editor 的 Remote Device 配置页：TCP/IP、192.168.56.1:502、超时 1000ms、Slave ID 1，以及 IO Tag Mapping 表](https://www.hubwiz.com/blog/content/images/2026/10/img_25e216e4adb967b186e937db42ea4059.jpg)

## 六、步骤 4：声明过程接口（14 个变量）

| 变量           | 类型    | 地址     | 用途            |
| ------------ | ----- | ------ | ------------- |
| Fill\_Btn    | BOOL  | %IX0.0 | 灌水按钮（操作员输入）   |
| Disch\_Btn   | BOOL  | %IX0.1 | 排水按钮（操作员输入）   |
| Fill\_Valve  | BOOL  | %QX0.0 | 进水阀           |
| Disch\_Valve | BOOL  | %QX0.1 | 出水阀           |
| Fill\_Light  | BOOL  | %QX0.2 | Fill 按钮灯      |
| Disch\_Light | BOOL  | %QX0.3 | Discharge 按钮灯 |
| Timer\_Disp  | INT   | %QW0   | 面板倒计时         |
| T\_Fill      | TON   | —      | 灌水定时器         |
| T\_Disch     | TON   | —      | 排水定时器         |
| Fill\_Seq    | BOOL  | —      | 灌水阶段锁存        |
| Disch\_Seq   | BOOL  | —      | 排水阶段锁存        |
| Fill\_sec    | LREAL | —      | 灌水已用秒数        |
| Disch\_sec   | LREAL | —      | 排水已用秒数        |
| Left\_Calc   | LREAL | —      | 剩余秒数          |

命名纪律值得学：**只有 `*_Valve`、`*_Light` 和 `Timer_Disp` 碰被控过程**，其余全是状态与算术。一眼就能分清"哪些线是接出去的"。

## 七、步骤 5：写梯形图（14 级、四组）

先用结构化文本（ST）表达意图，更好读：

```iecst
(* ---- 灌水阶段 ---- *)
Fill_Seq  := (Fill_Btn OR Fill_Seq) AND NOT Disch_Seq AND NOT T_Fill.Q;
T_Fill(IN := Fill_Seq, PT := T#10s);
Fill_Valve := Fill_Seq;
Fill_Light := Fill_Seq;

(* ---- 排水阶段 ---- *)
Disch_Seq := (Disch_Btn OR Disch_Seq) AND NOT Fill_Seq AND NOT T_Disch.Q;
T_Disch(IN := Disch_Seq, PT := T#10s);
Disch_Valve := Disch_Seq;
Disch_Light := Disch_Seq;

(* ---- 面板倒计时 ---- *)
IF Fill_Seq THEN
    Fill_sec  := TIME_TO_S(T_Fill.ET);      (* 已用时间 -> 秒 *)
    Left_Calc := 10.0 - Fill_sec;           (* 剩余 *)
    Timer_Disp := TO_INT(Left_Calc);        (* 面板收整数 *)
ELSIF Disch_Seq THEN
    Disch_sec := TIME_TO_S(T_Disch.ET);
    Left_Calc := 10.0 - Disch_sec;
    Timer_Disp := TO_INT(Left_Calc);
END_IF;

```

**Rung 1–3：灌水阶段。** Rung 1 是自锁：

```
(Fill_Btn OR Fill_Seq) AND NOT Disch_Seq AND NOT T_Fill.Q → Fill_Seq

```

与 `Fill_Btn` 并联的 `Fill_Seq` 触点让"按一次"能启动并保持整个阶段；`NOT Disch_Seq` 是互锁；`NOT T_Fill.Q` 让阶段**自己结束**。Rung 2 是定时器 `TON T_Fill`，`PT := T#10s`。Rung 3 用阶段位驱动进水阀——**锁存一掉，阀门不可能还开着**。

![Rung 01：Fill_Btn 与 Fill_Seq 并联自锁，串 Disch_Seq 与 T_Fill.Q 的常闭触点，驱动 Fill_Seq](https://www.hubwiz.com/blog/content/images/2026/10/img_fed1af2b31555cb90a91e57705ae0795.jpg)

**Rung 4–6：排水阶段。** 灌水的镜像，安全条件就在这里：

```
(Disch_Btn OR Disch_Seq) AND NOT Fill_Seq AND NOT T_Disch.Q → Disch_Seq

```

Rung 4 的 `NOT Fill_Seq` 意味着"灌水时排不了水"，Rung 1 的 `NOT Disch_Seq` 意味着"排水时灌不了水"。**两个阶段在结构上互斥，而不是靠操作员自觉。**

**Rung 7–8：面板指示。** `Fill_Seq → Fill_Light`、`Disch_Seq → Disch_Light`。对过程不是必需，但顺手把面板变成了状态显示器。

![Rung 03：Fill_Seq 常开触点直接驱动进水阀线圈 Fill_Valve](https://www.hubwiz.com/blog/content/images/2026/10/img_86692cc3586b1a9bf325e33c4bd7c445.jpg)

**Rung 9–14：倒计时。** 定时器的 `ET` 是 `TIME` 类型，不能直接做算术，所以 Rung 9 先转换：`TIME_TO_S(T_Fill.ET) → Fill_sec`；Rung 10 相减（`10.0 - Fill_sec → Left_Calc`）；Rung 11 截断成整数写进显示寄存器（`TO_INT(Left_Calc) → Timer_Disp`）。Rung 12–14 用同样的两个暂存变量和同一个显示寄存器，为排水阶段重复这条链——程序因此保持得很短。

这里能顺手学到一个真实工程的纪律：**预设值 10 秒不要硬编码在多个地方**。一旦要把灌水时间改成 12 秒，梯级、定时器 PT、倒计时减法都必须同步修改，漏一处就出现"阀门按 12 秒关、面板却按 10 秒数"的分裂。做法是把它抽成一个常量，三处都引用它。

## 八、步骤 6：配置 Factory I/O 驱动

回到 Factory I/O 打开驱动页，选 **Modbus TCP/IP Server**：

```
Port                : 502
Slave ID            : 1
Network adapter     : PLC 能到达的那块网卡
Write Digital       : Inputs              # 模拟器发给 PLC 的
Read Digital        : Coils               # 模拟器从 PLC 取的
Write Register      : Input Registers
Read Register       : Holding Registers
Scale               : 1
I/O points          : 3 DI, 4 DO, 0 register inputs, 1 register output

```

驱动启动、PLC 客户端连上后，绿色连接指示会出现。驱动页会把映射在一屏里列出来——这是确认"coil 0 真的是进水阀而不是某盏灯"最快的方法。

## 九、步骤 7：跑起来，看三件事

挂上编辑器调试器，按下 **Fill**：`Fill_Seq`、`Fill_Valve`、`Fill_Light` 同时变 TRUE，梯级高亮显示有电流通过；进水阀打开、水箱开始灌水、Fill 灯亮；面板从 10 开始倒数。

作者的捕获：灌水计时器显示**已用 2.2 秒、剩余 7.8 秒**，面板显示 **8**（10 − 2.2 截断取整）。**一个数字同时验证了三件事**：定时器在计数、算术链正确、数值真的经 Modbus 到了模拟器显示屏上。

- **互锁测试**：灌水中故意按 Discharge。调试器里 `Disch_Btn = TRUE`，但 `Disch_Seq = FALSE`——**命令到达了 PLC，程序拒绝了它**，因为 Rung 4 要求 `NOT Fill_Seq`。出水阀纹丝不动。这就是互锁的意义：**操作员可以按错，过程仍然正确。**
- **灌水完成**：10 秒到，`T_Fill.Q` 变 TRUE，锁存断开，进水阀自动关闭，无需再输入。
- **排水**：按 Discharge 锁存 `Disch_Seq`，出水阀打开、第二盏灯亮、倒计时重新开始——此时调试器显示灌水定时器停在 10.0 秒、排水计时器已用 1.9 秒（剩余 8.1 秒，面板显示 8）。

## 十、怎么验证：三层，别只信模拟器窗口

**第 1 层：先查网络路径，再怀疑逻辑。**

```bash
ping 192.168.56.1                     # PLC 侧能到达模拟器吗
netstat -ano | findstr :502           # Windows：谁在监听 502
ss -ltnp | grep 502                   # Linux / macOS

```

如果主站打印了 `Connected ... (attempt 1)`，说明 socket 是通的，剩下的问题只可能在逻辑或地址映射。

**第 2 层：轮询 PLC 自己的 I/O 映像。** Runtime 暴露了 502 端口的从站服务器，可以在不打开编辑器的情况下读实时输出：

```python
# pip install pymodbus
from pymodbus.client import ModbusTcpClient

client = ModbusTcpClient("127.0.0.1", port=502)
client.connect()
coils = client.read_coils(0, 4)              # %QX0.0 .. %QX0.3
regs  = client.read_holding_registers(0, 1)  # %QW0 = Timer_Disp
print("Fill valve      :", coils.bits[0])
print("Discharge valve :", coils.bits[1])
print("Fill lamp       :", coils.bits[2])
print("Discharge lamp  :", coils.bits[3])
print("Countdown       :", regs.registers[0])
client.close()

```

灌水中运行：进水阀与灯 TRUE、倒计时在走、出水阀 FALSE。中途按 Discharge 什么都不该变——**从 PLC 外面看到的互锁**。注意：不同 pymodbus 版本可能要写成 `read_coils(0, 4, slave=1)` 或 `device_id=1`。

**第 3 层：对账数字。** 面板显示应该永远等于**预设值 − 已用时间，截断取整**。显示值与调试器里的 `ET` 对不上，问题就在转换梯级，而不在通信。这一层看起来只是算术，实际是在验证一条完整的端到端链路：**逻辑 → 算术 → Modbus 写 → 过程显示**，任何一环断了它都会先露馅。

## 十一、作者总结出的六条经验

1. **定时器是控制策略，不是退路。** 没有液位传感器，时间就是唯一能控的量，这会改变设计：水箱不能"灌到某个液位"，只能"灌一段时间"。选了 10 秒，就等于接受**液位是结果而不是目标**。
2. **自锁改变了操作员的工作方式。** 有锁存，按一次就启动一个会自己完成的阶段；没有锁存，操作员得按住按钮 10 秒——又累又危险。
3. **互锁属于逻辑，不属于操作规程。** 把 `NOT Fill_Seq` 写进排水梯级，意味着**任何按键顺序都不可能同时打开两个阀门**；写在手册里，只是祈祷。
4. **数据方向是最容易搞错的部分。** 这里是控制器当 Modbus 客户端、轮询模拟器的服务器；方向、功能码（FC 02 读、FC 15 写线圈、FC 16 写寄存器）占了通信工作的大部分。
5. **倒计时很便宜，但很值。** 三条梯级就把定时器变成面向操作员的显示，而且它是**最好的端到端测试**：面板数字正确，就说明逻辑、算术、Modbus 写路径全都通了。
6. **顺手想一想安全。** Modbus TCP **没有认证、没有加密、没有完整性检查**——任何能到达 502 端口的东西都能读写过程 I/O。这正是两个程序只需要 IP、端口和从站 ID 就连上的原因，也是真实系统必须依赖**网络分段、防火墙和受监控的通道**、而不是协议本身的原因。

第 6 条值得再补一句。自锁的按钮还藏着一个真实世界常见的隐患：**如果按钮是电平式的、又没有上升沿检测，定时器复位后按住不放会再次触发循环**。练习阶段就该养成习惯——在按钮触点前加 `R_TRIG`（上升沿触发），放到真实面板上时，手感和可靠性都对。

## 十二、10 个常见坑

| 坑                | 你会看到什么           | 怎么修                             |
| ---------------- | ---------------- | ------------------------------- |
| 两端从站 ID 不一致      | 超时或静默失败，没有有用的报错  | 设成同一个 ID（1 就行）                  |
| 先启动 PLC、后启动模拟器驱动 | PLC 连上了，但什么都不动   | 先起 Factory I/O 驱动，再看连接指示        |
| 模拟器服务器绑错网卡       | 主站永远连不上，ping 也不通 | 绑到 PLC 实际能到达的网卡                 |
| 以为有液位传感器         | 模拟量永远不来，液位控制不工作  | 看寄存器输入数量——是 0 就说明是定时器场景         |
| 电平式按钮没有边沿检测      | 定时器复位后按住按钮会重启循环  | 在按钮触点前加 R\_TRIG（上升沿）            |
| 预设值硬编码在两个地方      | 改了 T#10s，倒计时却不对  | 用一个常量，减法也引用它                    |
| 忘了显示是整数寄存器       | 数值差一，或显示一直不变     | 写入 %QW0 前显式 TO\_INT             |
| 502 端口被占用        | 从站插件绑定失败         | 启动 Runtime 前查端口占用               |
| 只盯着 3D 场景调试      | 分不清是 PLC 还是映射出问题 | 用在线监视 + 从 Modbus 读 PLC 的 I/O 映像 |
| 把锁存写成"操作员按住"     | 需要一直按住按钮         | 并联阶段位自锁                         |

## 十三、从虚拟实验台到真实工程

这套实验台练的是通用能力：**接口盘点、数据方向、锁存、互锁、定时、端到端验证**。迁移到真实项目时，通常沿着两条路走：

- **留在通用生态**：把 OpenPLC 换成真实 PLC（或继续用软 PLC + 现场 I/O），把 Modbus TCP 换成实际使用的现场总线（PROFINET、EtherCAT 等），程序结构不变；
- **转向西门子博途**：在博途里用同样的框架重画梯形图，再用 PLCSIM 做仿真回归。梯形图的编辑风格、定时器实例的组织方式会有差异，但**锁存与互锁的结构逻辑是一样的**——真正需要重新学的，是博途的工程设计习惯（符号表、数据类型、块接口）。

如果是后者，练习阶段的梯形图可以先通过 [LAD2SCL](https://tia.hubwiz.com/lad2scl?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp) 转成 SCL 源码，或从自然语言描述用 [TXT2LAD](https://tia.hubwiz.com/txt2lad?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp) 生成梯形图骨架，再进博途编译与仿真核对。**练习环境里养成的验证习惯，比任何一次"能跑就行"的调试都值钱。**

## 十四、Q&A

**Q：没有 Windows 电脑，能搭这套实验台吗？** Factory I/O 与 OpenPLC Editor 都在 Windows 上跑得最顺（原文环境就是 Windows + 虚拟机网卡 192.168.56.x）。如果只有 Mac/Linux，可以先从浏览器里的梯形图仿真练起，或用支持你平台的软 PLC 替代，协议部分保持一致。

**Q：一定要 Ultimate Edition 吗？** 原文用的就是 Ultimate Edition。是否必须取决于驱动支持与场景库；学习目的是把"真实协议 + 独立两半"的结构跑通，选哪个版本看官方当前的能力说明。

**Q：10 秒的定时练习，改成 30 秒要动哪些地方？** 常量抽出来后只需要一处改动；如果没抽常量，就要同步改定时器 PT 和倒计时减法两处。这正是"预设值管理"这个习惯的练习点。

**Q：这个实验有实际工程价值吗？** 有。它练的是真实项目的核心动作：**先界定接口，再写逻辑，最后用外部手段验证**。真实工程只是多了安全回路、硬件诊断和更严格的上线流程，方法是一致的。

## 十五、小结

虚拟实验台给到的是教科书给不了的东西：**按钮、梯级、线圈和水，在同一个画面里**，而且链条上的每一环——逻辑、定时、算术、Modbus 映射——都是你自己写错、再自己修好的。最终程序 14 级梯级：两个各自带定时器的锁存阶段、一条交叉互锁、两盏显示当前阶段的灯，加一个在 PLC 里计算、经总线传到过程面板的倒计时。一次按钮启动、自动结束、拒绝同时灌排。

有一台 Windows 机器和几个小时就能复现。作者下一步想加的：让急停切断两个阀门、给按钮加边沿检测，以及做一个读同一个 Modbus 服务器的小 HMI。

想先用最低配置练手，也可以从浏览器里的梯形图仿真开始：[BimAnt PLC 在线仿真器](https://plcsim.bimant.com/?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp) 支持触点、线圈、置位/复位、定时器、计数器、比较与数学指令，打开即用；整套博途工程工具的入口见 [TIA Forge](https://tia.hubwiz.com/tia-forge?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp)。

---

## 参考来源

- @almahmudkhalif《Virtual PLC Lab: Ladder Logic Control of a Filling Tank with Factory I/O and OpenPLC over Modbus TCP》，dev.to，2026-10-09。
- 本文完整编译原文的搭台步骤、地址映射、梯形图逻辑、运行观测、三层验证与常见坑；文中截图来自原文作者实机捕获（Factory I/O / OpenPLC 界面），数字以其运行记录为准。迁移路径、Q&A 与预设值/边沿检测的展开为在来源基础上的整理，供参考。

## 延伸阅读（本站）

- [AI × 工业 / PLC 完全指南](https://www.hubwiz.com/blog/ai-industrial-stack/)
- [用开源软件模拟工业控制系统](https://www.hubwiz.com/blog/simulating-industrial-control-systems-with-open-source-softwares/)
- [梯形图新手常犯的 7 个错误](https://www.hubwiz.com/blog/top7-ladder-logic-mistakes-that-trip-up-new-programmers/)
- [用梯形图实现状态机](https://www.hubwiz.com/blog/programming-a-state-machine-in-ladder-logic/)
- [梯形图转 SCL 迁移实战指南](https://www.hubwiz.com/blog/lad-to-scl-migration-guide-tia-portal/)
- [接手别人的 PLC 程序：动第一行代码前的 4 步取证](https://www.hubwiz.com/blog/taking-over-legacy-plc-program-4-steps/)

工具入口：[PLC 在线仿真器](https://plcsim.bimant.com/?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp) ｜ [LAD2SCL](https://tia.hubwiz.com/lad2scl?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp) ｜ [TIA Forge 总入口](https://tia.hubwiz.com/tia-forge?utm%5Fsource=blog&utm%5Fmedium=article&utm%5Fcampaign=virtual-plc-lab-factory-io-openplc-modbus-tcp)

> **这是「AI × 工业 / PLC」专题的一部分。**完整地图见：[AI × 工业 / PLC 完全指南](https://www.hubwiz.com/blog/ai-industrial-stack/)