OOP
PLC Object-Oriented Programming: Methods, Interfaces & Inheritance in Structured Text
Modern PLCs support OOP — and it changes everything. Learn how methods, interfaces, inheritance, and polymorphism let you build automation libraries that scale across machines and projects.
The OOP Revolution in PLC Programming
For decades, PLC programming meant flat function blocks and global variables. Then platforms like CODESYS 3.5, Beckhoff TwinCAT 3, and Siemens TIA Portal v16+ introduced Object-Oriented Programming to Structured Text. Suddenly, concepts from software engineering — methods, interfaces, inheritance, polymorphism — became available to automation engineers.
This isn't academic theory. OOP in PLCs solves real problems: how do you write a motor control library that handles DOL starters, VFDs, and servo drives with the same calling code? How do you add a new sensor type without modifying existing logic? OOP gives you the tools.
🧩 Mental Model: The Vending Machine
Before diving into syntax, think about a vending machine:
The key insight: the user only sees the interface. The internal mechanism can be completely different between a snack machine and a drink machine, but you interact with both the same way. That's polymorphism.
Methods: Functions Inside Function Blocks
A method is a function that belongs to a function block. It can access all the FB's internal variables:
FUNCTION_BLOCK FB_Actuator
VAR
_position : REAL := 0.0;
_speed : REAL := 0.0;
_enabled : BOOL := FALSE;
_errorCode : DINT := 0;
_targetPos : REAL := 0.0;
END_VAR
// METHOD: Enable — turns on the actuator
METHOD Enable : BOOL
VAR_INPUT
safetyOK : BOOL;
END_VARIF NOT safetyOK THEN
_errorCode := 1001;
Enable := FALSE;
RETURN;
END_IF;
_enabled := TRUE;
_errorCode := 0;
Enable := TRUE; // Return success
// METHOD: MoveTo — commands a position
METHOD MoveTo : BOOL
VAR_INPUT
targetPosition : REAL;
moveSpeed : REAL;
END_VARIF NOT _enabled THEN
MoveTo := FALSE;
RETURN;
END_IF;
IF targetPosition < 0.0 OR targetPosition > 1000.0 THEN
_errorCode := 1002; // Out of range
MoveTo := FALSE;
RETURN;
END_IF;
_targetPos := targetPosition;
_speed := moveSpeed;
MoveTo := TRUE;
// METHOD: GetStatus — returns current state
METHOD GetStatus : INT
// 0=Disabled, 1=Idle, 2=Moving, 3=AtPosition, 4=ErrorIF _errorCode <> 0 THEN
GetStatus := 4;
ELSIF NOT _enabled THEN
GetStatus := 0;
ELSIF ABS(_position - _targetPos) < 0.1 THEN
GetStatus := 3;
ELSIF _speed > 0.0 THEN
GetStatus := 2;
ELSE
GetStatus := 1;
END_IF;
Calling Methods
PROGRAM Main
VAR
gripper : FB_Actuator;
moveOK : BOOL;
status : INT;
END_VAR// Enable with safety check
IF gripper.Enable(safetyOK := SafetyCircuitOK) THEN
// Move to pick position
moveOK := gripper.MoveTo(targetPosition := 150.0, moveSpeed := 200.0);
END_IF;
// Check status
status := gripper.GetStatus();
🔧 Properties: Controlled Access with GET/SET
Properties look like variables from the outside but execute code internally. They replace the need for getter/setter methods:
FUNCTION_BLOCK FB_TemperatureZone
VAR
_setpoint : REAL := 20.0;
_actualTemp : REAL;
_minLimit : REAL := -10.0;
_maxLimit : REAL := 300.0;
_spChanged : BOOL;
END_VAR
// PROPERTY: Setpoint (read/write with validation)
PROPERTY Setpoint : REAL// GET accessor — called when reading the property
GET
Setpoint := _setpoint;
END_GET
// SET accessor — called when writing the property
SET
// Validate before accepting
IF Setpoint >= _minLimit AND Setpoint <= _maxLimit THEN
_setpoint := Setpoint;
_spChanged := TRUE;
END_IF;
// Invalid values are silently rejected — the setpoint doesn't change
END_SET
// PROPERTY: ActualTemp (read-only — no SET accessor)
PROPERTY ActualTemp : REALGET
ActualTemp := _actualTemp;
END_GET
// No SET = read-only property
// Usage — looks like a simple variable access
PROGRAM Main
VAR
zone1 : FB_TemperatureZone;
END_VARzone1.Setpoint := 85.0; // Calls SET — validates internally
currentTemp := zone1.ActualTemp; // Calls GET — read-only access
zone1.Setpoint := 999.0; // Rejected silently — stays at 85.0
🏗️ Interfaces: The Contract
An INTERFACE defines what an object can do, without specifying how. It's a contract: "Any FB that implements this interface must provide these methods and properties."
INTERFACE I_Driveable
// Any FB implementing this must provide:
METHOD Enable : BOOL
VAR_INPUT
safetyOK : BOOL;
END_VAR
END_METHOD METHOD Disable : BOOL
END_METHOD
METHOD SetSpeed : BOOL
VAR_INPUT
speedPercent : REAL;
END_VAR
END_METHOD
PROPERTY IsRunning : BOOL // GET only
PROPERTY ActualSpeed : REAL // GET only
PROPERTY HasError : BOOL // GET only
END_INTERFACE
The Power: Different Implementations, Same Interface
// Implementation 1: Simple DOL motor
FUNCTION_BLOCK FB_Motor_DOL IMPLEMENTS I_Driveable
VAR
_contactor : BOOL;
_overload : BOOL;
_running : BOOL;
END_VARMETHOD Enable : BOOL
VAR_INPUT safetyOK : BOOL; END_VAR
IF safetyOK AND NOT _overload THEN
_contactor := TRUE;
_running := TRUE;
Enable := TRUE;
END_IF;
END_METHOD
METHOD SetSpeed : BOOL
VAR_INPUT speedPercent : REAL; END_VAR
// DOL motor is either on or off — ignore speed
SetSpeed := _running;
END_METHOD
PROPERTY IsRunning : BOOL
GET IsRunning := _running; END_GET
// Implementation 2: VFD-controlled motor
FUNCTION_BLOCK FB_Motor_VFD IMPLEMENTS I_Driveable
VAR
_vfdRunCmd : BOOL;
_vfdSpeedRef : REAL;
_vfdFeedback : REAL;
_vfdFaulted : BOOL;
END_VARMETHOD Enable : BOOL
VAR_INPUT safetyOK : BOOL; END_VAR
IF safetyOK AND NOT _vfdFaulted THEN
_vfdRunCmd := TRUE;
Enable := TRUE;
END_IF;
END_METHOD
METHOD SetSpeed : BOOL
VAR_INPUT speedPercent : REAL; END_VAR
IF speedPercent >= 0.0 AND speedPercent <= 100.0 THEN
_vfdSpeedRef := speedPercent;
SetSpeed := TRUE;
ELSE
SetSpeed := FALSE;
END_IF;
END_METHOD
PROPERTY IsRunning : BOOL
GET IsRunning := _vfdRunCmd AND NOT _vfdFaulted; END_GET
PROPERTY ActualSpeed : REAL
GET ActualSpeed := _vfdFeedback; END_GET
🎯 Polymorphism in Action
Now the magic — write code that works with any driveable device:
// This function doesn't know or care if it's a DOL, VFD, or servo
FUNCTION FC_StartConveyor : BOOL
VAR_INPUT
drive : I_Driveable; // Accept ANY implementation
targetSpeed: REAL;
END_VARIF drive.HasError THEN
FC_StartConveyor := FALSE;
RETURN;
END_IF;
IF drive.Enable(safetyOK := TRUE) THEN
FC_StartConveyor := drive.SetSpeed(speedPercent := targetSpeed);
END_IF;
// Usage — same function, different hardware
PROGRAM Main
VAR
conveyorA : FB_Motor_DOL; // Simple starter
conveyorB : FB_Motor_VFD; // Variable speed
result : BOOL;
END_VAR// Same function call for completely different hardware!
result := FC_StartConveyor(drive := conveyorA, targetSpeed := 100.0);
result := FC_StartConveyor(drive := conveyorB, targetSpeed := 75.0);
Add a new motor type (servo, stepper, pneumatic) and you never touch FC_StartConveyor. Just implement I_Driveable. This is the Open/Closed Principle: open for extension, closed for modification.
🧬 Inheritance: Extending Behavior
Inheritance lets you create specialized versions of existing function blocks:
// Base class: generic valve
FUNCTION_BLOCK FB_Valve_Base
VAR
_isOpen : BOOL := FALSE;
_isClosed : BOOL := TRUE;
_faulted : BOOL := FALSE;
_openCmd : BOOL := FALSE;
_closeCmd : BOOL := FALSE;
END_VARMETHOD Open : BOOL
IF NOT _faulted THEN
_openCmd := TRUE;
_closeCmd := FALSE;
Open := TRUE;
END_IF;
END_METHOD
METHOD Close : BOOL
_closeCmd := TRUE;
_openCmd := FALSE;
Close := TRUE;
END_METHOD
// Derived class: adds modulating control
FUNCTION_BLOCK FB_Valve_Modulating EXTENDS FB_Valve_Base
VAR
_positionSP : REAL := 0.0;
_positionPV : REAL := 0.0;
_deadband : REAL := 1.0;
END_VAR// NEW method — only on modulating valves
METHOD SetPosition : BOOL
VAR_INPUT
position : REAL; // 0–100%
END_VAR
IF position >= 0.0 AND position <= 100.0 AND NOT _faulted THEN
_positionSP := position;
SetPosition := TRUE;
END_IF;
END_METHOD
// OVERRIDE: Open now means "go to 100%"
METHOD Open : BOOL
_positionSP := 100.0;
_openCmd := TRUE;
_closeCmd := FALSE;
Open := NOT _faulted;
END_METHOD
// Usage
VAR
shutoffValve : FB_Valve_Base; // On/off only
controlValve : FB_Valve_Modulating; // Position control
END_VARshutoffValve.Open(); // Simple open
controlValve.SetPosition(position := 65.0); // Modulated to 65%
controlValve.Open(); // Opens to 100%
📐 Design Pattern: Strategy via Interface
The Strategy Pattern lets you swap algorithms at runtime. Real-world example — a filling machine that supports different dosing methods:
INTERFACE I_DosingStrategy
METHOD CalculateDose : REAL
VAR_INPUT
targetWeight : REAL;
currentWeight: REAL;
flowRate : REAL;
END_VAR
END_METHOD PROPERTY Name : STRING // Which strategy is active
END_INTERFACE
// Strategy 1: Simple cutoff
FUNCTION_BLOCK FB_Dosing_SimpleCutoff IMPLEMENTS I_DosingStrategy
METHOD CalculateDose : REAL
VAR_INPUT
targetWeight : REAL;
currentWeight: REAL;
flowRate : REAL;
END_VAR
IF currentWeight >= targetWeight THEN
CalculateDose := 0.0; // Stop filling
ELSE
CalculateDose := 100.0; // Full speed until target
END_IF;
END_METHOD
PROPERTY Name : STRING
GET Name := 'Simple Cutoff'; END_GET
// Strategy 2: Predictive with preact
FUNCTION_BLOCK FB_Dosing_Predictive IMPLEMENTS I_DosingStrategy
VAR
_preactWeight : REAL;
END_VAR
METHOD CalculateDose : REAL
VAR_INPUT
targetWeight : REAL;
currentWeight: REAL;
flowRate : REAL;
END_VAR
// Calculate preact point: material-in-flight compensation
_preactWeight := flowRate * 0.3; // 300ms valve close time
IF currentWeight >= (targetWeight - 0.5) THEN
CalculateDose := 0.0; // Final cutoff
ELSIF currentWeight >= (targetWeight - _preactWeight) THEN
CalculateDose := 20.0; // Dribble feed
ELSE
CalculateDose := 100.0; // Fast fill
END_IF;
END_METHOD
PROPERTY Name : STRING
GET Name := 'Predictive Preact'; END_GET
// The filler uses whichever strategy is assigned
FUNCTION_BLOCK FB_FillingMachine
VAR
_strategy : I_DosingStrategy; // Interface reference
_valveCmd : REAL;
END_VARMETHOD SetStrategy : BOOL
VAR_INPUT
strategy : I_DosingStrategy;
END_VAR
_strategy := strategy;
SetStrategy := (_strategy <> 0); // Non-null check
END_METHOD
METHOD Execute : REAL
VAR_INPUT
targetWt : REAL;
currentWt: REAL;
flowRate : REAL;
END_VAR
IF _strategy <> 0 THEN
_valveCmd := _strategy.CalculateDose(
targetWeight := targetWt,
currentWeight := currentWt,
flowRate := flowRate
);
END_IF;
Execute := _valveCmd;
END_METHOD
// Runtime strategy swap — change dosing algorithm without modifying the filler
PROGRAM Main
VAR
filler : FB_FillingMachine;
simple : FB_Dosing_SimpleCutoff;
predictive: FB_Dosing_Predictive;
output : REAL;
END_VAR// Use simple mode for rough fills
filler.SetStrategy(strategy := simple);
output := filler.Execute(targetWt:=50.0, currentWt:=PV_Weight, flowRate:=PV_Flow);
// Switch to predictive for precision fills — no code change in the filler!
filler.SetStrategy(strategy := predictive);
output := filler.Execute(targetWt:=50.0, currentWt:=PV_Weight, flowRate:=PV_Flow);
⚠️ When NOT to Use OOP
OOP adds complexity. Use it when you get real benefit:
| Use OOP When | Don't Use OOP When | | Multiple implementations of same concept | Simple, one-off logic | | Building reusable libraries | Small projects (< 500 I/O) | | Team of 3+ programmers | Solo developer, single machine | | Multiple machine variants | Logic will never change | | Swappable algorithms needed | Fixed calculation |
The test: If you can't name at least two future implementations of your interface, you probably don't need an interface.
Summary
| Concept | Purpose | Syntax | | Method | Function inside an FB | METHOD name : ReturnType | | Property | Controlled variable access | PROPERTY name : Type / GET / SET | | Interface | Contract for capabilities | INTERFACE I_Name / IMPLEMENTS | | Inheritance | Extend existing FB | EXTENDS FB_Base | | Polymorphism | Same code, different behavior | Pass I_Interface as parameter | | Strategy Pattern | Swappable algorithms | Interface + multiple implementations |
OOP in PLCs isn't about making code look like Java. It's about building automation libraries that survive machine variants, product changes, and team turnover. Start with interfaces for your equipment types, and the rest follows naturally.