Skip to content

Plant

Plant is truST's library of plant equipment: motors, pumps, fans, drives, heaters, valves, dampers and cylinders; instruments, totals and tanks; PID, cascade and on/off controllers; interlocks and permissives; equipment groups; sequences; and alarm and sequence managers. Each block comes with its symbol, panel and settings for the HMI Builder.

Library Guide

The Plant library holds a plant's equipment as function blocks: motors, pumps, fans, drives, heaters, valves, dampers and cylinders; analog and digital instruments, totals and tanks; an on/off controller; interlocks, permissives and protection; duty/standby, lead/lag and staged groups; sequences; and alarm and sequence managers. Each block comes with its symbol, its panel and its settings for the HMI Builder. A PID loop is no block of the library: it is ordinary code around the Control library's Control.PID, which Create PLC code writes and the library's PID panel operates. The specification is trust-plant-library.

Add the package

[project]
include_paths = ["src"]

[dependencies]
Plant = { path = "../../libraries/plant", version = "0.1.0" }

Adjust the path to the project. Plant uses the Alarm and Control libraries, which it declares itself. Write USING Plant; in a source to use the short names of its enumerations, such as E_Mode#Manual; the blocks are written with their namespace, such as Plant.FB_Pump.

How every device works

Who commands it. A device is in one of three modes:

Mode Who commands the device Typical use
Auto the program, through the device's Auto… inputs (AutoRun, AutoOpen, AutoSpeed, AutoPosition) normal operation
Manual the operator, through the device's methods (Start, Stop, Open, Close, SetSpeed, SetPosition) commissioning, local operation
OutOfService nobody: the device is held in its safe state, its supervision and alarms are off maintenance

A mode change never starts, stops or moves anything: a running pump switched to Manual keeps running. The program changes the mode with SetMode(Value := …); at the HMI an operator may switch between Auto and Manual and an engineer may take a device out of service. A method returns TRUE when the device accepts the command; a refused command changes nothing, and the device's CommandText says why ("Auto: switch to Manual", "Interlocked: Tank level high").

What stops it. Interlock := TRUE holds the device in its safe state in every mode, with InterlockText as the reason. Permissive := FALSE refuses a start but leaves a running device running. A protection trip (Trip) and a supervision failure (failed to start, failed to stop, end-position conflict) latch a Fault that stops the device until Reset is called after the cause has gone. Drives and heaters start at 0 % after a stop and after a restart.

What it reports. Every device has Mode, Manual, OutOfService, Fault, Interlocked, Blocked with BlockReason, StartPermit and CommandText. Its alarms are Alarm library alarms that the runtime lists in the alarm list without any further code.

Its settings. A device's settings are public RETAIN members with defaults, such as StartTimeout. Set them for one instance in its declaration: P101 : Plant.FB_Pump := (StartTimeout := T#10s);. An engineer can change them later from the device's panel; RETAIN keeps the changed value through a restart and a power loss.

A first complete program: a pump that starts when a tank is low, stops when it is full, and is interlocked by a high-high level switch. The hardware signals are ordinary variables, mapped to I/O in the configuration.

USING Plant;

