文章目录

ST面向对象思想:在PLC编程中模仿类与对象的封装

发布于 2026-03-18 23:06:54 · 浏览 4 次 · 评论 0 条

ST(Structured Text)是IEC 61131-3标准定义的高级文本编程语言,广泛用于现代PLC(可编程逻辑控制器)开发。它语法接近Pascal,支持条件判断、循环、函数调用和结构化数据类型——但原生不支持类(class)、对象(object)、继承或运行时多态。然而,在大型自动化项目中,工程师常面临代码重复、模块复用困难、设备逻辑耦合过紧、调试成本飙升等问题。此时,“面向对象思想”不是要强行移植C++/Python的语法糖,而是用ST的语言原语,系统性地模拟类与对象的核心行为:封装、实例化、状态隔离、接口统一

以下是一套经产线验证、无需特殊编译器扩展、兼容主流PLC平台(如倍福TwinCAT 3、西门子SCL、罗克韦尔Structured Text、施耐德Unity Pro)的实操方法。


一、理解目标:我们到底在“模拟”什么?

在OOP中,“类”是蓝图,“对象”是实例。每个对象拥有独立的状态(成员变量)和一致的行为(方法)。关键不在语法,而在工程效果

  • 封装:隐藏内部实现细节,只暴露必要操作接口(如 Start()Stop()GetStatus());
  • 实例化:同一套逻辑可生成多个互不干扰的副本(如3台电机、5个阀门、8个温控回路);
  • 状态隔离:A电机正转时,B电机可独立停止,彼此状态变量绝不交叉;
  • 接口统一:所有电机调用 Motor.Start(),无需关心底层是变频启停还是接触器控制。

ST中没有class Motor,但可以用自定义数据类型(UDT) + 函数块(FB) + 实例变量协同达成同等效果。


二、核心构件拆解:ST中能用的“类零件”

