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:

DirectoryContentsExample files
Classes/Classes (abstract and final)BasePump.st, VFD_Pump.st, DOL_Pump.st
Interfaces/Interfaces (contracts)IPump.st, IVariableSpeedPump.st
Functions/Global functionsValidateModbusResults.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 .st file begins with {INCLUDE '../LibVer.g.st'}
  • The LibraryNamespaceAndVersion macro 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:

  • USING imports 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:

DefineEffect
FEATURE_MODBUS_DRIVESEnables VFD control via Modbus — requires extended communication API
FEATURE_ADVANCED_MOTORSEnables 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:

RegionContents
SettersMethods that set parameters
GettersMethods that return values
Public / ProtectedSub-regions inside Setters/Getters grouped by visibility
Default paramsDefault constants
NAMED VALUESEnumeration types with explicit numeric values
ENUMERATORSStandard enumerators
CONFIGURATIONConfiguration structures
INFORMATIONInformation/status structures
Conditional API callsBlocks 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_STATE has gaps: 12→18, 21→100)
  • The INTERNAL modifier 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_Pump inside 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;
AttributeMeaning
{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_CASE with 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:

RuleExample
I prefix in the nameIPump, IVariableSpeedPump
No method bodiesSignatures only
Grouping into regions{REGION Setters}, {REGION Getters}
Attributes on methods{ATTRIBUTE UOM BAR} before METHOD
VAR_IN_OUT CONSTANT for structuresPass 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 directly
  • INTERNAL — class visible only within the library
  • IMPLEMENTS IPump — interface contract implementation
  • METHOD 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
ModifierVisibility scope
PRIVATEOnly within the given class
PROTECTEDWithin the class and its subclasses
PUBLICEverywhere (default for interface methods)
INTERNALWithin 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 implementation
  • OVERRIDE — overrides a base class method
  • SUPER.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 inherited
  • PUBLIC — visible to library consumers
  • Implements OnRun() — concrete control logic
  • OVERRIDE PROTECTED ApplyConfiguration — extends the base configuration, always calls SUPER.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:

ClassOnRun() logic
VFD_PumpWriteMotorData → Driver(analog out) → ReadMotorData → CanGo
SoftStartPumpWriteMotorData → Driver(soft-start) → ReadMotorData → CanGo
VFD_PumpModbusWriteMotorData → Modbus calls (WriteSpeed, ReadFeedback…) → ReadMotorData → Error check
ExternalControllerSynchronization with standalone inverter via REF_TO T_MOTOR_DATA
DOL_PumpWriteMotorData → 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

ModifierMeaningExample
ABSTRACTCannot be instantiated; requires subclassesBasePump, BaseVariableSpeedPump
FINALCannot be inheritedVFD_Pump, SoftStartPump, DOL_Pump
INTERNALVisible only within the libraryBasePump, BaseVariableSpeedPump
PUBLICVisible to library consumersVFD_Pump, ExternalController

Common combinations:

DeclarationRole
CLASS ABSTRACT INTERNALBase class, hidden from the user
CLASS FINAL PUBLICFinal class, public API

7.2. Method modifiers

ModifierMeaning
ABSTRACTNo body — must be implemented in a subclass
OVERRIDEOverrides a base class method
PUBLICAccessible from outside
PROTECTEDAccessible within the class and subclasses
PRIVATEAccessible 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 array
  • LOWER_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
  • RETURN as 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

SyntaxPurposeVisibility
(*comment*)Standard block commentCode
(**comment*)Documentation comment (IntelliSense)IDE + tooltips
//commentLine commentCode

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_OUT parameters: 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

ElementConventionExamples
InterfaceI + PascalCaseIPump, IVariableSpeedPump
Base classBase + PascalCaseBasePump, BaseVariableSpeedPump
Final classPascalCase (no prefix)VFD_Pump, SoftStartPump, ExternalController
Public methodGet/Set + PascalCaseGetMotorSpeed, SetRegParams
Protected methodPascalCaseOnRun, ApplyConfiguration, UpdateAlarmStatus
Private methodPascalCaseDriver, CoreProcessingPhase
Local variablePascalCaseCfgDefault, InternalMotorType, ErrCode
Instance variablePascalCasePumpSel, IsConfigChanged, ReadyToCmd
Global constantUPPER_SNAKE_CASEDEFAULT_MAX_SPEED, TARGET_CONTROLLER_ID
Type (struct)UPPER_SNAKE_CASEREG_PARAMS, CFG_VFD_MOTOR, PRESSURE_PROTECTION
EnumeratorUPPER_SNAKE_CASEMOTOR_TYPE, REG_TYPE, MOTOR_CFG_VARIANT
Enumerator valuePascalCaseVFD, ExternalController, Factory
Named valuePascalCasePump1, EmergencyStop, Regulation
Bool prefixIs/En/AlIsConfigChanged, En_Backup, Al_Present
ReferenceRef + nameRefRegParams, RefAdvRegParams

10.2. Separators in names

In the STone environment, I use the underscore _ as a separator in complex technical names:

  • PumpSel — Pump Selection
  • PID_Params — PID Parameters
  • HighPress_Ti — High Pressure Integral Time
  • AlarmStatus — Alarm Status
  • IsConfigChanged — with Is prefix

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

  1. One file = one element (class, interface, function)
  2. Interfaces as contracts — define the INTERFACE before the implementation
  3. Hierarchy: abstract base → final classes — the user sees only FINAL PUBLIC
  4. Hide the implementation — base classes as INTERNAL
  5. Template Method PatternMETHOD ABSTRACT PROTECTED OnRun as the abstract method and extension point

Memory and performance

  1. VAR_IN_OUT CONSTANT for large structures passed to methods
  2. REF_TO for storing references — avoid copying
  3. Smallest base typeUSINT instead of UINT when the 0–255 range is sufficient
  4. Type rangesUINT(0..100) instead of bare UINT

Conventions

  1. Prefixes: I (interface), Base (abstract class), Get/Set (methods), Is/En (bool)
  2. (**...*) comments (IntelliSense) on every public element
  3. {REGION} for code organization in the IDE
  4. Conditional compilation {IF DEF} for hardware variant support
  5. Explicit type conversionsTO_BYTE(), TO_REAL(), TO_USINT()

Safety

  1. NULL validation before dereferencing REF_TO
  2. Fail-fast with RETURN instead of deep nesting
  3. Configuration change detectionIsConfigChanged flag + Reset()
  4. Division by zero — check <> 0 before dividing

Author: Hubert | Article based on experience with production-grade libraries in the STone IDE.