假期间隙,可以回顾下C#的知识,这里依旧对比西门子博图来进行
用C#的控制台程序写一个基本的电机起保停控制如下
using System;
namespace AutoEquipment
{
// ==========================================
// 一、定义类(相当于你在博途里建了一个 FB块)
// ==========================================
public class FB_ConveyorMotor
{
// 1. 属性(Property)------ 就是FB块上面的引脚
public string DeviceName { get; set; } // 设备名字,比如"3号流水线"
// 输入引脚(IN)
public bool StartButton { get; set; } // 启动按钮
public bool StopButton { get; set; } // 停止按钮
// 输出引脚(OUT),private set 表示外部只能看,不能强行改写
public bool IsRunning { get; private set; }
// 内部状态(相当于FB块里的Static静态变量,用于自锁)
private bool _internalLatch = false;
// ==========================================
// 2. 事件(Event)------ 就是上下游的连锁接线
// ==========================================
public event Action<string> OnEmergencyStop;
public event Action<string, bool> OnStatusChanged;
// ==========================================
// 3. 方法(Method)------ 就是FB块里面的逻辑
// ==========================================
// 这个方法就相当于OB1里的扫描周期,每个扫描周期调用一次
public void ExecuteLogic()
{
// 【起保停逻辑】
// 按下停止按钮,自锁断开
if (StopButton == true)
{
_internalLatch = false;
}
// 按下启动按钮,或者已经处于自锁状态,则电机运行
else if (StartButton == true || _internalLatch == true)
{
_internalLatch = true;
}
// 记录上一个扫描周期的状态,用于判断是否发生变化
bool lastState = IsRunning;
IsRunning = _internalLatch;
// 如果状态变了,触发事件通知上下游
if (lastState != IsRunning)
{
OnStatusChanged?.Invoke(DeviceName, IsRunning);
}
}
// 专门用来模拟"急停"的方法
public void TriggerEmergencyStop()
{
_internalLatch = false;
IsRunning = false;
Console.WriteLine($"\n【急停触发】{DeviceName} 急停!");
// 触发事件,通知所有订阅了的上下游设备
OnEmergencyStop?.Invoke(DeviceName);
}
}
// ==========================================
// 二、主程序(相当于你的 OB1)
// ==========================================
class Program
{
static void Main(string\[\] args)
{
Console.WriteLine("--- 开始模拟PLC扫描周期 ---");
// 1. 实例化(在OB1里调用FB,生成背景DB,命名为 motor3)
var motor3 = new FB_ConveyorMotor { DeviceName = "3号流水线" };
// 2. 上下游"接线"(订阅事件)------ 就是你在博途里连的连锁线
motor3.OnEmergencyStop += (deviceName) =>
{
Console.WriteLine($" -> 上游1号、2号收到通知:{deviceName} 急停了,我马上停送料!");
Console.WriteLine($" -> 下游4号收到通知:{deviceName} 急停了,我马上停止接收!");
};
motor3.OnStatusChanged += (deviceName, isRunning) =>
{
string status = isRunning ? "启动" : "停止";
Console.WriteLine($"监控 {deviceName} 状态改变:{status}");
};
// ===== 开始模拟运行 =====
Console.WriteLine("\n--- 模拟1:按下启动按钮 ---");
motor3.StartButton = true;
motor3.ExecuteLogic(); // 扫描周期1:按钮按下
motor3.StartButton = false; // 模拟物理按钮回弹
motor3.ExecuteLogic(); // 扫描周期2:自锁生效,电机保持运行
Console.WriteLine("\n--- 模拟2:按下停止按钮 ---");
motor3.StopButton = true;
motor3.ExecuteLogic(); // 扫描周期1:停止按钮按下
motor3.StopButton = false;
motor3.ExecuteLogic(); // 扫描周期2:确认停机
Console.WriteLine("\n--- 模拟3:按下启动后,突发急停 ---");
// 先快速启动一下
motor3.StartButton = true; motor3.ExecuteLogic(); motor3.StartButton = false; motor3.ExecuteLogic();
// 触发急停
motor3.TriggerEmergencyStop();
// 防止黑框一闪而过,停在这里等你敲回车
Console.WriteLine("\n--- 按回车键退出 ---");
Console.ReadLine();
}
}
}

1. static(静态)
-
博途类比 :不需要分配背景DB块就能调用的系统常驻块(类似OB1,或者无需背景DB的FC)。
-
解释 :普通的方法(比如写的
ExecuteLogic())必须先new一个实例(分配背景DB)才能调用。但加了static,说明它是"全局唯一、直接就能跑"的。C#的Main是整个程序启动的入口,不需要提前去"实例化"它,系统一启动就自动找它。
2. void(无返回值)
-
博途类比 :没有 OUT 引脚的 FC 块 / 只执行逻辑,不输出返回值的动作。
-
解释 :PLC里有些块执行完要把结果传给别的块(有返回值)。但
Main主程序是程序的起点,它执行完,程序就结束了,不需要向"谁"汇报一个结果 ,所以用void(空)。
3. Main(主方法)
-
博途类比 :OB1(主程序循环)。
-
解释 :C#的编译器规定,整个程序必须有且只有一个
Main。按下F5,系统就从这个Main开始逐行往下扫描,跟PLC的扫描周期一模一样。
4. string[] args(字符串数组参数)
-
博途类比 :OB1的启动参数(IN 引脚)。
-
解释 :
string[]代表"字符串数组",args是名字。 -
在博途里,如果上位机(HMI)想在下发启动指令时顺便传个"配方号"给OB1,OB1的接口上就得建一个
IN引脚。args就是这个引脚。 -
它的作用是:你在电脑命令行里输入
MyApp.exe "3号流水线" "启动",args[0]就接住了"3号流水线",args[1]接住了"启动"。 -
注意 :C#强制规定,主程序的参数必须写成
string[](字符串数组)。如果不传参数,它就是空的,完全不用管它。
📝 总结对照表:
| C# 单词 | 博途里的对应 | 详细含义 |
|---|---|---|
| static | 系统级全局块 | 不用建背景DB,系统启动就自动执行 |
| void | 无返回值的FC | 执行完逻辑就结束,不需要把结果输出给调用者 |
| Main | OB1 | 整个程序扫描的起点和入口 |
| string\[\] args | OB1的启动参数(IN引脚) | 外部启动程序时塞进来的初始字符串,不写就是空 |
所以,这句话在博途里的完美翻译是:
"这是一个全局系统级、不需要外部分配背景DB(
static)、执行完不返回任何数据(void)、系统上电就自动从上往下扫描(Main)、并且预留了一个外部启动参数(string[] args)的主程序块。"