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.
What OOP brings to Structured Text
For decades, PLC programming meant flat function blocks and global variables. Then platforms such as CODESYS 3.5 and Beckhoff TwinCAT 3 added object-oriented programming (OOP) to Structured Text, and automation engineers got methods, interfaces, inheritance and polymorphism from software engineering.
OOP in a PLC solves practical 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 the existing logic? The features below are the tools for both.
The listings below are vendor syntax in the CODESYS and TwinCAT style (METHOD, PROPERTY, INTERFACE, IMPLEMENTS, EXTENDS), shown for reading rather than for pasting into the site's editor.
A mental model: the vending machine
Before the syntax, picture a vending machine. The INTERFACE is the buttons and the coin slot, the part you interact with. The FUNCTION_BLOCK is the mechanism inside that does the work. Each METHOD is one action, such as InsertCoin(), SelectProduct() or Dispense(), and each PROPERTY is a status you can read, such as CurrentBalance or IsEmpty. INHERITANCE is a snack machine and a drink machine that share the same coin handling but dispense differently.
The customer only ever sees the interface. The mechanism inside a snack machine can be completely different from the one in a drink machine, yet you use both the same way, and that is polymorphism.
Methods: functions inside a function block
A method is a function that belongs to a function block, and it can access all of 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_VAR
IF 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_VAR
IF 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=Error
IF _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 and SET
From the outside a property looks like a variable, but reading or writing it runs code. That removes the need for separate getter and 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 : REAL
GET
ActualTemp := _actualTemp;
END_GET
// No SET = read-only property
// Usage — looks like a simple variable access
PROGRAM Main
VAR
zone1 : FB_TemperatureZone;
END_VAR
zone1.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 saying how it does it. It works as a contract: any FB that implements the interface must provide its 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
Two implementations of one interface
// Implementation 1: Simple DOL motor
FUNCTION_BLOCK FB_Motor_DOL IMPLEMENTS I_Driveable
VAR
_contactor : BOOL;
_overload : BOOL;
_running : BOOL;
END_VAR
METHOD 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_VAR
METHOD 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: one function for every drive
Now you can 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_VAR
IF 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);
When you add a new motor type (servo, stepper, pneumatic), you implement I_Driveable for it and leave FC_StartConveyor untouched. Software engineers call this the Open/Closed Principle: open for extension, closed for modification.
Inheritance: extending a function block
Inheritance lets you build a specialised version of an existing function block:
// 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_VAR
METHOD 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_VAR
shutoffValve.Open(); // Simple open
controlValve.SetPosition(position := 65.0); // Modulated to 65%
controlValve.Open(); // Opens to 100%
The strategy pattern through an interface
The strategy pattern lets you swap algorithms at runtime. A filling machine that supports different dosing methods is a good example:
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_VAR
METHOD 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 to leave OOP out
OOP adds complexity, so use it only where it pays you back:
| 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 |
Syntax at a glance
| 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 |