PROGRAM Main
VAR
    LevelSignal : LREAL;
    HighHighSwitch : BOOL;
    PumpRunning : BOOL;
    PumpRun : BOOL;

    LT101 : Plant.FB_LevelInput := (Unit := 'm', RangeHigh := 4.0);
    LSHH101 : Plant.FB_LevelSwitch;
    P101 : Plant.FB_Pump := (StartTimeout := T#10s);
    Filling : BOOL;
END_VAR
    LT101(Raw := LevelSignal);
    LSHH101(Input := HighHighSwitch);

    IF LT101.Value < 1.0 THEN
        Filling := TRUE;
    ELSIF LT101.Value > 3.5 THEN
        Filling := FALSE;
    END_IF;

    P101(AutoRun := Filling, RunningFeedback := PumpRunning,
        Interlock := LSHH101.Value, InterlockText := 'Tank level high-high');
    PumpRun := P101.RunCommand;
END_PROGRAM

Equipment

Motors, pumps, fans, blowers, agitators and conveyors

FB_Motor, FB_Pump, FB_Fan, FB_Blower, FB_Agitator and FB_Conveyor behave alike; they differ in name and symbol.

Inputs Outputs Settings Commands
AutoRun, RunningFeedback, Trip, HasFeedback (TRUE), Interlock, InterlockText, Permissive, PermissiveText RunCommand (the contactor), Running, FailedToStart, FailedToStop, Tripped, ServiceDue; RETAIN Starts, RunHours, HoursSinceService StartTimeout (T#5s), StopTimeout (T#5s), ServiceInterval (4000 h, 0 for none) Start, Stop, Reset, ServiceDone

Without a running contact, declare HasFeedback := FALSE: Running then follows RunCommand and nothing is supervised.

Variable-speed motors, pumps, fans and dosing pumps

FB_VariableSpeedMotor, FB_VariableSpeedPump, FB_VariableSpeedFan and FB_DosingPump add a speed to the motor above.

Inputs Outputs Settings Commands
AutoSpeed (%), SpeedFeedback, HasSpeedFeedback SpeedSetpoint (the drive's reference, 0 % while stopped), Speed, ManualSpeed, SpeedDeviating MinSpeed (0 %), MaxSpeed (100 %), SpeedDeviation (5 %), DeviationTime (T#10s) SetSpeed(Value := …)

The speed is clamped to MinSpeed…MaxSpeed while running; a manual start from 0 % runs at MinSpeed.

Heater

FB_Heater adds AutoPower (%), outputs PowerOutput (%) and PulseOutput (time-proportioned for a solid-state relay, PulsePeriod), ManualPower, the setting MaxPower (100 %) and the command SetPower(Value := …). Like a drive it starts at 0 %.

Reversing and two-speed motors

FB_ReversingMotor has AutoForward, AutoReverse, feedback per direction, outputs ForwardCommand and ReverseCommand (never both) and waits ReversalDelay (T#2s) after a stop before the other direction; commands StartForward, StartReverse, Stop. FB_TwoSpeedMotor has AutoRun, AutoFast, outputs SlowCommand and FastCommand and drops both for ChangeoverDelay (T#1s) when the speed changes; commands StartSlow, StartFast, Stop. Both count starts, running hours and the hours since the last service like a motor.

Valves and dampers

FB_OnOffValve and FB_Damper:

Inputs Outputs Settings Commands
AutoOpen, OpenFeedback, ClosedFeedback, HasOpenFeedback, HasClosedFeedback (TRUE), FailOpen Solenoid, OpenCommand, Opened, Closed, Moving, FailedToOpen, FailedToClose, FeedbackConflict, OpenPermit, ClosePermit; RETAIN Operations OpenTimeout, CloseTimeout (T#10s) Open, Close, Reset

FailOpen says where the valve goes when its solenoid loses power. The safe state is that fail position, so an interlock, a fault or Out of service sends a fail-open valve open and a fail-closed valve closed. Moving away from the fail position counts as a start: it needs the permissive.

FB_MotorValve drives a motor-operated valve with open and close contactors, a travel timeout, torque and thermal trips, and StopTravel. FB_Cylinder drives a single- or double-acting cylinder with AutoExtend, end-position supervision and Extend / Retract commands.

Control valves and modulating dampers

FB_ControlValve and FB_ModulatingDamper:

Inputs Outputs Settings Commands
AutoPosition (%), PositionFeedback, HasFeedback, FailPosition (%) PositionSetpoint, Position, InPosition, ManualPosition, Deviating MinPosition (0 %), MaxPosition (100 %), PositionDeviation (5 %), DeviationTime (T#10s) SetPosition(Value := …)

An interlock or Out of service sends the valve to FailPosition. A deviation between setpoint and feedback raises a low-priority alarm; it does not latch a fault.

A drive and a valve together: a fail-closed control valve that the program opens step by step, and a variable-speed pump that runs at 60 % once the valve is open beyond 20 %.

USING Plant;

PROGRAM Main
VAR
    PumpRunning : BOOL;
    PumpRun : BOOL;
    PumpReference : LREAL;
    ValveFeedback : LREAL;
    ValveReference : LREAL;

    P201 : Plant.FB_VariableSpeedPump := (MinSpeed := 30.0, HasFeedback := FALSE);
    FV201 : Plant.FB_ControlValve := (HasFeedback := TRUE, PositionDeviation := 10.0);
    Opening : LREAL;
END_VAR
    IF Opening < 80.0 THEN
        Opening := Opening + 1.0;
    END_IF;
    FV201(AutoPosition := Opening, PositionFeedback := ValveFeedback, FailPosition := 0.0);
    P201(AutoRun := FV201.Position > 20.0, AutoSpeed := 60.0, RunningFeedback := PumpRunning);
    ValveReference := FV201.PositionSetpoint;
    PumpRun := P201.RunCommand;
    PumpReference := P201.SpeedSetpoint;
END_PROGRAM

Instruments

Analog instruments

FB_AnalogInput, and with their own symbols FB_LevelInput, FB_PressureInput, FB_TemperatureInput, FB_AnalyzerInput and FB_FlowInput:

Inputs Outputs Settings
Raw (the channel's value, for example in mA), SignalFault (the module's diagnosis) Value, Percent, Quality (E_Quality), Fault, HighHigh, High, Low, LowLow Unit, RawLow (4), RawHigh (20), RangeLow (0), RangeHigh (100), BadBelow (3.6), BadAbove (21), FilterTime, HighHighEnabled … LowLowEnabled, HighHighLimit … LowLowLimit, Deadband, AlarmDelay

Value is Raw scaled from RawLow…RawHigh to RangeLow…RangeHigh, filtered by Control.LowPass when FilterTime is set. A signal outside BadBelow…BadAbove, a module fault or a value that is not a number make the quality Bad: Value keeps its last good reading, one "Signal fault" alarm is raised and the limit alarms are suppressed. The defaults suit a 4–20 mA signal; for a signal already in engineering units set RawLow/RawHigh equal to the range and widen BadBelow/BadAbove.

FB_FlowInput adds Total (RETAIN), TotalPer (the time base of the flow unit, T#1h for m³/h) and ResetTotal.

Digital instruments, totals and counters

FB_DigitalInput, FB_LevelSwitch and FB_PressureSwitch debounce (Debounce) and optionally invert (Invert) a contact; with AlarmEnabled they raise an alarm with AlarmText while the value equals AlarmWhen. FB_Totalizer totals a rate (Rate, or Measurement taken from an instrument) into Total; FB_Counter counts the rising edges of Pulse up to Preset.

Weigh scale and tank

FB_WeighScale is an analog instrument with Net, TareWeight, Stable and Tare (accepted only while the weight is stable). FB_Tank turns a level instrument into level, percent and volume: connect the instrument to LevelInstrument and set Height and Capacity.

A tank with its level transmitter, a flow meter and a weigh scale:

USING Plant;

PROGRAM Main
VAR
    LevelSignal : LREAL;
    FlowSignal : LREAL;
    WeightSignal : LREAL;

    LT301 : Plant.FB_LevelInput := (Unit := 'm', RangeHigh := 3.0,
        HighEnabled := TRUE, HighLimit := 2.7, LowEnabled := TRUE, LowLimit := 0.3, Deadband := 0.05);
    T301 : Plant.FB_Tank := (Height := 3.0, Capacity := 12.0);
    FT301 : Plant.FB_FlowInput := (Unit := 'm³/h', RangeHigh := 50.0);
    WT301 : Plant.FB_WeighScale := (Unit := 'kg', RangeHigh := 500.0);
    Volume : LREAL;
END_VAR
    LT301(Raw := LevelSignal);
    T301(LevelInstrument := LT301);
    FT301(Raw := FlowSignal);
    WT301(Raw := WeightSignal);
    Volume := T301.Volume;
END_PROGRAM

Controllers

PID loop

A PID loop is ordinary code around the Control library's Control.PID, run through its Configure, Initialize and Update methods, with no block around it. Placing a PID controller in the HMI Builder and pressing Create PLC code writes it (specification 39, §10.5); written by hand it is the same code. Its values are variables beside the PID, named after it:

Variable Kept through a restart Holds
LIC401 (Control.PID), LIC401_Input, LIC401_Start, LIC401_Initialized no The PID, its input and start records, whether it has started
LIC401_Parameters (Control.PIDParameters) yes The gains and limits: what the HMI's settings and Control Studio change
LIC401_Manual yes The operator's mode: TRUE is Manual, FALSE Auto
LIC401_Setpoint, LIC401_ManualOutput, LIC401_Output yes The setpoint, the manual output and the output

In Auto the manual output follows the output, so Manual takes over where the loop is; from Manual the PID continues from the manual output, so Auto starts without a bump. While the measurement is bad the loop holds its output in Manual, and it starts again, without a bump, from the next good measurement. The three gains share one sign: negative gains make the output rise while the measurement is above the setpoint, as for a pump emptying a tank. The library's PID panel shows the loop's mode, measurement, setpoint, output and trend; the operator needs write rules for the mode, the setpoint and the manual output, which Create PLC code adds.

A level loop: the controller keeps a tank at 1.5 m with its outlet valve.

USING Plant;
USING Control;

PROGRAM Main
VAR
    LevelSignal : LREAL;
    ValveReference : LREAL;

    LT401 : Plant.FB_LevelInput := (Unit := 'm', RangeHigh := 3.0);
    LV401 : Plant.FB_ControlValve;
    LIC401 : Control.PID;
    LIC401_Input : Control.PIDInput;
    LIC401_Start : Control.PIDInitialState;
    LIC401_Initialized : BOOL;
END_VAR
VAR RETAIN
    LIC401_Parameters : Control.PIDParameters := (ProportionalGain := -20.0, IntegralGain := -1.0,
        DerivativeGain := 0.0, ProportionalSetpointWeight := 1.0, DerivativeSetpointWeight := 0.0,
        Bias := 0.0, OutputMinimum := 0.0, OutputMaximum := 100.0, RateMinimum := -1000000000.0,
        RateMaximum := 1000000000.0, TrackingTime := 1.0, AntiWindup := PIDAntiWindup#BackCalculation);
    LIC401_Manual : BOOL := FALSE;
    LIC401_Setpoint : LREAL := 1.5;
    LIC401_ManualOutput : LREAL;
    LIC401_Output : LREAL;
END_VAR
    LT401(Raw := LevelSignal);
    LIC401.Configure(Parameters := LIC401_Parameters);
    LIC401_Input.Setpoint := LIC401_Setpoint;
    LIC401_Input.ManualOutput := LIC401_ManualOutput;
    LIC401_Input.CycleTime := 0.1;
    IF LT401.GetQuality() = E_Quality#Good THEN
        LIC401_Input.ProcessValue := LT401.GetValue();
        IF NOT LIC401_Initialized THEN
            LIC401_Start.Setpoint := LIC401_Setpoint;
            LIC401_Start.ProcessValue := LIC401_Input.ProcessValue;
            LIC401_Start.NominalOutput := LIC401_Output;
            LIC401_Start.SaturatedOutput := LIC401_Output;
            LIC401.Initialize(State := LIC401_Start);
            LIC401_Initialized := TRUE;
        END_IF;
        IF LIC401_Manual THEN
            LIC401_Input.Mode := PIDMode#Manual;
        ELSE
            LIC401_Input.Mode := PIDMode#Auto;
        END_IF;
    ELSE
        LIC401_Input.Mode := PIDMode#Manual;
        LIC401_Initialized := FALSE;
    END_IF;
    LIC401_Output := LIC401.Update(Input := LIC401_Input);
    IF LIC401_Input.Mode = PIDMode#Auto THEN
        LIC401_ManualOutput := LIC401_Output;
    END_IF;
    LV401(AutoPosition := LIC401_Output, FailPosition := 0.0);
    ValveReference := LV401.PositionSetpoint;
END_PROGRAM

Cascade and on/off control

Two loops are a cascade when one loop's output is the other's setpoint: connect the outer controller to the inner one in the HMI Builder. Create PLC code then writes TIC201_Setpoint := TIC200_Output, and while the inner loop is in Manual the outer loop tracks the inner loop's measurement, so closing the cascade is bumpless. FB_OnOffController switches Demand with a Hysteresis around its Setpoint; wire Demand to a device's AutoRun.

Interlocks, permissives and protection

FB_Interlocks, FB_Permissives and FB_Protection each take up to eight conditions Condition1…Condition8, TRUE meaning healthy, with texts Text1…Text8. They give Healthy, Active, Text (the first condition that went bad) and FirstOut. Wire an interlock set's Active and Text to a device's Interlock and InterlockText, and a permissive set's Healthy and Text to Permissive and PermissiveText. An engineer may bypass an interlock or permissive condition for BypassTime (T#1h); protection cannot be bypassed. These are process conditions, not certified safety functions.

USING Plant;

PROGRAM Main
VAR
    OilPressureOk : BOOL;
    GuardClosed : BOOL;
    EmergencyStopOk : BOOL;
    MotorRunning : BOOL;
    MotorRun : BOOL;

    StartConditions : Plant.FB_Permissives;
    StopConditions : Plant.FB_Interlocks;
    M501 : Plant.FB_Conveyor;
END_VAR
    StartConditions(Condition1 := OilPressureOk, Text1 := 'Oil pressure low',
        Condition2 := GuardClosed, Text2 := 'Guard open');
    StopConditions(Condition1 := EmergencyStopOk, Text1 := 'Emergency stop pressed');
    M501(AutoRun := TRUE, RunningFeedback := MotorRunning,
        Permissive := StartConditions.Healthy, PermissiveText := StartConditions.Text,
        Interlock := StopConditions.Active, InterlockText := StopConditions.Text);
    MotorRun := M501.RunCommand;
END_PROGRAM

Groups

FB_DutyStandbyGroup (input Run: one member runs), FB_LeadLagGroup (input Required: that many members run) and FB_StagedGroup (input Demand in %: members are added and removed in equal stages with Hysteresis, MinRunTime and MinStopTime) command up to four members, Member1…Member4. A group starts the available member with the fewest running hours first (Preferred first when set), replaces a member that trips or is taken into Manual, starts and stops members StartDelay / StopDelay apart, and raises a shortfall alarm when it cannot run as many as it should. Its members stay in Auto: the group's demand reaches them through their Demand method and is added to their own AutoRun, so leave AutoRun unconnected for a member that only the group commands.

USING Plant;

PROGRAM Main
VAR
    Pump1Running : BOOL;
    Pump2Running : BOOL;
    Pump1Run : BOOL;
    Pump2Run : BOOL;
    CoolingWanted : BOOL := TRUE;

    P601 : Plant.FB_Pump;
    P602 : Plant.FB_Pump;
    Coolant : Plant.FB_DutyStandbyGroup := (StartDelay := T#1s);
    Alarms : Plant.FB_AlarmManager;
END_VAR
    Coolant(Run := CoolingWanted, Member1 := P601, Member2 := P602);
    P601(RunningFeedback := Pump1Running);
    P602(RunningFeedback := Pump2Running);
    Pump1Run := P601.RunCommand;
    Pump2Run := P602.RunCommand;
    Alarms(Source1 := P601, Source2 := P602, Source3 := Coolant);
END_PROGRAM

Sequences

A sequence extends FB_Sequence. Its body calls Execute(), which applies the operator's commands (Start, Hold, Resume, Stop, Abort, Reset) and the step timer, and returns TRUE while the sequence is running; the steps are a CASE on CurrentStep using the helpers GoTo(Number, Name), Wait(Text), Complete(), Fail(Text) and StepTimeout(Limit, Text). While it is stopping the application brings the plant to rest and calls StopDone(). The sequence's outputs command equipment through the devices' Auto… inputs. Nothing of a sequence is retained: after a restart it is idle.

USING Plant;

FUNCTION_BLOCK MixSequence EXTENDS Plant.FB_Sequence
VAR_INPUT
    Level : LREAL;
    FullLevel : LREAL := 2.0;
    MixingTime : TIME := T#2m;
END_VAR
VAR_OUTPUT
    FillValve : BOOL;
    Mixer : BOOL;
END_VAR
    FillValve := FALSE;
    Mixer := FALSE;
    IF Execute() THEN
        CASE CurrentStep OF
            0:
                GoTo(Number := 10, Name := 'Fill');
            10:
                FillValve := TRUE;
                Wait(Text := 'Filling');
                IF Level >= FullLevel THEN
                    GoTo(Number := 20, Name := 'Mix');
                ELSE
                    StepTimeout(Limit := T#20m, Text := 'The tank did not fill within 20 min');
                END_IF;
            20:
                Mixer := TRUE;
                IF StepTime >= MixingTime THEN
                    Complete();
                END_IF;
        END_CASE;
    ELSIF State = E_SequenceState#Stopping THEN
        StopDone();
    END_IF;
END_FUNCTION_BLOCK

PROGRAM Main
VAR
    LevelSignal : LREAL;
    ValveOut : BOOL;
    MixerOut : BOOL;

    LT701 : Plant.FB_LevelInput := (Unit := 'm', RangeHigh := 3.0);
    XV701 : Plant.FB_OnOffValve := (HasOpenFeedback := FALSE, HasClosedFeedback := FALSE);
    M701 : Plant.FB_Agitator := (HasFeedback := FALSE);
    Batch : MixSequence;
    Sequences : Plant.FB_SequenceManager;
END_VAR
    LT701(Raw := LevelSignal);
    Batch(Level := LT701.Value);
    Sequences(Sequence1 := Batch);
    XV701(AutoOpen := Batch.FillValve);
    M701(AutoRun := Batch.Mixer);
    ValveOut := XV701.Solenoid;
    MixerOut := M701.RunCommand;
END_PROGRAM

Alarm and sequence managers

FB_AlarmManager takes up to sixteen alarm sources (Source1…Source16, any device, instrument or group) and gives ActiveCount, UnacknowledgedCount and Horn, with AcknowledgeAll and Silence. The HMI's alarm list does not need it: the runtime lists every alarm. Use it for a horn and for counts on an overview. FB_SequenceManager observes up to eight sequences (Sequence1…Sequence8) and gives RunningCount, HeldCount and FailedCount, with StopAll and AbortAll.

In the HMI Builder

Each block has a component definition in libraries/plant/hmi/pairs and a panel in libraries/plant/hmi/panels. Drag a component from the Equipment, Instruments, Controllers or Groups and sequences sections onto a page; the inspector shows its tag, its hardware status and its key settings with their real values. Connect hardware opens Devices & Connections for it. Create PLC code then writes one declaration and one call per new device into marked regions of the program; Create HMI does the opposite and places a symbol for each declared device.

At run time the device's panel shows its state, its mode, Start and Stop (greyed in Auto, with the reason beside them) and, for an engineer, its settings.

Restart and power loss

A device's mode, counters and settings are RETAIN: they survive a warm restart and a power loss, also for an instance inside another function block. Commands, faults and alarms are not retained: after a restart a device is stopped, with no fault, in the mode it had. A cold restart starts every device from its declaration.