Programowanie obiektowe w STone
Kompletny przewodnik
Programowanie obiektowe w STone
Kompletny przewodnik
Niniejszy artykuł to zbiór zasad i konwencji programowania obiektowego w języku Structured Text (ST) w środowisku STone. Wszystkie opisane wzorce i praktyki bazują na moim wieloletnim doświadczeniu z produkcyjnymi bibliotekami przemysłowymi. Jako przykład przewodni wybrałem bibliotekę sterowania pompownią przemysłową — system zarządzający różnymi typami pomp (VFD, DOL, soft-start) z regulacją ciśnienia, przepływu i poziomu cieczy. Zasady mają jednak zastosowanie uniwersalne.
1. Struktura projektu STone
1.1. Plik projektu/biblioteki .stprj
Każdy projekt/biblioteka STone posiada plik XML .stprj, który definiuje metadane i strukturę kompilacji.
<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. Organizacja katalogów
Dobrą praktyką jest podział kodu na katalogi odzwierciedlające elementy OOP:
| Katalog | Zawartość | Przykłady plików |
|---|---|---|
Classes/ | Klasy (abstrakcyjne i finalne) | BasePump.st, VFD_Pump.st, DOL_Pump.st |
Interfaces/ | Interfejsy (kontrakty) | IPump.st, IVariableSpeedPump.st |
Functions/ | Funkcje globalne | ValidateModbusResults.st, CheckControllerID.st |
Libs/ | Zależności zewnętrzne (.stlib) | PumpCore_v.2.0.3.0.stlib |
Konwencja: każda klasa/interfejs = osobny plik .st o nazwie identycznej z nazwą klasy/interfejsu.
1.3. Wersjonowanie bibliotek — LibVer.g.st
STone stosuje mechanizm preprocesora do zarządzania wersjami i namespace’ami:
{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}
Kluczowe zasady:
- Wersja jest częścią namespace’a (
Libs.PumpStation_Lib_v2_0_0) — pozwala na współistnienie wielu wersji biblioteki - Include guard (
__LIBRARYNAMESPACEANDVERSION__) zapobiega wielokrotnemu definiowaniu - Każdy plik
.stzaczyna się od{INCLUDE '../LibVer.g.st'} - Makro
LibraryNamespaceAndVersionjest używane jako nazwa NAMESPACE w każdym pliku
2. Namespace i dyrektywy preprocesora
2.1. Namespace
Każdy plik w bibliotece jest opakowany w NAMESPACE z makrem wersyjnym:
{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
Konwencje:
USINGimportuje inne namespace’y — zawsze na początku, po otwarciu NAMESPACE- Zależne biblioteki importowane przez pełną ścieżkę z wersją:
Libs.PumpCore_v2_0_3_0 - Typy systemowe przez
System.IO,System.Math
2.2. Kompilacja warunkowa {IF DEF ...}
STone wspiera dyrektywy preprocesora do kompilacji warunkowej. W praktyce intensywnie je wykorzystuję do obsługi wariantów sprzętowych:
{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}
Praktyczne zastosowania:
| Define | Efekt |
|---|---|
FEATURE_MODBUS_DRIVES | Włącza sterowanie VFD przez Modbus — wymaga rozszerzonego API komunikacyjnego |
FEATURE_ADVANCED_MOTORS | Włącza pompy soft-start i DOL z rozszerzonym monitoringiem |
Dzięki temu jeden codebase obsługuje wiele wariantów sprzętowych. Klasy mogą mieć dwie niezależne implementacje w tym samym pliku, przełączane w compile-time.
2.3. Regiony {REGION ... ENDREGION}
Regiony porządkują sekcje kodu wizualnie w IDE:
{REGION Setters}
METHOD PUBLIC SetDefaultConfig
(* ... *)
END_METHOD
METHOD PUBLIC SetCustomConfig
(* ... *)
END_METHOD
{ENDREGION}
{REGION Getters}
METHOD PUBLIC GetMotorType : MOTOR_TYPE
(* ... *)
END_METHOD
{ENDREGION}
Zalecana konwencja regionów:
| Region | Zawartość |
|---|---|
Setters | Metody ustawiające parametry |
Getters | Metody zwracające wartości |
Public / Protected | Podregiony wewnątrz Setters/Getters grupujące po widoczności |
Default params | Stałe domyślne |
NAMED VALUES | Typy wyliczeniowe z przypisanymi wartościami numerycznymi |
ENUMERATORS | Standardowe enumeratory |
CONFIGURATION | Struktury konfiguracyjne |
INFORMATION | Struktury informacyjne/statusowe |
Conditional API calls | Bloki wywołań warunkowanych na feature-flags sprzętu |
3. Typy danych
3.1. Named Values (enumeratory z wartościami)
STone pozwala na definiowanie enumeratorów z jawnie przypisanymi wartościami numerycznymi. Składnia:
TYPE
(**Type description*)
TYPE_NAME : BASE_TYPE(
(**Value 1 description*)
Value1 := 0,
(**Value 2 description*)
Value2 := 1,
(**Value 3 description*)
Value3 := 2
);
END_TYPE
Przykład — wybór pompy:
(**Selection of which pump to control*)
PUMP_SEL : USINT(
(**Control first pump*)
Pump1 := 1,
(**Control second pump*)
Pump2 := 2,
(**Control third pump*)
Pump3 := 3
);
Przykład — stany maszyny sterującej:
(**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
);
Kluczowe cechy:
- Typ bazowy (
USINT,UINT,DINT) determinuje rozmiar w pamięci — optymalizacja zasobów PLC - Wartości nie muszą być ciągłe (np.
PUMP_STATEma luki: 12→18, 21→100) - Modyfikator
INTERNALogranicza widoczność:TYPE INTERNAL— typ niedostępny spoza biblioteki - Dostęp przez kwalifikator:
PUMP_STATE#Regulation,PUMP_SEL#Pump1
3.2. Enumeratory standardowe
Bez jawnych wartości — kompilator przypisuje je automatycznie od 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
Zwróć uwagę na przecinek przed
DOL_Pumpwewnątrz{IF DEF}— jest to konieczne, aby zachować poprawną składnię niezależnie od stanu kompilacji warunkowej.
3.3. Struktury (STRUCT)
Struktury grupują powiązane dane. W praktyce stosuję intensywne zagnieżdżanie struktur:
(**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;
Konwencje struktur:
- Każde pole ma wartość domyślną (
:= ...) - Zakresy definiowane wprost:
UINT(0..100),UINT(0..18000) - Zagnieżdżanie jako forma kompozycji — hierarchia danych
3.4. Tablice typowane
(**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. Atrybuty i metadane pól
STone wspiera atrybuty przypisywane do pól i metod:
(**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;
| Atrybut | Znaczenie |
|---|---|
{ATTRIBUTE UOM ...} | Jednostka miary (CELSIUS, BAR, PERCENT, AMPERE, OHM, RPM, SECOND, MINUTE, HERTZ, CUBICMETERPERHOUR) |
{METADATA MIN_VAL ...} | Minimalna wartość parametru |
{METADATA MAX_VAL ...} | Maksymalna wartość parametru |
Te atrybuty mogą być odczytywane przez narzędzia IDE (IntelliSense) i systemy supervisory.
3.6. Stałe globalne 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
Konwencje:
INTERNAL— stałe widoczne tylko wewnątrz biblioteki- Nazwy:
UPPER_SNAKE_CASEz prefiksem tematycznym (np.DEFAULT_) - Wartości domyślne są źródłem prawdy dla parametrów fabrycznych
4. Interfejsy (INTERFACE)
4.1. Definicja interfejsu
Interfejs definiuje kontrakt — zestaw metod, które klasa musi zaimplementować:
(**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. Dziedziczenie interfejsów (EXTENDS)
Interfejsy mogą rozszerzać inne interfejsy:
(**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
Konwencje interfejsów:
| Reguła | Przykład |
|---|---|
Prefix I w nazwie | IPump, IVariableSpeedPump |
| Brak ciał metod | Tylko sygnatury |
| Grupowanie w regiony | {REGION Setters}, {REGION Getters} |
| Atrybuty na metodach | {ATTRIBUTE UOM BAR} przed METHOD |
VAR_IN_OUT CONSTANT dla struktur | Przekazanie przez referencję bez kopii, read-only |
4.3. Wzorzec VAR_IN_OUT CONSTANT
Kluczowy wzorzec optymalizacji pamięci PLC:
METHOD SetRegParams
VAR_IN_OUT CONSTANT
RegParams : REG_PARAMS;
END_VAR
END_METHOD
VAR_IN_OUT— przekazanie przez referencję (brak kopiowania dużych struktur)CONSTANT— zabezpieczenie przed modyfikacją wewnątrz metody- Idealny do przekazywania dużych struktur konfiguracyjnych do setterów
5. Klasy (CLASS)
5.1. Hierarchia klas
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. Klasa abstrakcyjna — 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
Kluczowe wzorce:
ABSTRACT— nie można tworzyć instancji bezpośrednioINTERNAL— klasa widoczna tylko wewnątrz bibliotekiIMPLEMENTS IPump— implementacja kontraktu interfejsuMETHOD ABSTRACT PROTECTED OnRun— metoda abstrakcyjna, punkt rozszerzenia (Template Method Pattern)THIS.— jawne odwołanie do członków instancji
5.3. Modyfikatory widoczności zmiennych
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
| Modyfikator | Zakres widoczności |
|---|---|
PRIVATE | Tylko w danej klasie |
PROTECTED | W klasie i jej podklasach |
PUBLIC | Wszędzie (domyślny dla metod interfejsu) |
INTERNAL | Wewnątrz biblioteki |
Konwencja: zmienne instancji FB i referencje → PRIVATE; struktury współdzielone z podklasami → PROTECTED.
5.4. Klasa pośrednia — 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
Wzorce:
EXTENDS BasePump IMPLEMENTS IVariableSpeedPump— dziedziczenie po jednej klasie + implementacja interfejsuOVERRIDE— nadpisanie metody klasy bazowejSUPER.Reset()— wywołanie implementacji rodzica
5.5. Klasa finalna — 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
Kluczowe cechy klas finalnych:
FINAL— klasa nie może być dalej dziedziczonaPUBLIC— widoczna dla użytkowników biblioteki- Implementuje
OnRun()— konkretna logika sterowania OVERRIDE PROTECTED ApplyConfiguration— rozszerza bazową konfigurację, zawsze wywołujeSUPER.ApplyConfiguration()
6. Wzorce projektowe (Design Patterns)
6.1. Template Method Pattern
Najważniejszy wzorzec, który stosuję w bibliotekach sterowania. Klasa bazowa BasePump.Run() definiuje szkielet algorytmu, a klasy pochodne dostarczają implementację OnRun():
BasePump.Run():
┌─ ApplyConfiguration() ← VIRTUAL (override w podklasach)
├─ Check IsConfigChanged
├─ UpdateDemand() ← VIRTUAL
├─ UpdateSensorsAndParams()
├─ CoreProcessingPhase()
├─ UpdateReadyToCommand() ← VIRTUAL
├─ OnRun() ← ABSTRACT (implementacja w podklasach)
└─ UpdateAlarmStatus() ← VIRTUAL
Każda klasa finalna implementuje OnRun() inaczej:
| Klasa | Logika OnRun() |
|---|---|
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 | Synchronizacja ze standalone-inverterem przez REF_TO T_MOTOR_DATA |
DOL_Pump | WriteMotorData → ReadMotorData → CanGo (direct on/off, no speed regulation) |
6.2. Wzorzec detekcji zmiany konfiguracji
Każda klasa z konfiguracją implementuje wzorzec wykrywania zmian:
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
Logika: flaga IsConfigChanged jest akumulowana (OR) — raz ustawiona, nie zostanie zresetowana aż do Reset(). Sprawdzana w Run() → jeśli TRUE, wykonuje Reset() i RETURN.
6.3. Wzorzec domyślnej konfiguracji przez zmienną lokalną
METHOD PUBLIC SetDefaultConfig
VAR
CfgDefault : CFG_VFD_MOTOR; (*Struct with default values*)
END_VAR
THIS.InternalCfg := CfgDefault;
END_METHOD
Zmienna lokalna CfgDefault jest inicjalizowana wartościami domyślnymi ze struktury CFG_VFD_MOTOR. Nie trzeba ich ustawiać ręcznie — wystarczy przypisać świeżą instancję.
6.4. Wzorzec referencji dla dużych struktur
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
Zasada: duże struktury konfiguracyjne są przechowywane jako REF_TO — dane pozostają w jednym miejscu, klasa trzyma tylko wskaźnik. Walidacja NULL przed użyciem:
IF THIS.RefAdvRegParams <> NULL THEN
(* ... use THIS.RefAdvRegParams^.FlowSetpoint ... *)
END_IF;
6.5. Wzorzec Getter/Setter
Konsekwentny wzorzec Get/Set:
(*SETTER — prefix Set*)
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
Kluczowe: wartość zwracana przez przypisanie do nazwy metody: GetXxx := wartość;
6.6. Przeciążanie metod (Method Overloading)
STone wspiera przeciążanie — ta sama nazwa metody, różne sygnatury:
(*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
Oraz przeciążanie z różnymi typami parametrów (na przykładzie obsługi błędów):
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. Modyfikatory klas i metod
7.1. Modyfikatory klas
| Modyfikator | Znaczenie | Przykład |
|---|---|---|
ABSTRACT | Nie można instancjonować; wymaga podklas | BasePump, BaseVariableSpeedPump |
FINAL | Nie można dziedziczyć | VFD_Pump, SoftStartPump, DOL_Pump |
INTERNAL | Widoczna tylko wewnątrz biblioteki | BasePump, BaseVariableSpeedPump |
PUBLIC | Widoczna dla użytkowników biblioteki | VFD_Pump, ExternalController |
Stosowane kombinacje:
| Deklaracja | Rola |
|---|---|
CLASS ABSTRACT INTERNAL | Klasa bazowa, ukryta przed użytkownikiem |
CLASS FINAL PUBLIC | Klasa końcowa, API publiczne |
7.2. Modyfikatory metod
| Modyfikator | Znaczenie |
|---|---|
ABSTRACT | Brak ciała — musi być zaimplementowana w podklasie |
OVERRIDE | Nadpisuje metodę klasy bazowej |
PUBLIC | Dostępna z zewnątrz |
PROTECTED | Dostępna w klasie i podklasach |
PRIVATE | Dostępna tylko w danej klasie |
Przykład łańcucha override:
(*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. Funkcje (FUNCTION)
Funkcje są samodzielne, bezstanowe i operują na danych wejściowych:
(**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
Cechy:
ARRAY [*]— tablica o dynamicznym rozmiarze (variable-length array)LOWER_BOUND()/UPPER_BOUND()— intrinsics do granic tablicy- Wartość zwracana przez przypisanie do nazwy funkcji:
ValidateModbusResults.ID := ... - Dostęp do pól zwracanej struktury przez
.na nazwie funkcji RETURNjako early exit
Kolejny przykład — sprawdzanie kompatybilności sprzętowej:
(**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. Komentarze i dokumentacja
9.1. Typy komentarzy
| Składnia | Cel | Widoczność |
|---|---|---|
(*komentarz*) | Komentarz blokowy standardowy | Kod |
(**komentarz*) | Komentarz dokumentacyjny (IntelliSense) | IDE + podpowiedzi |
//komentarz | Komentarz liniowy | Kod |
9.2. Komentarze dokumentacyjne (**...*)
Każdy publiczny element powinien mieć komentarz (**...*) bezpośrednio przed deklaracją:
(**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
Konwencje dokumentacji:
- Klasy: opis roli i odpowiedzialności (multi-line, z
*na początku kontynuacji) - Metody: jeden wiersz opisujący akcję (zaczyna się od czasownika: Returns, Sets, Executes, Resets)
- Zmienne: krótki opis przeznaczenia
- Parametry
VAR_INPUT/VAR_IN_OUT: opis każdego parametru
9.3. Komentarze operacyjne (*...*)
Wewnątrz ciał metod — wyjaśniają logikę:
(*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. Konwencje nazewnictwa
10.1. Podsumowanie
| Element | Konwencja | Przykłady |
|---|---|---|
| Interfejs | I + PascalCase | IPump, IVariableSpeedPump |
| Klasa bazowa | Base + PascalCase | BasePump, BaseVariableSpeedPump |
| Klasa finalna | PascalCase (bez prefixu) | VFD_Pump, SoftStartPump, ExternalController |
| Metoda publiczna | Get/Set + PascalCase | GetMotorSpeed, SetRegParams |
| Metoda chroniona | PascalCase | OnRun, ApplyConfiguration, UpdateAlarmStatus |
| Metoda prywatna | PascalCase | Driver, CoreProcessingPhase |
| Zmienna lokalna | PascalCase | CfgDefault, InternalMotorType, ErrCode |
| Zmienna instancji | PascalCase | PumpSel, IsConfigChanged, ReadyToCmd |
| Stała globalna | UPPER_SNAKE_CASE | DEFAULT_MAX_SPEED, TARGET_CONTROLLER_ID |
| Typ (struct) | UPPER_SNAKE_CASE | REG_PARAMS, CFG_VFD_MOTOR, PRESSURE_PROTECTION |
| Enumerator | UPPER_SNAKE_CASE | MOTOR_TYPE, REG_TYPE, MOTOR_CFG_VARIANT |
| Wartość enumeratora | PascalCase | VFD, ExternalController, Factory |
| Named value | PascalCase | Pump1, EmergencyStop, Regulation |
| Prefiks Bool | Is/En/Al | IsConfigChanged, En_Backup, Al_Present |
| Referencja | Ref + nazwa | RefRegParams, RefAdvRegParams |
10.2. Separatory w nazwach
W środowisku STone stosuję podkreślnik _ jako separator w złożonych nazwach technicznych:
PumpSel— Pump SelectionPID_Params— PID ParametersHighPress_Ti— High Pressure Integral TimeAlarmStatus— Alarm StatusIsConfigChanged— z prefixemIs
11. Wzorce konwersji typów
STone wymaga jawnych konwersji typów za pomocą funkcji TO_xxx:
(*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. Obsługa błędów
12.1. Wzorzec sprawdzania wyników komunikacji
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. Wzorzec kaskadowej walidacji
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
Wzorzec: seria kroków z RETURN po każdym potencjalnym błędzie — fail-fast bez zagnieżdżonych IF-ów.
12.3. Warunkowe wywołania API
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;
Zasada: sprawdzaj feature-flags drivera/falownika przed wywołaniem opcjonalnych poleceń — dzięki temu kod działa z różnymi modelami i wersjami firmware.
13. Podsumowanie najlepszych praktyk
Architektura
- Jeden plik = jeden element (klasa, interfejs, funkcja)
- Interfejsy jako kontrakty — definiuj
INTERFACEprzed implementacją - Hierarchia: abstrakcyjna baza → klasy finalne — użytkownik widzi tylko
FINAL PUBLIC - Ukrywaj implementację — klasy bazowe jako
INTERNAL - Template Method Pattern —
METHOD ABSTRACT PROTECTED OnRunjako metoda abstrakcyjna i punkt rozszerzenia
Pamięć i wydajność
VAR_IN_OUT CONSTANTdla dużych struktur przekazywanych do metodREF_TOdo przechowywania referencji — unikaj kopiowania- Najmniejszy typ bazowy —
USINTzamiastUINTgdy wystarczy zakres 0-255 - Zakresy typów —
UINT(0..100)zamiast gołegoUINT
Konwencje
- Prefiksy:
I(interfejs),Base(klasa abstrakcyjna),Get/Set(metody),Is/En(bool) - Komentarze
(**...*)(IntelliSense) na każdym publicznym elemencie {REGION}do organizacji kodu w IDE- Kompilacja warunkowa
{IF DEF}do obsługi wariantów sprzętowych - Jawne konwersje typów —
TO_BYTE(),TO_REAL(),TO_USINT()
Bezpieczeństwo
- Walidacja
NULLprzed dereferencjąREF_TO - Fail-fast z
RETURNzamiast głębokiego zagnieżdżania - Detekcja zmiany konfiguracji — flaga
IsConfigChanged+Reset() - Dzielenie przez zero — sprawdzenie
<> 0przed dzieleniem
Autor: Hubert | Artykuł oparty na doświadczeniu z produkcyjnymi bibliotekami w środowisku STone.