Object-Oriented Programming in STone
A Complete Guide
Object-Oriented Programming in STone
A Complete Guide
This article is a collection of rules and conventions for object-oriented programming in Structured Text (ST) within the STone IDE. All the patterns and practices described here are based on my years of experience with production-grade industrial libraries. As a running example I chose an industrial pump station control library — a system managing various pump types (VFD, DOL, soft-start) with pressure, flow, and level regulation. The principles, however, are universally applicable.
1. STone Project Structure
1.1. The .stprj project/library file
Every STone project/library has an XML .stprj file that defines metadata and the compilation structure.
<SToneProject
name="PumpStation_Lib_v.2.0.0 [Controller_LIB]"
type="Library"
version="1.2"
projectVersion="2.0.0"
author="Hubert Lepiarczyk"
vendor="IceLAB"
libname="PumpStation_Lib">
<Files>
<Folder path="Libs">...</Folder>
<Folder path="Classes">...</Folder>
<Folder path="Functions">...</Folder>
<Folder path="Interfaces">...</Folder>
</Files>
<Library Ver="1">
<Info Version="2.0.0" ID="..." Name="PumpStation_Lib">
<Dependency ID="..." Name="PumpCore" Version="2.0.3.0" />
<Dependency ID="..." Name="HydraulicCalc" Version="1.0.1" />
</Info>
</Library>
</SToneProject>
1.2. Directory organization
A good practice is to split the code into directories that reflect OOP elements:
| Directory | Contents | Example files |
|---|---|---|
Classes/ | Classes (abstract and final) | BasePump.st, VFD_Pump.st, DOL_Pump.st |
Interfaces/ | Interfaces (contracts) | IPump.st, IVariableSpeedPump.st |
Functions/ | Global functions | ValidateModbusResults.st, CheckControllerID.st |
Libs/ | External dependencies (.stlib) | PumpCore_v.2.0.3.0.stlib |
Convention: each class/interface = a separate .st file whose name matches the class/interface name.
1.3. Library versioning — LibVer.g.st
STone uses a preprocessor mechanism to manage versions and namespaces:
{IF NOT DEF(__LIBRARYNAMESPACEANDVERSION__)}
{DEFINE __LIBRARYNAMESPACEANDVERSION__}
{DEFINE LibraryNamespaceAndVersion Libs.PumpStation_Lib_v2_0_0}
{DEFINE LibraryName 'PumpStation_Lib'}
{DEFINE LibraryVersion 'PumpStation_Lib_v2_0_0'}
{DEFINE LibraryVersionMajor 2}
{DEFINE LibraryVersionMinor 0}
{DEFINE LibraryVersionBuild 0}
{ENDIF}
Key principles:
- The version is part of the namespace (
Libs.PumpStation_Lib_v2_0_0) — this allows multiple library versions to coexist - The include guard (
__LIBRARYNAMESPACEANDVERSION__) prevents multiple definitions - Every
.stfile begins with{INCLUDE '../LibVer.g.st'} - The
LibraryNamespaceAndVersionmacro is used as the NAMESPACE name in every file
2. Namespaces and Preprocessor Directives
2.1. Namespace
Every file in the library is wrapped in a NAMESPACE using the version macro:
{INCLUDE '../LibVer.g.st'}
NAMESPACE LibraryNamespaceAndVersion
USING System;
USING System.IO;
USING System.Math;
USING Libs.PumpCore_v2_0_3_0;
(* All logic goes here: classes, interfaces, functions, types *)
END_NAMESPACE
Conventions:
USINGimports other namespaces — always at the top, right after the NAMESPACE opening- Dependent libraries are imported via their fully versioned path:
Libs.PumpCore_v2_0_3_0 - System types via
System.IO,System.Math
2.2. Conditional compilation {IF DEF ...}
STone supports preprocessor directives for conditional compilation. In practice, I use them extensively to handle hardware variants:
{IF DEF (FEATURE_MODBUS_DRIVES)}
(* VFD control via Modbus RTU *)
USING Libs.PumpCore_v2_0_3_0;
{ELSE}
(* Direct control: 0-10V analog outputs *)
USING Libs.PumpCore_v1_0_0_0;
{ENDIF}
{IF DEF (FEATURE_ADVANCED_MOTORS)}
(* Additional drive types: soft-start, DOL with current monitoring *)
{ENDIF}
Practical applications:
| Define | Effect |
|---|---|
FEATURE_MODBUS_DRIVES | Enables VFD control via Modbus — requires extended communication API |
FEATURE_ADVANCED_MOTORS | Enables soft-start and DOL pumps with extended monitoring |
This way, a single codebase supports multiple hardware variants. Classes can have two independent implementations in the same file, switched at compile-time.
2.3. Regions {REGION ... ENDREGION}
Regions visually organize code sections in the IDE:
{REGION Setters}
METHOD PUBLIC SetDefaultConfig
(* ... *)
END_METHOD
METHOD PUBLIC SetCustomConfig
(* ... *)
END_METHOD
{ENDREGION}
{REGION Getters}
METHOD PUBLIC GetMotorType : MOTOR_TYPE
(* ... *)
END_METHOD
{ENDREGION}
Recommended region conventions:
| Region | Contents |
|---|---|
Setters | Methods that set parameters |
Getters | Methods that return values |
Public / Protected | Sub-regions inside Setters/Getters grouped by visibility |
Default params | Default constants |
NAMED VALUES | Enumeration types with explicit numeric values |
ENUMERATORS | Standard enumerators |
CONFIGURATION | Configuration structures |
INFORMATION | Information/status structures |
Conditional API calls | Blocks of calls guarded by hardware feature-flags |
3. Data Types
3.1. Named Values (enumerators with values)
STone allows defining enumerators with explicitly assigned numeric values. Syntax:
TYPE
(**Type description*)
TYPE_NAME : BASE_TYPE(
(**Value 1 description*)
Value1 := 0,
(**Value 2 description*)
Value2 := 1,
(**Value 3 description*)
Value3 := 2
);
END_TYPE
Example — pump selection:
(**Selection of which pump to control*)
PUMP_SEL : USINT(
(**Control first pump*)
Pump1 := 1,
(**Control second pump*)
Pump2 := 2,
(**Control third pump*)
Pump3 := 3
);
Example — controller state machine:
(**Pump controller state machine*)
PUMP_STATE : USINT(
(**No state*)
None := 0,
(**Motor stopped*)
Stopped := 1,
(**Emergency shutdown active*)
EmergencyStop := 2,
(**Standby — waiting for demand*)
StandBy := 3,
(**Ramp-up in progress*)
RampUp := 4,
(**Pre-start system check*)
SystemCheck := 5,
(**Waiting for conditions*)
WaitConditions := 6,
(**PID regulation active*)
Regulation := 7,
(**Low-flow protection active*)
LowFlowProtection := 8,
(**Dry-run protection active*)
DryRunProtection := 9,
(**Power-on self-test*)
PowerOnTest := 10,
(**Waiting after regulation mode change*)
WaitModeChange := 11,
(**Pump re-start sequence*)
RestartSequence := 12,
(* ... protections ... *)
(**Low inlet pressure protection*)
LowInletPress := 18,
(**High outlet pressure protection*)
HighOutletPress := 19,
(**Overload protection*)
OverloadProtection := 20,
(**Over-temperature protection*)
OverTemperature := 21,
(**External controller not ready*)
ExtControllerNotReady := 100
);
Key features:
- The base type (
USINT,UINT,DINT) determines the memory footprint — PLC resource optimization - Values do not need to be contiguous (e.g.,
PUMP_STATEhas gaps: 12→18, 21→100) - The
INTERNALmodifier restricts visibility:TYPE INTERNAL— the type is inaccessible outside the library - Access via qualifier:
PUMP_STATE#Regulation,PUMP_SEL#Pump1
3.2. Standard enumerators
Without explicit values — the compiler assigns them automatically starting from 0:
TYPE
(**Motor / drive type*)
MOTOR_TYPE: (
(**Variable Frequency Drive*)
VFD,
(**External controller — e.g. standalone inverter*)
ExternalController
{IF DEF (FEATURE_ADVANCED_MOTORS)}
,
(**Direct on-line motor, no speed control*)
DOL_Pump,
(**Soft-start motor controller*)
SoftStart
{ENDIF}
);
END_TYPE
Note the comma before
DOL_Pumpinside the{IF DEF}block — this is necessary to maintain valid syntax regardless of the conditional compilation state.
3.3. Structures (STRUCT)
Structures group related data. In practice, I make extensive use of nested structures:
(**Regulation parameters*)
REG_PARAMS : STRUCT
(**Regulation type: pressure, flow, level...*)
RegTyp : REG_TYPE(...) := REG_TYPE#Pressure;
(**Enable pump in stand-by mode*)
StandByEn : BOOL := FALSE;
(**Pump speed in stand-by*) {ATTRIBUTE UOM PERCENT}
StandBySpeedPerc : UINT(0..100) := 0;
(**Speed at startup*) {ATTRIBUTE UOM PERCENT}
StartSpeedRatio : UINT(0..100) := 50;
(**Delay before regulation starts*) {ATTRIBUTE UOM SECOND}
RegStartDelay : UINT(0..18000) := 6;
(**Protection thresholds*)
Protections : PRESSURE_PROTECTION;
(**PID parameters*)
PID_Params : PID_PARAMS;
END_STRUCT;
Structure conventions:
- Every field has a default value (
:= ...) - Ranges defined explicitly:
UINT(0..100),UINT(0..18000) - Nesting as a form of composition — data hierarchy
3.4. Typed arrays
(**Polynomial coefficients for pressure sensor linearization*)
CFG_SENSOR_POLY: ARRAY[1..6] OF REAL := [
0.00124,
-0.03782,
1.24560,
-0.00415,
0.09832,
-0.00067
];
3.5. Field attributes and metadata
STone supports attributes attached to fields and methods:
(**Motor rated current*) {ATTRIBUTE UOM AMPERE}
RatedCurrent : UINT := 12;
(**High outlet pressure threshold*) {ATTRIBUTE UOM BAR} {METADATA MIN_VAL 0} {METADATA MAX_VAL 40}
HighPressThreshold : REAL := 10;
(**Motor impedance*) {ATTRIBUTE UOM OHM}
MotorImpedance : UINT := 40;
| Attribute | Meaning |
|---|---|
{ATTRIBUTE UOM ...} | Unit of measurement (CELSIUS, BAR, PERCENT, AMPERE, OHM, RPM, SECOND, MINUTE, HERTZ, CUBICMETERPERHOUR) |
{METADATA MIN_VAL ...} | Minimum parameter value |
{METADATA MAX_VAL ...} | Maximum parameter value |
These attributes can be read by IDE tools (IntelliSense) and supervisory systems.
3.6. Global constants VAR_GLOBAL CONSTANT
VAR_GLOBAL CONSTANT INTERNAL
TARGET_CONTROLLER_ID : UINT := 17;
{REGION Default motor params}
(**Default motor params*)
DEFAULT_MAX_SPEED : UINT := 1450;
DEFAULT_MIN_SPEED : UINT := 300;
DEFAULT_RAMP_UP_TIME : UINT := 10;
DEFAULT_RAMP_DOWN_TIME : UINT := 8;
DEFAULT_RATED_CURRENT : UINT := 12;
DEFAULT_OVERLOAD_CURRENT : UINT := 15;
DEFAULT_RATED_FREQUENCY : UINT := 50;
DEFAULT_MIN_FLOW_SPEED : UINT := 600;
DEFAULT_STARTUP_DELAY : UINT := 30;
DEFAULT_DRY_RUN_DELAY : UINT := 5;
{ENDREGION}
END_VAR
Conventions:
INTERNAL— constants visible only within the library- Names:
UPPER_SNAKE_CASEwith a thematic prefix (e.g.,DEFAULT_) - Default values serve as the single source of truth for factory parameters
4. Interfaces (INTERFACE)
4.1. Interface definition
An interface defines a contract — a set of methods that a class must implement:
(**Common interface defining the contract between the regulation logic
and pump control implementations.
*Exposes lifecycle control, configuration setters and status getters
*that are implemented by concrete pump classes such as VFD, DOL or soft-start*)
INTERFACE IPump
(**Executes one control cycle of the regulation logic*)
METHOD Run
END_METHOD
(**Resets the internal control state and forces a clean restart*)
METHOD Reset
END_METHOD
{REGION Setters}
(**Sets whether battery-backed emergency stop is enabled*)
METHOD SetEmergencyStop
VAR_INPUT
En_Backup : BOOL;
END_VAR
END_METHOD
(**Sets regulation parameters*)
METHOD SetRegParams
VAR_IN_OUT CONSTANT
RegParams : REG_PARAMS;
END_VAR
END_METHOD
(**Sets sensor readings*)
METHOD SetSensors
VAR_INPUT
{ATTRIBUTE UOM BAR}
(**Inlet pressure*)
PS1 : REAL;
{ATTRIBUTE UOM CELSIUS}
(**Motor temperature*)
TS1 : REAL;
{ATTRIBUTE UOM BAR}
(**Outlet pressure*)
PS2 : REAL;
{ATTRIBUTE UOM CUBICMETERPERHOUR}
(**Flow rate*)
FS1 : REAL;
END_VAR
VAR_IN_OUT CONSTANT
(**Sensor linearization coefficients*)
SensorCoeff : CFG_SENSOR_POLY;
END_VAR
END_METHOD
{ENDREGION}
{REGION Getters}
(**Returns TRUE if the pump is ready to accept a command*)
METHOD GetReadyToCommand : BOOL
END_METHOD
{ATTRIBUTE UOM BAR}
(**Returns the current outlet pressure*)
METHOD GetOutletPressure : REAL
END_METHOD
(**Returns the current controller state*)
METHOD GetState : PUMP_STATE
END_METHOD
(**Returns the alarm status structure*)
METHOD GetAlarmStatus : ALARM_STATUS
END_METHOD
{ENDREGION}
END_INTERFACE
4.2. Interface inheritance (EXTENDS)
Interfaces can extend other interfaces:
(**Specialized interface extending IPump for variable-speed implementations.
*Adds the contract required to select the output channel
*and to expose speed data meaningful only for VFD-driven motors*)
INTERFACE IVariableSpeedPump EXTENDS IPump
{REGION Setters}
(**Sets which output channel the implementation should address*)
METHOD SetPumpChannel
VAR_INPUT
PumpSel : PUMP_SEL;
END_VAR
END_METHOD
(**Sets the default factory configuration*)
METHOD SetDefaultConfig
END_METHOD
{ENDREGION}
{REGION Getters}
(**Returns the current motor speed*) {ATTRIBUTE UOM RPM}
METHOD GetMotorSpeed : UINT
END_METHOD
(**Returns the current speed as a percentage of rated speed*)
{ATTRIBUTE UOM PERCENT}
METHOD GetSpeedPercent : REAL
END_METHOD
(**Returns the complete motor data structure*)
METHOD GetMotorData : T_MOTOR_DATA
END_METHOD
{ENDREGION}
END_INTERFACE
Interface conventions:
| Rule | Example |
|---|---|
I prefix in the name | IPump, IVariableSpeedPump |
| No method bodies | Signatures only |
| Grouping into regions | {REGION Setters}, {REGION Getters} |
| Attributes on methods | {ATTRIBUTE UOM BAR} before METHOD |
VAR_IN_OUT CONSTANT for structures | Pass by reference without copy, read-only |
4.3. The VAR_IN_OUT CONSTANT pattern
A key PLC memory optimization pattern:
METHOD SetRegParams
VAR_IN_OUT CONSTANT
RegParams : REG_PARAMS;
END_VAR
END_METHOD
VAR_IN_OUT— pass by reference (no copying of large structures)CONSTANT— prevents modification inside the method- Ideal for passing large configuration structures to setters
5. Classes (CLASS)
5.1. Class hierarchy — pump station example
IPump (interface)
├── IVariableSpeedPump (interface, extends IPump)
│
├── BasePump (abstract, implements IPump)
│ ├── BaseVariableSpeedPump (abstract, extends BasePump, implements IVariableSpeedPump)
│ │ ├── VFD_Pump (final) — analog output control
│ │ ├── VFD_PumpModbus (final) — Modbus RTU control
│ │ └── SoftStartPump (final)
│ ├── ExternalController (final) — standalone inverter interface
│ └── DOL_Pump (final) — direct on-line, no speed control
5.2. Abstract class — BasePump
CLASS ABSTRACT INTERNAL BasePump IMPLEMENTS IPump
VAR PRIVATE
CoreInit : CORE_ControllerInit;
CoreController : CORE_Controller;
CoreEmergency : CORE_EmergencyMng;
RefRegParams : REF_TO REG_PARAMS;
RefAdvRegParams : REF_TO ADV_REG_PARAMS;
Demand : UINT;
PS1, TS1, PS2, FS1 : REAL;
PressureSetpoint : REAL;
ReadyToCmd : BOOL;
END_VAR
VAR PROTECTED
ControllerParams : CONTROLLER_DATA_PARAMS;
ControllerVars : CONTROLLER_DATA_VARS;
Motor : T_MOTOR_DATA;
AlarmStatus : ALARM_STATUS;
IsConfigChanged : BOOL;
END_VAR
(*Extension point for derived classes*)
METHOD ABSTRACT PROTECTED OnRun
END_METHOD
(**Resets internal logic, disables motor, clears alarms*)
METHOD PUBLIC Reset
VAR
EmptyAlarmStatus : ALARM_STATUS;
END_VAR
THIS.Motor.Enable := 0;
THIS.SetCanGo(FALSE);
THIS.CoreProcessingPhase();
THIS.UpdateReadyToCommand();
THIS.UpdateAlarmStatus();
(* ... clear regulation variables ... *)
THIS.AlarmStatus := EmptyAlarmStatus;
THIS.IsConfigChanged := FALSE;
END_METHOD
(**Executes a full regulation cycle*)
METHOD PUBLIC Run
THIS.ApplyConfiguration();
IF THIS.IsConfigChanged THEN
THIS.Reset();
RETURN;
END_IF;
THIS.UpdateDemand();
THIS.UpdateSensorsAndParams();
THIS.CoreProcessingPhase();
THIS.UpdateReadyToCommand();
THIS.OnRun(); (*Abstract — implemented by subclass*)
THIS.UpdateAlarmStatus();
END_METHOD
(* ... CoreProcessingPhase, WriteMotorData, ReadMotorData,
UpdateDemand, UpdateReadyToCommand, UpdateAlarmStatus,
UpdateSensorsAndParams — omitted for brevity ... *)
{REGION Setters}
METHOD PUBLIC SetRegParams
VAR_IN_OUT CONSTANT
RegParams : REG_PARAMS;
END_VAR
THIS.RefRegParams := REF(RegParams);
THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);
THIS.SetPID_Params();
THIS.SetEngineParams();
END_METHOD
METHOD PUBLIC SetAdvRegParams
VAR_IN_OUT CONSTANT
AdvRegParams : ADV_REG_PARAMS;
END_VAR
THIS.RefAdvRegParams := REF(AdvRegParams);
END_METHOD
(**Overloaded SetDemand — UINT version delegates to USINT version*)
METHOD PUBLIC SetDemand
VAR_INPUT
Demand : UINT(0..100);
END_VAR
THIS.SetDemand(TO_USINT(Demand));
END_METHOD
METHOD PUBLIC SetDemand
VAR_INPUT
Demand : USINT(0..100);
END_VAR
THIS.Demand := TO_UINT(Demand);
END_METHOD
{ENDREGION}
{REGION Getters}
METHOD PUBLIC GetReadyToCommand : BOOL
GetReadyToCommand := THIS.ReadyToCmd;
END_METHOD
METHOD PUBLIC GetAlarmCode : ALARM_CODE
IF THIS.AlarmStatus.PowerFailure THEN
GetAlarmCode := ALARM_CODE#PowerFailure;
ELSIF THIS.AlarmStatus.DryRun THEN
GetAlarmCode := ALARM_CODE#DryRun;
ELSIF THIS.AlarmStatus.Overload THEN
GetAlarmCode := ALARM_CODE#Overload;
(* ... remaining priority checks ... *)
ELSE
GetAlarmCode := ALARM_CODE#None;
END_IF;
END_METHOD
{ENDREGION}
METHOD PROTECTED ApplyConfiguration
THIS.ControllerVars.Engine_vars.Motor_vars.Enable := 1;
END_METHOD
METHOD PROTECTED SetCanGo
VAR_INPUT
Enable : BOOL;
END_VAR
THIS.ControllerParams.CanGo := Enable;
END_METHOD
END_CLASS
Key patterns:
ABSTRACT— cannot be instantiated directlyINTERNAL— class visible only within the libraryIMPLEMENTS IPump— interface contract implementationMETHOD ABSTRACT PROTECTED OnRun— abstract method, extension point (Template Method Pattern)THIS.— explicit reference to instance members
5.3. Variable visibility modifiers
VAR PRIVATE
(*Accessible ONLY within this class*)
CoreInit : CORE_ControllerInit;
RefRegParams : REF_TO REG_PARAMS;
END_VAR
VAR PROTECTED
(*Accessible in this class and its subclasses*)
Motor : T_MOTOR_DATA;
AlarmStatus : ALARM_STATUS;
END_VAR
| Modifier | Visibility scope |
|---|---|
PRIVATE | Only within the given class |
PROTECTED | Within the class and its subclasses |
PUBLIC | Everywhere (default for interface methods) |
INTERNAL | Within the library |
Convention: FB instance variables and references → PRIVATE; structures shared with subclasses → PROTECTED.
5.4. Intermediate class — BaseVariableSpeedPump
CLASS INTERNAL ABSTRACT BaseVariableSpeedPump EXTENDS BasePump IMPLEMENTS IVariableSpeedPump
VAR PROTECTED
(**Identifies which output channel the controller should address*)
PumpSel : PUMP_SEL;
(**Stores the current motor configuration variant*)
MotorCfgVar : MOTOR_CFG_VARIANT;
END_VAR
(**Resets the complete internal control logic and clears estimated speed*)
METHOD OVERRIDE PUBLIC Reset
SUPER.Reset();
THIS.Motor.CurrentSpeed := 0;
END_METHOD
{REGION Setters}
(**Sets which output channel the controller should address*)
METHOD PUBLIC SetPumpChannel
VAR_INPUT
PumpSel : PUMP_SEL;
END_VAR
THIS.PumpSel := PumpSel;
END_METHOD
{ENDREGION}
{REGION Getters}
(**Returns the current motor speed*) {ATTRIBUTE UOM RPM}
METHOD PUBLIC GetMotorSpeed : UINT
GetMotorSpeed := MIN(THIS.Motor.MaxSpeed, THIS.Motor.CurrentSpeed);
END_METHOD
{ATTRIBUTE UOM PERCENT}
(**Returns the current speed as a percentage of max speed*)
METHOD PUBLIC GetSpeedPercent : REAL
IF THIS.Motor.MaxSpeed <> 0 THEN
GetSpeedPercent := MIN(100,
TO_REAL(THIS.Motor.CurrentSpeed) * 100
/ TO_REAL(THIS.Motor.MaxSpeed));
END_IF;
END_METHOD
(**Returns the complete motor data structure*)
METHOD PUBLIC GetMotorData : T_MOTOR_DATA
GetMotorData := THIS.Motor;
END_METHOD
{ENDREGION}
END_CLASS
Patterns:
EXTENDS BasePump IMPLEMENTS IVariableSpeedPump— single class inheritance + interface implementationOVERRIDE— overrides a base class methodSUPER.Reset()— calls the parent implementation
5.5. Final class — VFD_Pump
CLASS FINAL PUBLIC VFD_Pump EXTENDS BaseVariableSpeedPump
VAR PRIVATE
(**Manages VFD communication and speed commands*)
VFD_Driver : System.IO.ANALOG_OUT_DRV;
(**Internal copy of VFD configuration used to detect parameter changes*)
InternalCfg : CFG_VFD_MOTOR;
END_VAR
(**Sends speed command to the VFD analog output*)
METHOD PRIVATE Driver
THIS.VFD_Driver(
ChannelNum := TO_BYTE(THIS.PumpSel),
MotorData := THIS.Motor
);
END_METHOD
(**Executes the runtime sequence: write data, send to driver,
*read back feedback, enable regulation*)
METHOD PROTECTED OnRun
THIS.WriteMotorData();
THIS.Driver();
THIS.ReadMotorData();
THIS.SetCanGo(TRUE);
END_METHOD
(**Resets the driver channel and reinitializes internal control logic*)
METHOD OVERRIDE PUBLIC Reset
THIS.Driver();
SUPER.Reset();
END_METHOD
(**Applies InternalCfg to controller params*)
METHOD OVERRIDE PROTECTED ApplyConfiguration
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MaxSpeed := THIS.InternalCfg.MaxSpeed;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MinSpeed := THIS.InternalCfg.MinSpeed;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampUpTime := THIS.InternalCfg.RampUpTime;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampDownTime := THIS.InternalCfg.RampDownTime;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RatedCurrent := THIS.InternalCfg.RatedCurrent;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.OverloadCurr := THIS.InternalCfg.OverloadCurrent;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RatedFrequency := THIS.InternalCfg.RatedFrequency;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.DryRunDelay := THIS.InternalCfg.DryRunDelay;
SUPER.ApplyConfiguration();
END_METHOD
{REGION Setters}
(**Sets the default factory configuration*)
METHOD PUBLIC SetDefaultConfig
VAR
CfgDefault : CFG_VFD_MOTOR; (*Initialized with struct defaults*)
END_VAR
THIS.IsConfigChanged := THIS.IsConfigChanged
OR THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Factory;
THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Factory;
THIS.InternalCfg := CfgDefault;
END_METHOD
(**Sets user-defined configuration*)
METHOD PUBLIC SetCustomConfig
VAR_INPUT
CfgCustom : CFG_VFD_MOTOR;
END_VAR
THIS.IsConfigChanged := THIS.IsConfigChanged
OR (THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Custom
OR THIS.InternalCfg <> CfgCustom);
THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Custom;
THIS.InternalCfg := CfgCustom;
END_METHOD
{ENDREGION}
{REGION Getters}
(**Returns the motor type*)
METHOD PUBLIC GetMotorType : MOTOR_TYPE
GetMotorType := MOTOR_TYPE#VFD;
END_METHOD
{ATTRIBUTE UOM RPM}
(**Returns the current motor speed*)
METHOD OVERRIDE PUBLIC GetMotorSpeed : UINT
GetMotorSpeed := SUPER.GetMotorSpeed();
END_METHOD
{ENDREGION}
END_CLASS
Key features of final classes:
FINAL— the class cannot be further inheritedPUBLIC— visible to library consumers- Implements
OnRun()— concrete control logic OVERRIDE PROTECTED ApplyConfiguration— extends the base configuration, always callsSUPER.ApplyConfiguration()
6. Design Patterns
6.1. Template Method Pattern
The most important pattern I use in control libraries. The base class BasePump.Run() defines the algorithm skeleton, and derived classes provide the OnRun() implementation:
BasePump.Run():
┌─ ApplyConfiguration() ← VIRTUAL (overridden in subclasses)
├─ Check IsConfigChanged
├─ UpdateDemand() ← VIRTUAL
├─ UpdateSensorsAndParams()
├─ CoreProcessingPhase()
├─ UpdateReadyToCommand() ← VIRTUAL
├─ OnRun() ← ABSTRACT (implemented in subclasses)
└─ UpdateAlarmStatus() ← VIRTUAL
Each final class implements OnRun() differently:
| Class | OnRun() logic |
|---|---|
VFD_Pump | WriteMotorData → Driver(analog out) → ReadMotorData → CanGo |
SoftStartPump | WriteMotorData → Driver(soft-start) → ReadMotorData → CanGo |
VFD_PumpModbus | WriteMotorData → Modbus calls (WriteSpeed, ReadFeedback…) → ReadMotorData → Error check |
ExternalController | Synchronization with standalone inverter via REF_TO T_MOTOR_DATA |
DOL_Pump | WriteMotorData → ReadMotorData → CanGo (direct on/off, no speed regulation) |
6.2. Configuration change detection pattern
Every class with configuration implements a change detection pattern:
METHOD PUBLIC SetCustomConfig
VAR_INPUT
CfgCustom : CFG_VFD_MOTOR;
END_VAR
(*Detect change: variant changed OR parameters differ*)
THIS.IsConfigChanged := THIS.IsConfigChanged
OR (THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Custom
OR THIS.InternalCfg <> CfgCustom);
THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Custom;
THIS.InternalCfg := CfgCustom;
END_METHOD
Logic: the IsConfigChanged flag is accumulated (OR) — once set, it will not be cleared until Reset(). It is checked in Run() → if TRUE, Reset() is called followed by RETURN.
6.3. Default configuration via local variable pattern
METHOD PUBLIC SetDefaultConfig
VAR
CfgDefault : CFG_VFD_MOTOR; (*Struct with default values*)
END_VAR
THIS.InternalCfg := CfgDefault;
END_METHOD
The local variable CfgDefault is initialized with the default values from the CFG_VFD_MOTOR structure. No need to set them manually — simply assign a fresh instance.
6.4. Reference pattern for large structures
VAR PRIVATE
RefRegParams : REF_TO REG_PARAMS;
RefAdvRegParams : REF_TO ADV_REG_PARAMS;
END_VAR
METHOD PUBLIC SetRegParams
VAR_IN_OUT CONSTANT
RegParams : REG_PARAMS;
END_VAR
THIS.RefRegParams := REF(RegParams);
THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);
THIS.SetPID_Params();
THIS.SetEngineParams();
END_METHOD
Principle: large configuration structures are stored as REF_TO — the data stays in one place, and the class holds only a pointer. Always validate for NULL before use:
IF THIS.RefAdvRegParams <> NULL THEN
(* ... use THIS.RefAdvRegParams^.FlowSetpoint ... *)
END_IF;
6.5. Getter/Setter pattern
A consistent Get/Set pattern:
(*SETTER — Set prefix*)
METHOD PUBLIC SetDemand
VAR_INPUT
Demand : UINT(0..100);
END_VAR
THIS.Demand := Demand;
END_METHOD
(*GETTER — Get prefix, return value via method name*)
METHOD PUBLIC GetReadyToCommand : BOOL
GetReadyToCommand := THIS.ReadyToCmd;
END_METHOD
(*GETTER with logic — compute before returning*)
{ATTRIBUTE UOM PERCENT}
METHOD PUBLIC GetSpeedPercent : REAL
IF THIS.Motor.MaxSpeed <> 0 THEN
GetSpeedPercent := MIN(100,
TO_REAL(THIS.Motor.CurrentSpeed) * 100
/ TO_REAL(THIS.Motor.MaxSpeed));
END_IF;
END_METHOD
Key point: the return value is assigned to the method name: GetXxx := value;
6.6. Method overloading
STone supports overloading — the same method name with different signatures:
(*UINT overload*)
METHOD PUBLIC SetDemand
VAR_INPUT
Demand : UINT(0..100);
END_VAR
THIS.SetDemand(TO_USINT(Demand));
END_METHOD
(*USINT overload*)
METHOD PUBLIC SetDemand
VAR_INPUT
Demand : USINT(0..100);
END_VAR
THIS.Demand := TO_UINT(Demand);
END_METHOD
And overloading with different parameter types (error handling example):
METHOD PRIVATE SetError
VAR_INPUT
ID : ERROR_ID;
Code : ERROR_CODE;
END_VAR
THIS.LastError.ID := ID;
THIS.LastError.Code := Code;
END_METHOD
METHOD PRIVATE SetError
VAR_INPUT
Error : ERROR_INFO;
END_VAR
THIS.SetError(Error.ID, Error.Code);
END_METHOD
7. Class and Method Modifiers
7.1. Class modifiers
| Modifier | Meaning | Example |
|---|---|---|
ABSTRACT | Cannot be instantiated; requires subclasses | BasePump, BaseVariableSpeedPump |
FINAL | Cannot be inherited | VFD_Pump, SoftStartPump, DOL_Pump |
INTERNAL | Visible only within the library | BasePump, BaseVariableSpeedPump |
PUBLIC | Visible to library consumers | VFD_Pump, ExternalController |
Common combinations:
| Declaration | Role |
|---|---|
CLASS ABSTRACT INTERNAL | Base class, hidden from the user |
CLASS FINAL PUBLIC | Final class, public API |
7.2. Method modifiers
| Modifier | Meaning |
|---|---|
ABSTRACT | No body — must be implemented in a subclass |
OVERRIDE | Overrides a base class method |
PUBLIC | Accessible from outside |
PROTECTED | Accessible within the class and subclasses |
PRIVATE | Accessible only within the given class |
Override chain example:
(*BasePump — base implementation*)
METHOD PROTECTED UpdateAlarmStatus
THIS.AlarmStatus.PowerFailure :=
THIS.Motor.Alarms = ALARM_POWER_SUPPLY_FAILURE;
END_METHOD
(*SoftStartPump — extends base: adds motor thermal alarm*)
METHOD OVERRIDE PROTECTED UpdateAlarmStatus
SUPER.UpdateAlarmStatus(); (*Base logic first*)
THIS.AlarmStatus.MotorOverTemp :=
(THIS.MotorSoftStart.CommonData.Alarms <> 0)
AND (THIS.MotorSoftStart.CommonData.Alarms <> ALARM_POWER_SUPPLY_FAILURE);
END_METHOD
8. Functions (FUNCTION)
Functions are standalone, stateless, and operate on input data:
(**Checks array elements for error codes, returns first error found*)
FUNCTION INTERNAL ValidateModbusResults : ERROR_INFO
VAR_IN_OUT CONSTANT
Results : ARRAY [*] OF ErrorCode; (*Variable-length array*)
END_VAR
VAR
i : DINT;
END_VAR
FOR i := LOWER_BOUND(Results, 1) TO UPPER_BOUND(Results, 1) DO
IF Results[i] <> ErrorCode#None THEN
ValidateModbusResults.ID := TO_USINT(i);
ValidateModbusResults.Code := Results[i];
RETURN; (*Early return — first error found*)
END_IF;
END_FOR;
END_FUNCTION
Features:
ARRAY [*]— variable-length arrayLOWER_BOUND()/UPPER_BOUND()— intrinsics for array bounds- Return value via assignment to the function name:
ValidateModbusResults.ID := ... - Access to fields of the returned structure via
.on the function name RETURNas early exit
Another example — hardware compatibility check:
(**Checks whether the current controller matches the expected hardware ID*)
FUNCTION INTERNAL CheckControllerID : BOOL
VAR_INPUT
ExpectedID : UINT;
END_VAR
VAR
DevID : UINT;
BoardType : UINT;
MachineType : UINT;
HwCode : UINT;
END_VAR
GetModel(DevID, BoardType, MachineType, HwCode);
CheckControllerID := DevID = ExpectedID;
END_FUNCTION
9. Comments and Documentation
9.1. Comment types
| Syntax | Purpose | Visibility |
|---|---|---|
(*comment*) | Standard block comment | Code |
(**comment*) | Documentation comment (IntelliSense) | IDE + tooltips |
//comment | Line comment | Code |
9.2. Documentation comments (**...*)
Every public element should have a (**...*) comment directly before the declaration:
(**Abstract class implementing the IPump interface.
*It manages the complete regulation cycle of the pump, including:
*- controller initialization,
*- sensor acquisition,
*- regulation parameter management,
*- execution of the core algorithm,
*- data synchronization with the core,
*- alarm management,
*- system state evaluation.*)
CLASS ABSTRACT INTERNAL BasePump IMPLEMENTS IPump
(**Emergency stop current used when operating
with reduced power from backup supply*)
EmergencyStopCurrent : UINT := 350;
(**Returns the current speed as a percentage of max speed*)
METHOD PUBLIC GetSpeedPercent : REAL
Documentation conventions:
- Classes: describe the role and responsibilities (multi-line, with
*at the start of continuation lines) - Methods: a single line describing the action (starts with a verb: Returns, Sets, Executes, Resets)
- Variables: a short description of the purpose
VAR_INPUT/VAR_IN_OUTparameters: describe each parameter
9.3. Operational comments (*...*)
Inside method bodies — explain the logic:
(*Disable motor output to shut off the drive*)
THIS.Motor.Enable := 0;
(*Force a processing phase to return to initial state*)
THIS.CoreProcessingPhase();
(*Configuration change detected — Reset required
to restart the control flow in a clean and consistent state*)
IF THIS.IsConfigChanged THEN
THIS.Reset();
RETURN;
END_IF;
10. Naming Conventions
10.1. Summary
| Element | Convention | Examples |
|---|---|---|
| Interface | I + PascalCase | IPump, IVariableSpeedPump |
| Base class | Base + PascalCase | BasePump, BaseVariableSpeedPump |
| Final class | PascalCase (no prefix) | VFD_Pump, SoftStartPump, ExternalController |
| Public method | Get/Set + PascalCase | GetMotorSpeed, SetRegParams |
| Protected method | PascalCase | OnRun, ApplyConfiguration, UpdateAlarmStatus |
| Private method | PascalCase | Driver, CoreProcessingPhase |
| Local variable | PascalCase | CfgDefault, InternalMotorType, ErrCode |
| Instance variable | PascalCase | PumpSel, IsConfigChanged, ReadyToCmd |
| Global constant | UPPER_SNAKE_CASE | DEFAULT_MAX_SPEED, TARGET_CONTROLLER_ID |
| Type (struct) | UPPER_SNAKE_CASE | REG_PARAMS, CFG_VFD_MOTOR, PRESSURE_PROTECTION |
| Enumerator | UPPER_SNAKE_CASE | MOTOR_TYPE, REG_TYPE, MOTOR_CFG_VARIANT |
| Enumerator value | PascalCase | VFD, ExternalController, Factory |
| Named value | PascalCase | Pump1, EmergencyStop, Regulation |
| Bool prefix | Is/En/Al | IsConfigChanged, En_Backup, Al_Present |
| Reference | Ref + name | RefRegParams, RefAdvRegParams |
10.2. Separators in names
In the STone environment, I use the underscore _ as a separator in complex technical names:
PumpSel— Pump SelectionPID_Params— PID ParametersHighPress_Ti— High Pressure Integral TimeAlarmStatus— Alarm StatusIsConfigChanged— withIsprefix
11. Type Conversion Patterns
STone requires explicit type conversions using TO_xxx functions:
(*USINT → BYTE*)
ChannelNum := TO_BYTE(THIS.PumpSel)
(*ENUM → USINT for use in CASE*)
CASE TO_USINT(THIS.ControllerParams.RegulationType) OF
REG_TYPE#Pressure, REG_TYPE#PressureCascade: ...
END_CASE;
(*UINT → REAL for floating-point arithmetic*)
SpeedPercent := TO_REAL(THIS.Motor.CurrentSpeed) * 100
/ TO_REAL(THIS.Motor.MaxSpeed);
(*ENUM → BYTE*)
THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);
(*Loop index → USINT*)
ValidateModbusResults.ID := TO_USINT(i);
12. Error Handling
12.1. Communication result checking pattern
METHOD PROTECTED OnRun
VAR
MbResults : ARRAY [101..105] OF ErrorCode;
RunError : ERROR_INFO;
END_VAR
MbResults[101] := WriteFrequency(THIS.PumpSel, THIS.Motor.TargetFrequency);
MbResults[102] := WriteSpeedRef(THIS.PumpSel, THIS.Motor.SpeedReference);
MbResults[103] := WriteRampTime(THIS.PumpSel, THIS.Motor.RampTime);
MbResults[104] := ReadActualSpeed(THIS.PumpSel, THIS.Motor.CurrentSpeed);
MbResults[105] := ReadMotorCurrent(THIS.PumpSel, THIS.Motor.ActualCurrent);
RunError := ValidateModbusResults(MbResults);
THIS.ReadMotorData();
IF RunError.Code = ERROR_CODE#None THEN
THIS.SetCanGo(TRUE);
END_IF;
IF THIS.ControllerParams.CanGo THEN
THIS.SetError(RunError);
END_IF;
END_METHOD
12.2. Cascading validation pattern
METHOD OVERRIDE PROTECTED ApplyConfiguration
VAR
ErrCode : ErrorCode;
END_VAR
IF THIS.ConfigRequired THEN
{REGION Check hardware compatibility}
ErrCode := GetDriverInfo(THIS.PumpSel, THIS.DriverInfo);
IF NOT CheckControllerID(TARGET_CONTROLLER_ID) THEN
THIS.SetError(ERROR_ID#HW, ERROR_CODE#NotCompatible);
ELSIF ErrCode = ErrorCode#NotAvailable THEN
THIS.SetError(ERROR_ID#OS, ERROR_CODE#NotCompatible);
ELSIF ErrCode <> ErrorCode#None THEN
THIS.SetError(ERROR_ID#GetDriverInfo, ErrCode);
ELSIF NOT THIS.DriverInfo.AvailableFeatures.Bits.ModbusExtMode THEN
THIS.SetError(ERROR_ID#FW, ERROR_CODE#NotCompatible);
END_IF;
IF THIS.LastError.ID <> ERROR_ID#None THEN
RETURN; (*Abort on error*)
END_IF;
{ENDREGION}
{REGION Load configuration}
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MaxSpeed := THIS.InternalCfg.MaxSpeed;
THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampUpTime := THIS.InternalCfg.RampUpTime;
(* ... *)
SUPER.ApplyConfiguration();
THIS.ApplyConfigurationToDriver();
IF THIS.LastError.ID <> ERROR_ID#None THEN
RETURN;
END_IF;
{ENDREGION}
{REGION Enable operation}
ErrCode := EnableOperation(THIS.PumpSel, TRUE);
IF ErrCode <> ErrorCode#None THEN
THIS.SetError(ERROR_ID#EnableOperation, ErrCode);
RETURN;
END_IF;
{ENDREGION}
THIS.ConfigRequired := FALSE;
END_IF;
END_METHOD
Pattern: a series of steps with RETURN after each potential error — fail-fast without nested IFs.
12.3. Conditional API calls
IF THIS.DriverInfo.AvailableFeatures.Bits.RampControl THEN
MbResults[5] := WriteRampUpTime(THIS.PumpSel,
THIS.InternalCfg.RampUpTime);
END_IF;
IF THIS.DriverInfo.AvailableFeatures.Bits.CurrentMonitoring THEN
MbResults[6] := WriteOverloadThreshold(THIS.PumpSel,
THIS.InternalCfg.OverloadCurrent);
END_IF;
Principle: check the driver/inverter feature-flags before calling optional commands — this way, the code works with different models and firmware versions.
13. Best Practices Summary
Architecture
- One file = one element (class, interface, function)
- Interfaces as contracts — define the
INTERFACEbefore the implementation - Hierarchy: abstract base → final classes — the user sees only
FINAL PUBLIC - Hide the implementation — base classes as
INTERNAL - Template Method Pattern —
METHOD ABSTRACT PROTECTED OnRunas the abstract method and extension point
Memory and performance
VAR_IN_OUT CONSTANTfor large structures passed to methodsREF_TOfor storing references — avoid copying- Smallest base type —
USINTinstead ofUINTwhen the 0–255 range is sufficient - Type ranges —
UINT(0..100)instead of bareUINT
Conventions
- Prefixes:
I(interface),Base(abstract class),Get/Set(methods),Is/En(bool) (**...*)comments (IntelliSense) on every public element{REGION}for code organization in the IDE- Conditional compilation
{IF DEF}for hardware variant support - Explicit type conversions —
TO_BYTE(),TO_REAL(),TO_USINT()
Safety
NULLvalidation before dereferencingREF_TO- Fail-fast with
RETURNinstead of deep nesting - Configuration change detection —
IsConfigChangedflag +Reset() - Division by zero — check
<> 0before dividing
Author: Hubert | Article based on experience with production-grade libraries in the STone IDE.