Control¶
Control is truST's PID library: Control.PID for P, PI, PD and PID loops with modes, limits and
anti-windup, Control.LowPass for filtering a signal, and optional structures for dead time, for a
plant that changes with its operating point, and for two loops that disturb each other.
Library Guide¶
The Control library is truST's PID controller and what goes around it. PID is one block for P,
PI, PD and PID control, with Manual, Auto and Track modes, output and rate limits and two
anti-windup methods. LowPass filters a measurement or a setpoint. Three optional structures help
with harder plants: a Smith predictor for dead time, a gain scheduler for a plant that changes
with its operating point, and a static decoupler for two loops that disturb each other. A function
converts PID settings written in another vendor's form.
PID follows A Practical Guide to PID Controller Implementation exactly. The contracts are
specification 45 (docs/specs/45-control-pid.md) for the PID and the filter and specification 37
(docs/specs/37-control-advanced-structures.md) for the structures. The design reasons and the
measured results are in the Control System Engineering report
(docs/guides/CONTROL_PID_ENGINEERING_REPORT.md).
Use the library for any feedback loop in a program. The library runs a loop; it does not tune it:
Control Studio identifies the plant and tunes the loop. For a loop that an operator runs from the
HMI, the HMI Builder's Create PLC code writes this same PID as ordinary code with the variables
the Plant library's PID panel operates: the operator's mode, setpoint and manual output, and one
parameter record that the HMI's settings and Control Studio share.
Package Setup¶
[project]
include_paths = ["src"]
[dependencies]
Control = { path = "../../libraries/control", version = "0.1.0" }
The library depends on nothing else. Its names are in the namespace Control: write
USING Control; and use PID, or write Control.PID in full. Write enumeration values with their
type, for example PIDMode#Auto.
Design Rules¶
- One instance per loop and one per filtered signal; each keeps its own history.
- Call
Configurewith the complete parameter record,Initializeonce at startup, thenUpdateonce in every scan. Never callUpdatebefore bothConfigureandInitialize. Configurecopies the whole record at once and keeps the history. Calling it in every scan beforeUpdateis cheap and makes a changed record, such as a new tuning, take effect in the next scan.Initializesets the history directly and calculates nothing. Start from the current setpoint and measurement, zero rates, the current feedforward, and the actuator's output at startup for both output fields.- Pass the actual time since the previous
Update, in seconds and greater than zero, asCycleTime. The derivative, the integral, the rate limits, the anti-windup and the filter all use it, so the loop stays correct when the task's timing varies. - P, PI, PD and PID are settings of the one
PID. An integral gain of exactly zero selects the positional form, P or PD, whose output at zero error isBias. Any other integral gain selects the incremental form, PI or PID, which adds each scan's change to the previous output. - The PID never filters its inputs. Filter a noisy measurement with its own
LowPassbefore the PID, and a stepped setpoint too when the derivative uses the setpoint. - The limits act in every mode, and the anti-windup keeps the integral from running away while the output is limited.
- The library checks no values (specification 45, §8). Keep the parameters inside the ranges below and the inputs finite, and handle a failed signal and the safe output in your program.
- The structures never change the PID. They prepare its measurement (Smith predictor) or its
parameter record (gain scheduler), or turn its outputs into actuator commands (decoupler). Their
Configurereturns FALSE and keeps the previous valid settings when a value is out of range.
Blocks¶
| Block | Use | Methods |
|---|---|---|
PID |
P, PI, PD or PID loop with modes, limits and anti-windup | Configure, Initialize, Update, GetState |
LowPass |
first- or second-order filter for one signal | Configure, Initialize, Update |
SmithPredictorFOPDT |
gives the PID a measurement predicted past the plant's dead time | Configure, Initialize, Update |
PIDGainScheduler |
interpolates the PID's tuning over the plant's operating point | Configure, Initialize, Update |
StaticDecoupler2x2 |
turns two PID outputs into two actuator commands that cancel the loops' interaction | Configure, ToActuators, ToVirtual, Snapshot |
| Function | Use |
|---|---|
ConvertPIDForm |
converts PID settings between the parallel, ideal and series forms |
Common Types¶
Enumerations¶
| Type | Values |
|---|---|
PIDMode |
Auto := 0: calculate from setpoint and measurement. Manual := 1: the output is ManualOutput, still limited. Track := 2: restart the calculation from TrackingOutput in every scan |
PIDAntiWindup |
BackCalculation := 0, Combined := 1 |
FilterOrder |
FirstOrder := 0, SecondOrder := 1: one stage or two in a row |
PIDParameterForm |
Parallel := 0, Ideal := 1, Series := 2 |
PIDFormConversionStatus |
Valid := 0, InvalidDomain := 1, NonRepresentable := 2 |
PIDParameters¶
One complete parameter set. "Output" means the output's unit, for example %; "PV" means the measurement's unit, for example °C.
| Field | Unit | Meaning |
|---|---|---|
ProportionalGain |
output/PV | Kp: output change per unit of error, error = setpoint − measurement. Make all gains negative when more output lowers the measurement, as for cooling or a pump that drains |
IntegralGain |
output/(PV·s) | Ki = Kp/Ti, where the integral time Ti is how long a constant error takes for the integral to add as much as the proportional part. Exactly 0 selects P or PD |
DerivativeGain |
output·s/PV | Kd = Kp·Td, where the derivative time Td is how far ahead the derivative acts on the error's trend |
ProportionalSetpointWeight |
— | b, incremental form only: the share of a setpoint change the proportional part sees. 1 reacts fully; 0 lets only the integral follow, so a setpoint step gives no output kick |
DerivativeSetpointWeight |
— | c: the share of the setpoint's rate the derivative sees. 0 lets the derivative act on the measurement only |
Bias |
output | u0: the output at zero error in the positional form; unused with integral action |
OutputMinimum, OutputMaximum |
output | the output's absolute limits; OutputMinimum <= OutputMaximum |
RateMinimum, RateMaximum |
output/s | the fastest the output may fall and rise; RateMinimum <= 0 <= RateMaximum. Very large values switch rate limiting off |
TrackingTime |
s | Tt: how fast the anti-windup pulls the integral back while the output is limited; above 0 whenever IntegralGain <> 0. Equal to the cycle time, it sets the stored output to the limit at once (clamping); a value between Td and Ti is a common start |
AntiWindup |
— | BackCalculation or Combined, see below |
Wind-up: while the output is at a limit, the error persists and the integral keeps growing, so
the output stays at the limit long after the error has reversed. BackCalculation pulls the
stored output back towards the limit, faster the shorter TrackingTime. Combined first stops
any integration that pushes further into the limit, while integration away from it continues,
then pulls back the rest in the same way, at most to the limit in one scan.
PID records¶
| Type | Fields |
|---|---|
PIDInput |
Setpoint, ProcessValue (the measurement, already filtered), FeedForward (a known disturbance's effect, in output units, added to the output), ManualOutput, TrackingOutput, Mode, CycleTime (s) |
PIDInitialState |
Setpoint, SetpointRate, ProcessValue, ProcessValueRate, FeedForward, NominalOutput (the stored output before limiting), SaturatedOutput (after limiting) |
LowPassParameters |
TimeConstant (s, of each stage), Order |
Structure records¶
| Type | Fields |
|---|---|
SmithPredictorParameters |
ProcessGain, TimeConstant (s, above 0), DeadTime (s, 0 or more), OperatingInput, OperatingOutput, MaximumCorrection (0 or less: unbounded) |
SmithPredictorInitialState |
ModelOutput, ProcessValue |
SmithPredictorInput |
ManipulatedValue (the command actually sent), ProcessValue, CycleTime, Enabled |
SmithPredictorOutput |
PredictedProcessValue, DelayFreeModel, DelayedModel, Correction, Active, HistoryComplete |
PIDTuning |
the six tuning fields of PIDParameters: ProportionalGain, IntegralGain, DerivativeGain, ProportionalSetpointWeight, DerivativeSetpointWeight, TrackingTime |
PIDGainSchedulePoint |
OperatingPoint, Tuning |
PIDGainSchedulePoints |
ARRAY[0..7] OF PIDGainSchedulePoint |
PIDGainScheduleParameters |
Points (rising operating points), PointCount (2 to 8), CommonParameters (bias, limits and anti-windup for every point; its tuning fields are ignored), FilterTimeConstant (s; 0 for none) |
PIDGainScheduleOutput |
Parameters, FilteredOperatingPoint, LowerIndex, UpperIndex, Fraction, Clamped, Valid |
ControlVector2 |
First, Second |
ProcessGainMatrix2x2 |
G11, G12, G21, G22: the steady-state change of measurement 1 or 2 per change of actuator 1 or 2 |
StaticDecoupler2x2Parameters |
GainMatrix, ActuatorBias (the actuators' operating point), SingularityTolerance (above 0) |
StaticDecoupler2x2Status |
Configured, Determinant, Inverse11, Inverse12, Inverse21, Inverse22 |
PIDFormParameters |
Form, Proportional, Integral, Derivative |
PIDFormConversionResult |
Parameters, Status |
Interface Summary¶
PID.Configure(Parameters : PIDParameters)
PID.Initialize(State : PIDInitialState)
PID.Update(Input : PIDInput) : LREAL the limited output
PID.GetState() : PIDInitialState a copy of the history; changes nothing
LowPass.Configure(Parameters : LowPassParameters)
LowPass.Initialize(Input : LREAL) both stages start at Input
LowPass.Update(Input : LREAL, CycleTime : LREAL) : LREAL
SmithPredictorFOPDT.Configure(Parameters : SmithPredictorParameters) : BOOL
SmithPredictorFOPDT.Initialize(State : SmithPredictorInitialState)
SmithPredictorFOPDT.Update(Input : SmithPredictorInput) : SmithPredictorOutput
PIDGainScheduler.Configure(Parameters : PIDGainScheduleParameters) : BOOL
PIDGainScheduler.Initialize(OperatingPoint : LREAL)
PIDGainScheduler.Update(OperatingPoint : LREAL, CycleTime : LREAL) : PIDGainScheduleOutput
StaticDecoupler2x2.Configure(Parameters : StaticDecoupler2x2Parameters) : BOOL
StaticDecoupler2x2.ToActuators(VirtualCommand : ControlVector2) : ControlVector2
StaticDecoupler2x2.ToVirtual(ActuatorCommand : ControlVector2) : ControlVector2
StaticDecoupler2x2.Snapshot() : StaticDecoupler2x2Status
ConvertPIDForm(Input : PIDFormParameters, Target : PIDParameterForm) : PIDFormConversionResult
PID¶
Each Update, in this order:
- The scan's limits: the absolute limits, narrowed by the rate limits around the previous output.
- The rates of the setpoint and of the measurement.
- In Track, the stored output is replaced by
TrackingOutput. - In Manual, the output is
ManualOutput. Otherwise the positional form (P, PD) or the incremental form (PI, PID) calculates it; the incremental form adds the scan's proportional, integral, derivative and feedforward changes to the stored output, then applies the anti-windup. - The output is limited, stored with the scan's signals, and returned.
Changes of mode and parameters:
- With integral action, Manual to Auto does not jump: in Manual the PID keeps its history, so Auto continues from the manual output. Track to Auto continues from the tracked output.
- Without integral action, the output is recalculated at once and may jump at a change of mode or parameters, and Track gives the same output as Auto.
- A new parameter record never resets the history; with integral action, new gains alone do not make the output jump.
GetState().NominalOutputis the stored output after the anti-windup, not the request before limiting.
The observation extension, beside GetState: every Update ends by setting two internal members,
ModeValue : PIDMode to the mode the call ran in and ModeObserved : BOOL to TRUE
(specification 45, §3.1). Tools read them; Control Studio confirms Manual mode from them before it
changes a controller's parameters. The members are NON_RETAIN: after a restart ModeObserved is
FALSE until the first Update, also in an instance declared RETAIN, and an online change keeps
them until the next Update. They change nothing the controller calculates.
The simulation extension: a PID also has two internal members, HeldMeasurement : LREAL and
MeasurementHeld : BOOL, which only a simulated plant writes, through a pid: feedback target
that names the instance (specification 46, §13.2). While a measurement is held, every Update
works with it as Input.ProcessValue; the program's own variables keep what the program wrote, and
StateValue.ProcessValue shows the value the controller used. Without such a plant
MeasurementHeld stays FALSE and Update computes exactly as described above. The members are
NON_RETAIN: a restart removes a held measurement, also in an instance declared RETAIN, and so
does a newly loaded program, until the plant publishes again.
LowPass¶
Each stage moves towards its input by CycleTime / (TimeConstant + CycleTime / 2) of the
difference; the second order puts two such stages in a row. A filter delays the signal, and delay
makes a loop harder to control, so use the smallest time constant that quiets the output.
TimeConstant equal to half the cycle time passes the signal through unchanged. A changed time
constant continues from the stored value.
SmithPredictorFOPDT¶
Dead time is the delay between a change of the output and the first response of the measurement, for example while air travels along a duct to the sensor. A PID cannot see the effect of its output during that time and must be tuned slowly. The predictor runs a model of the plant with a gain, a time constant and a dead time (first order plus dead time, FOPDT) and gives the PID what the measurement would be now without the delay, corrected by the measured difference between plant and model.
In every scan, update it with the command actually sent to the actuator and with the measurement,
and pass PredictedProcessValue to the PID as its ProcessValue. When Active is FALSE it passes
the measurement through: it is not configured or initialized, Enabled is FALSE, or its 256 stored
samples do not reach back over the dead time (HistoryComplete FALSE; keep DeadTime below 255
cycle times). Use the plain measurement whenever the history is incomplete, the measurement is bad,
the plant is outside the model's range, or Correction sits at MaximumCorrection: a correction at
its bound means the model no longer fits.
PIDGainScheduler¶
A plant whose gain or speed changes with its operating point needs a different tuning at different
points. The scheduler holds up to eight tunings, each validated at its operating point, and
interpolates in a straight line between neighbouring points, on an operating point filtered with
FilterTimeConstant. Below the first point and above the last it keeps the end tuning and reports
Clamped.
In every scan, pass Parameters of its output to PID.Configure before PID.Update. When
Clamped is TRUE or Valid is FALSE, configure a fixed safe tuning instead and show that the loop
is outside its schedule. The PID keeps its history on every reconfiguration.
StaticDecoupler2x2¶
Two loops interact when each actuator moves both measurements: heating a climate chamber lowers its humidity, and an evaporative humidifier cools it. From the steady-state gains of both actuators on both measurements, the decoupler turns the two PID outputs, the virtual commands, into actuator commands that cancel the interaction in steady state.
Pass the two PID outputs to ToActuators and limit its result at the actuators; the decoupler
limits nothing itself. When an actuator limit binds, map the applied commands back with ToVirtual
and run both PIDs in Track with those values as TrackingOutput, so neither winds up. Configure
refuses a matrix that is nearly singular, one whose determinant relative to the size of its gains
is not above SingularityTolerance. It cancels steady-state interaction only; loops whose
interaction paths have very different speeds need more than this.
ConvertPIDForm¶
Controllers state their PID settings in different forms, with error e and output u:
- parallel, as
PIDuses: u = Kp·e + Ki·∫e dt + Kd·de/dt; - ideal: u = Kc·(e + (1/Ti)·∫e dt + Td·de/dt);
- series, or interacting: an ideal PI part multiplied by a PD part, as in many older controllers; in parallel form Kp = Kc·(1 + Td/Ti), Ki = Kc/Ti, Kd = Kc·Td.
Valid returns the converted values. InvalidDomain means the input is out of range: a zero proportional or
integral gain, Ti of 0 or less, Td below 0, gains of different signs, or a value that is not finite
or larger than 1e100.
NonRepresentable means the parallel values have no series form. On failure the values are 0.
Check the old controller's units first: some state Ti and Td in minutes, or the integral in
repeats per minute.
Examples¶
These are short teaching examples, each showing one point, with a task that runs every 100 ms
(CycleTime := 0.1); a task whose interval varies passes the measured time instead. Record fields
left out of an initializer keep their default, 0 for numbers. The complete, runnable project, with its I/O bindings, simulated
plant, modes and the steps for adapting it to another plant, is examples/control-pid-commissioning;
the control course in examples/control/ teaches the controller lesson by lesson.
A PI loop holds a temperature with a heater:
USING Control;
PROGRAM Heating
VAR
TemperatureC AT %IL0 : LREAL;
HeaterPct AT %QL0 : LREAL;
SetpointC : LREAL := 60.0;
Loop : PID;
Parameters : PIDParameters := (
ProportionalGain := 4.0,
IntegralGain := 0.2,
DerivativeGain := 0.0,
ProportionalSetpointWeight := 1.0,
DerivativeSetpointWeight := 0.0,
Bias := 0.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -1000.0,
RateMaximum := 1000.0,
TrackingTime := 10.0,
AntiWindup := PIDAntiWindup#BackCalculation
);
Start : PIDInitialState;
Input : PIDInput;
Initialized : BOOL;
END_VAR
Loop.Configure(Parameters := Parameters);
IF NOT Initialized THEN
Start.Setpoint := SetpointC;
Start.ProcessValue := TemperatureC;
Start.NominalOutput := HeaterPct;
Start.SaturatedOutput := HeaterPct;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
Input.Setpoint := SetpointC;
Input.ProcessValue := TemperatureC;
Input.Mode := PIDMode#Auto;
Input.CycleTime := 0.1;
HeaterPct := Loop.Update(Input := Input);
END_PROGRAM
The operator chooses Manual or Auto and sets the manual output on the HMI (AutoMode,
ManualPct). The PID runs in both modes. In Auto the manual value follows the output, so a switch to
Manual holds the valve where it is; in Manual the PID keeps its history, so the switch back to Auto
continues from the manual output. The rate limits of 10 %/s also slow the operator's changes:
USING Control;
PROGRAM Flow
VAR
FlowM3h AT %IL0 : LREAL;
ValvePct AT %QL0 : LREAL;
AutoMode : BOOL;
ManualPct : LREAL;
SetpointM3h : LREAL := 12.0;
Loop : PID;
Parameters : PIDParameters := (
ProportionalGain := 0.8,
IntegralGain := 0.4,
ProportionalSetpointWeight := 1.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -10.0,
RateMaximum := 10.0,
TrackingTime := 1.0,
AntiWindup := PIDAntiWindup#Combined
);
Start : PIDInitialState;
Input : PIDInput;
Initialized : BOOL;
END_VAR
Loop.Configure(Parameters := Parameters);
IF NOT Initialized THEN
Start.Setpoint := SetpointM3h;
Start.ProcessValue := FlowM3h;
Start.NominalOutput := ValvePct;
Start.SaturatedOutput := ValvePct;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
IF AutoMode THEN
Input.Mode := PIDMode#Auto;
ManualPct := ValvePct;
ELSE
Input.Mode := PIDMode#Manual;
END_IF;
Input.Setpoint := SetpointM3h;
Input.ProcessValue := FlowM3h;
Input.ManualOutput := ManualPct;
Input.CycleTime := 0.1;
ValvePct := Loop.Update(Input := Input);
END_PROGRAM
An interlock closes the steam valve of a heat exchanger while the product pump is stopped. The
program then sends 0 % to the valve and runs the PID in Track with that value as TrackingOutput:
in every scan the PID restarts its calculation from what the valve really gets, so it does not wind
up, and when the pump runs again it continues from the closed valve. Use the same pattern whenever
something else drives the actuator, such as a hand station, an override selector or a sequence.
Track needs integral action:
USING Control;
PROGRAM Exchanger
VAR
ProductC AT %IL0 : LREAL;
PumpRunning AT %IX0.0 : BOOL;
SteamValvePct AT %QL0 : LREAL;
SetpointC : LREAL := 72.0;
Loop : PID;
Parameters : PIDParameters := (
ProportionalGain := 3.0,
IntegralGain := 0.1,
ProportionalSetpointWeight := 1.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -1000.0,
RateMaximum := 1000.0,
TrackingTime := 15.0,
AntiWindup := PIDAntiWindup#BackCalculation
);
Start : PIDInitialState;
Input : PIDInput;
Request : LREAL;
Initialized : BOOL;
END_VAR
Loop.Configure(Parameters := Parameters);
IF NOT Initialized THEN
Start.Setpoint := SetpointC;
Start.ProcessValue := ProductC;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
Input.Setpoint := SetpointC;
Input.ProcessValue := ProductC;
IF PumpRunning THEN
Input.Mode := PIDMode#Auto;
ELSE
Input.Mode := PIDMode#Track;
Input.TrackingOutput := 0.0;
END_IF;
Input.CycleTime := 0.1;
Request := Loop.Update(Input := Input);
IF PumpRunning THEN
SteamValvePct := Request;
ELSE
SteamValvePct := 0.0;
END_IF;
END_PROGRAM
A noisy level measurement is smoothed by a second-order LowPass with 2 s per stage before it
reaches the PID:
USING Control;
PROGRAM Level
VAR
LevelM AT %IL0 : LREAL;
PumpPct AT %QL0 : LREAL;
SetpointM : LREAL := 1.5;
Smoothing : LowPass;
FilterSettings : LowPassParameters := (TimeConstant := 2.0, Order := FilterOrder#SecondOrder);
LevelFilteredM : LREAL;
Loop : PID;
Parameters : PIDParameters := (
ProportionalGain := 40.0,
IntegralGain := 2.0,
ProportionalSetpointWeight := 1.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -1000.0,
RateMaximum := 1000.0,
TrackingTime := 10.0,
AntiWindup := PIDAntiWindup#BackCalculation
);
Start : PIDInitialState;
Input : PIDInput;
Initialized : BOOL;
END_VAR
Smoothing.Configure(Parameters := FilterSettings);
Loop.Configure(Parameters := Parameters);
IF NOT Initialized THEN
Smoothing.Initialize(Input := LevelM);
Start.Setpoint := SetpointM;
Start.ProcessValue := LevelM;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
LevelFilteredM := Smoothing.Update(Input := LevelM, CycleTime := 0.1);
Input.Setpoint := SetpointM;
Input.ProcessValue := LevelFilteredM;
Input.Mode := PIDMode#Auto;
Input.CycleTime := 0.1;
PumpPct := Loop.Update(Input := Input);
END_PROGRAM
A loop moves from an old controller whose settings are in ideal form (Kc 2.5, Ti 120 s, Td 10 s). The program converts them once and stays in Manual if they are not valid:
USING Control;
PROGRAM Migrated
VAR
TemperatureC AT %IL0 : LREAL;
HeaterPct AT %QL0 : LREAL;
SetpointC : LREAL := 80.0;
OldSettings : PIDFormParameters := (
Form := PIDParameterForm#Ideal,
Proportional := 2.5,
Integral := 120.0,
Derivative := 10.0
);
Converted : PIDFormConversionResult;
SettingsValid : BOOL;
Loop : PID;
Parameters : PIDParameters := (
ProportionalSetpointWeight := 1.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -1000.0,
RateMaximum := 1000.0,
TrackingTime := 30.0,
AntiWindup := PIDAntiWindup#BackCalculation
);
Start : PIDInitialState;
Input : PIDInput;
Initialized : BOOL;
END_VAR
IF NOT Initialized THEN
Converted := ConvertPIDForm(Input := OldSettings, Target := PIDParameterForm#Parallel);
SettingsValid := Converted.Status = PIDFormConversionStatus#Valid;
Parameters.ProportionalGain := Converted.Parameters.Proportional;
Parameters.IntegralGain := Converted.Parameters.Integral;
Parameters.DerivativeGain := Converted.Parameters.Derivative;
END_IF;
Loop.Configure(Parameters := Parameters);
IF NOT Initialized THEN
Start.Setpoint := SetpointC;
Start.ProcessValue := TemperatureC;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
Input.Setpoint := SetpointC;
Input.ProcessValue := TemperatureC;
IF SettingsValid THEN
Input.Mode := PIDMode#Auto;
ELSE
Input.Mode := PIDMode#Manual;
Input.ManualOutput := 0.0;
END_IF;
Input.CycleTime := 0.1;
HeaterPct := Loop.Update(Input := Input);
END_PROGRAM
A pump fills a tank that drains through an outlet by gravity. The outflow grows with the square
root of the level, so the tank responds differently at low and at high level, and one tuning is
either slow at one end or restless at the other. Three tunings, each validated at its level, are
scheduled on the level. Outside the schedule the program uses the fixed tuning Safe, whose
limits and anti-windup also serve every point:
USING Control;
PROGRAM Tank
VAR
LevelM AT %IL0 : LREAL;
PumpPct AT %QL0 : LREAL;
SetpointM : LREAL := 1.0;
Low : PIDTuning := (ProportionalGain := 40.0, IntegralGain := 0.8, ProportionalSetpointWeight := 1.0, TrackingTime := 25.0);
Middle : PIDTuning := (ProportionalGain := 40.0, IntegralGain := 0.46, ProportionalSetpointWeight := 1.0, TrackingTime := 43.0);
High : PIDTuning := (ProportionalGain := 40.0, IntegralGain := 0.36, ProportionalSetpointWeight := 1.0, TrackingTime := 55.0);
Safe : PIDParameters := (
ProportionalGain := 40.0,
IntegralGain := 0.36,
ProportionalSetpointWeight := 1.0,
OutputMinimum := 0.0,
OutputMaximum := 100.0,
RateMinimum := -1000.0,
RateMaximum := 1000.0,
TrackingTime := 55.0,
AntiWindup := PIDAntiWindup#BackCalculation
);
Settings : PIDGainScheduleParameters;
Schedule : PIDGainScheduler;
Scheduled : PIDGainScheduleOutput;
ScheduleConfigured : BOOL;
OutsideSchedule : BOOL;
Loop : PID;
Start : PIDInitialState;
Input : PIDInput;
Initialized : BOOL;
END_VAR
IF NOT Initialized THEN
Settings.Points[0].OperatingPoint := 0.4;
Settings.Points[0].Tuning := Low;
Settings.Points[1].OperatingPoint := 1.2;
Settings.Points[1].Tuning := Middle;
Settings.Points[2].OperatingPoint := 2.0;
Settings.Points[2].Tuning := High;
Settings.PointCount := 3;
Settings.CommonParameters := Safe;
Settings.FilterTimeConstant := 5.0;
ScheduleConfigured := Schedule.Configure(Parameters := Settings);
Schedule.Initialize(OperatingPoint := LevelM);
END_IF;
Scheduled := Schedule.Update(OperatingPoint := LevelM, CycleTime := 0.1);
OutsideSchedule := NOT ScheduleConfigured OR NOT Scheduled.Valid OR Scheduled.Clamped;
IF OutsideSchedule THEN
Loop.Configure(Parameters := Safe);
ELSE
Loop.Configure(Parameters := Scheduled.Parameters);
END_IF;
IF NOT Initialized THEN
Start.Setpoint := SetpointM;
Start.ProcessValue := LevelM;
Loop.Initialize(State := Start);
Initialized := TRUE;
END_IF;
Input.Setpoint := SetpointM;
Input.ProcessValue := LevelM;
Input.Mode := PIDMode#Auto;
Input.CycleTime := 0.1;
PumpPct := Loop.Update(Input := Input);
END_PROGRAM
Validation¶
cargo build -p trust-dev
cargo test -p trust-runtime --test control_pid_library
cargo test -p trust-runtime --test control_pid_state_observation
cargo test -p trust-runtime --test control_pid_mode_observation
cargo test -p trust-runtime --test control_pid_performance
cargo test -p trust-runtime --test control_pid_mimo_performance
cargo test -p trust-runtime --test control_pid_commissioning_example
cargo test -p trust-runtime --test control_guide
cargo test -p trust-runtime --lib simulation::plant::pid_signal_tests
control_pid_library runs the library's Structured Text conformance tests in
crates/trust-runtime/tests/fixtures/control_pid with trust-dev, so build that first or set
TRUST_DEV_BIN: the PID's equations, modes, limits and anti-windup, the filter, and the three
structures and the conversion. control_pid_state_observation checks GetState, and
control_pid_mode_observation checks ModeValue and ModeObserved. The two
performance tests run closed loops against simulated plants; their results are simulation evidence,
not proof on a physical plant. control_pid_commissioning_example checks the commissioning project,
control_guide compiles and runs every example of this guide, and pid_signal_tests checks the
simulation extension against a simulated plant's pid: target.