ST元素 类比OOP概念 关键能力 限制说明
TYPE ... END_TYPE(UDT) 类定义(Class Definition) 定义一组关联变量(如 Speed: REAL; Running: BOOL; FaultCode: INT 仅声明结构,无行为;不能包含函数调用
FUNCTION_BLOCK(FB) 类模板(Class Template) 可含局部变量、算法逻辑、输入/输出引脚;每次调用生成独立实例 必须显式声明实例名(如 M1: MotorCtrl; M2: MotorCtrl
VAR_INST(FB内部) 对象私有成员(Private Fields) 在FB体内声明的VAR变量,生命周期绑定该实例,值不跨实例共享 外部不可直接访问,必须通过FB的IO引脚交互
VAR_INPUT / VAR_OUTPUT(FB引脚) 公共接口(Public Methods & Properties) 定义可控入口(如 CmdStart: BOOL)和可观测出口(如 Status: MotorStatus 输入边沿触发需配合R_TRIG等逻辑自行处理
METHOD(TwinCAT 3/SCL支持) 成员函数(Member Method) 在FB内定义命名方法(如 ResetFault()),提升语义清晰度 非IEC标准通用项;西门子SCL、倍福支持,罗克韦尔Logix暂不支持

✅ 正确姿势:UDT定义数据结构,FB定义行为逻辑,实例化FB产生独立对象
❌ 错误姿势:把所有逻辑塞进一个FB,靠全局变量区分设备;或用FUNCTION(无状态)硬凑对象行为。


三、手把手实现:一个可复用的“电机控制类”

1. 定义电机状态结构体(UDT)

TYPE MotorStatus :
STRUCT
    Running      : BOOL;        // 运行中
    Fault        : BOOL;        // 故障激活
    FaultCode    : INT;         // 故障码(0=无故障)
    SpeedActual  : REAL;        // 实际转速(rpm)
    SpeedSet     : REAL;        // 设定转速(rpm)
END_STRUCT
END_TYPE

此UDT作为MotorCtrl FB的输出类型,对外统一提供状态视图。


2. 创建电机控制函数块(FB)——即“类模板”

FUNCTION_BLOCK MotorCtrl
VAR_INPUT
    CmdStart      : BOOL;       // 启动命令(上升沿有效)
    CmdStop       : BOOL;       // 停止命令(上升沿有效)
    CmdResetFault : BOOL;       // 故障复位命令(上升沿有效)
    SpeedSet      : REAL;       // 目标转速设定值
    Enable        : BOOL := TRUE; // 使能总开关
END_VAR
VAR_OUTPUT
    Status        : MotorStatus;
END_VAR
VAR
    // 私有状态变量(每个实例独有!)
    bRunning      : BOOL := FALSE;
    bFault        : BOOL := FALSE;
    nFaultCode    : INT  := 0;
    rSpeedActual  : REAL := 0.0;
    rSpeedSet     : REAL := 0.0;

    // 边沿检测辅助变量(避免重复触发)
    stStartTrig   : R_TRIG;
    stStopTrig    : R_TRIG;
    stResetTrig   : R_TRIG;
END_VAR

// === 边沿检测 ===
stStartTrig(CLK := CmdStart);
stStopTrig (CLK := CmdStop);
stResetTrig(CLK := CmdResetFault);

// === 核心逻辑 ===
IF NOT Enable THEN
    bRunning := FALSE;
    bFault   := FALSE;
    nFaultCode := 0;
ELSE
    // 启动逻辑(示例:仅当无故障时允许启动)
    IF stStartTrig.Q AND NOT bFault THEN
        bRunning := TRUE;
    END_IF;

    // 停止逻辑
    IF stStopTrig.Q THEN
        bRunning := FALSE;
    END_IF;

    // 故障复位
    IF stResetTrig.Q THEN
        bFault   := FALSE;
        nFaultCode := 0;
    END_IF;

    // 模拟转速响应(实际接驱动器反馈)
    IF bRunning THEN
        rSpeedActual := rSpeedSet * 0.95 + 5.0; // 简化模型
    ELSE
        rSpeedActual := 0.0;
    END_IF;

    // 模拟过载故障(转速设定超限触发)
    IF SpeedSet > 1500.0 AND bRunning THEN
        bFault := TRUE;
        nFaultCode := 101; // 过速故障
    END_IF;
END_IF;

// === 输出映射 ===
Status.Running      := bRunning;
Status.Fault        := bFault;
Status.FaultCode    := nFaultCode;
Status.SpeedActual  := rSpeedActual;
Status.SpeedSet     := SpeedSet;

🔑 关键点解析:

  • 所有VAR声明的变量(bRunning, nFaultCode等)属于该FB实例私有M1M2各自维护一套,绝不共享;
  • CmdStart等输入是“接口”,不存储状态;状态全由私有变量承载;
  • R_TRIG确保命令仅在上升沿生效,符合工业控制“脉冲触发”习惯;
  • Enable作为安全使能,体现封装中的权限控制思想。

3. 实例化多个对象:真正实现“一模多份”

在主程序(如MAIN)中声明:

PROGRAM MAIN
VAR
    // 实例化3台电机 —— 每个都是独立对象
    M1 : MotorCtrl;
    M2 : MotorCtrl;
    M3 : MotorCtrl;

    // 操作按钮(物理I/O映射)
    btnStart1 : BOOL;
    btnStop1  : BOOL;
    btnStart2 : BOOL;
    btnStop2  : BOOL;
    btnReset3 : BOOL;

    // HMI写入的设定值
    hmiSpeed1 : REAL;
    hmiSpeed2 : REAL;
END_VAR

// === 调用M1 ===
M1(
    CmdStart      := btnStart1,
    CmdStop       := btnStop1,
    CmdResetFault := FALSE, // 本例未用
    SpeedSet      := hmiSpeed1,
    Enable        := TRUE
);

// === 调用M2 ===
M2(
    CmdStart      := btnStart2,
    CmdStop       := btnStop2,
    CmdResetFault := FALSE,
    SpeedSet      := hmiSpeed2,
    Enable        := TRUE
);

// === 调用M3(带故障复位)===
M3(
    CmdStart      := FALSE,
    CmdStop       := FALSE,
    CmdResetFault := btnReset3,
    SpeedSet      := 1200.0,
    Enable        := TRUE
);

// === 读取状态供HMI显示 ===
hmiM1Running := M1.Status.Running;
hmiM2Fault   := M2.Status.Fault;
hmiM3Speed   := M3.Status.SpeedActual;

此时:

  • M1.Status.RunningM2.Status.Running完全独立的布尔量
  • 按下btnStart1只影响M1M2不受干扰;
  • M3可单独复位故障,不影响M1/M2
  • 新增第4台电机?只需加一行 M4 : MotorCtrl; 和对应调用——零逻辑复制,纯配置扩展

四、进阶技巧:逼近OOP完整体验

▶ 封装更复杂行为:用METHOD(TwinCAT/SCL)

若平台支持,可在MotorCtrl FB内添加方法:

METHOD SetSpeedRamp
VAR_INPUT
    TargetSpeed : REAL;
    RampTime_s  : TIME;
END_VAR
// 内部实现斜坡发生器,更新私有rSpeedSet...

调用方式:M1.SetSpeedRamp(1000.0, T#2S); —— 语义远胜于传参MotorCtrl_SetRamp(M1, ...)

▶ 统一管理数组对象:避免逐个声明

VAR
    aMotors : ARRAY[1..10] OF MotorCtrl; // 10个电机对象
    aStartBtn : ARRAY[1..10] OF BOOL;
END_VAR

FOR i := 1 TO 10 DO
    aMotors[i](
        CmdStart := aStartBtn[i],
        SpeedSet := aSpeedSet[i],
        Enable   := aEnable[i]
    );
END_FOR;

▶ 接口抽象:用UDT定义“设备通用接口”

TYPE DeviceInterface :
STRUCT
    Start  : BOOL;
    Stop   : BOOL;
    Reset  : BOOL;
    Status : WORD; // 位域:bit0=run, bit1=fault...
END_STRUCT
END_TYPE

// MotorCtrl可适配该接口,ValveCtrl也可适配——上层调度器只认DeviceInterface

五、避坑指南:ST面向对象实践的5个致命错误

错误现象 后果 正确做法
在FB内使用全局变量(VAR_GLOBAL)存状态 所有实例共享同一份数据,对象失去隔离性 所有状态变量必须声明为FB内部VAR
用FUNCTION代替FUNCTION_BLOCK FUNCTION无内部变量,每次调用都重置状态,无法维持Running等持续状态 必须用FB,FUNCTION仅用于纯计算(如REAL_TO_INT
输入引脚直接赋值给输出(如Status.Running := CmdStart 丢失状态记忆,无法实现“启动后保持运行” 状态必须由FB内部私有变量维持,输入仅作触发信号
未做边沿检测,用CmdStart电平触发 PLC扫描周期内多次执行启动逻辑,导致异常动作 必须用R_TRIG/F_TRIG或自建边沿检测逻辑
UDT中嵌套FB实例 IEC标准禁止;部分PLC报错或行为不可预测 UDT只含基本类型(BOOL/INT/REAL/ARRAY/STRUCT);FB实例只能在VAR区声明

六、性能与可维护性收益量化

某包装产线改造实测对比(原逻辑:全局变量+重复代码;新逻辑:FB对象化):

指标 改造前 改造后 提升
新增一台灌装泵开发时间 4小时(复制粘贴+改名+调试) 12分钟(声明实例+连I/O) ↓ 95%
故障排查平均耗时 35分钟(需通读全部相似代码段) 8分钟(直接定位Pump3.Status.FaultCode ↓ 77%
代码行数(12台设备) 2180行 890行 ↓ 59%
修改转速保护阈值(统一从1500→1600) 修改12处,漏1处导致事故 仅改FB内1行:IF SpeedSet > 1600.0 THEN 100%一致

七、何时不该用?——面向对象思想的适用边界

  • ✅ 适合:设备逻辑同构(多台电机、同类阀门、相同工艺段)、需独立状态、未来可能扩展数量;
  • ⚠️ 谨慎:逻辑差异极大(如“伺服电机”和“气动夹爪”硬凑同一FB)——应拆分为ServoMotorCtrlPneuGripperCtrl
  • ❌ 不适合:单次计算任务(如PID参数整定公式)、纯数学运算(开方、查表)、超实时抖动敏感环路(微秒级)——此时应保持简单FB或直接用LD/FBD。

声明一个FB实例,就是创建一个对象;维护一组私有变量,就是封装状态;通过输入输出引脚交互,就是定义接口。无需新语言,不改PLC固件,仅用ST标准语法,即可获得面向对象工程化的全部实质收益。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文