wrps-demo-kit/03-plc/as-built/program.st
Clio Liu 55279ca78f docs(plc): as-built copy, versions, and a getting-started guide
Adds what the folder was missing for someone picking it up cold.

as-built/  the STruC++ output copied out of the live container - the C++
           the PLC is actually executing, plus program.st and the Modbus
           buffer config. The running image was made with docker commit
           and exists in no registry, so this is the only other copy of
           the compiled form.

           Verified while copying: the deployed program.st matches
           build/wrps.st generated from src/. Identical POU structure,
           ZERO differences in non-declaration lines. src/ is genuinely
           canonical and the running PLC agrees with it.

VERSIONS.md         every version read from the running system, not from
                    documentation: runtime v4.1.10, STruC++ 0.6.2, Editor
                    4.2.11, Debian 12, g++ 12.2, pymodbus 3.11.2, CI
                    Server R1.03. Plus which plugins are enabled -
                    ethercat is on for no reason - and a v3-vs-v4 table,
                    since most OpenPLC guidance online is for v3 and the
                    %MW HR1024 change silently produces wrong data.

GETTING-STARTED.md  three questions answered in a page: how to modify the
                    program, how to stand up a new PLC container from
                    scratch, how to move this one. Includes the compose
                    file, the Editor steps that are not generated and are
                    always missed, and a symptom-to-document table.
2026-09-02 16:48:30 +10:00

1687 lines
52 KiB
Smalltalk

FUNCTION_BLOCK FB_DUTY_SELECT
VAR_INPUT
Available : ARRAY [1..3] OF BOOL;
RunHours : ARRAY [1..3] OF REAL;
ServiceDue : ARRAY [1..3] OF BOOL;
RunningNow : ARRAY [1..3] OF BOOL;
PumpsRequired : INT;
END_VAR
VAR_OUTPUT
RunRequest : ARRAY [1..3] OF BOOL;
DutyPump : INT;
END_VAR
VAR
SERVICE_PENALTY : REAL := 1000000.0;
Rank : ARRAY [1..3] OF INT;
Used : ARRAY [1..3] OF BOOL;
Sel : ARRAY [1..3] OF BOOL;
i : INT;
k : INT;
best : INT;
bestKey : REAL;
key : REAL;
nRanked : INT;
slots : INT;
cnt : INT;
END_VAR
(* --- 1. Reset working state --------------------------------------- *)
FOR i := 1 TO 3 DO
Used[i] := FALSE;
Sel[i] := FALSE;
Rank[i] := 0;
END_FOR;
nRanked := 0;
(* --- 2. Build the ranked list by repeated selection.
Scanning i ascending with a strict "<" test means an equal key
never displaces an earlier pump, which is rule 4. ------------ *)
FOR k := 1 TO 3 DO
best := 0;
bestKey := 0.0;
FOR i := 1 TO 3 DO
IF Available[i] AND NOT Used[i] THEN
key := RunHours[i];
IF ServiceDue[i] THEN
key := key + SERVICE_PENALTY;
END_IF;
IF (best = 0) OR (key < bestKey) THEN
best := i;
bestKey := key;
END_IF;
END_IF;
END_FOR;
IF best > 0 THEN
nRanked := nRanked + 1;
Rank[nRanked] := best;
Used[best] := TRUE;
END_IF;
END_FOR;
(* --- 3. Clamp the demand ------------------------------------------ *)
slots := PumpsRequired;
IF slots < 0 THEN
slots := 0;
END_IF;
IF slots > 3 THEN
slots := 3;
END_IF;
(* --- 4. Pass A: units already running keep their slots.
Walking in rank order means that if the demand has dropped it
is the worst-ranked running unit that loses its slot. -------- *)
cnt := 0;
FOR k := 1 TO nRanked DO
i := Rank[k];
IF RunningNow[i] AND (cnt < slots) THEN
Sel[i] := TRUE;
cnt := cnt + 1;
END_IF;
END_FOR;
(* --- 5. Pass B: fill the slots that remain, by rank --------------- *)
FOR k := 1 TO nRanked DO
i := Rank[k];
IF (NOT Sel[i]) AND (cnt < slots) THEN
Sel[i] := TRUE;
cnt := cnt + 1;
END_IF;
END_FOR;
(* --- 6. Publish. DutyPump is the best-ranked selected unit. ------- *)
DutyPump := 0;
FOR k := 1 TO nRanked DO
i := Rank[k];
IF Sel[i] AND (DutyPump = 0) THEN
DutyPump := i;
END_IF;
END_FOR;
FOR i := 1 TO 3 DO
RunRequest[i] := Sel[i];
END_FOR;
END_FUNCTION_BLOCK
FUNCTION_BLOCK FB_HEADROOM
VAR_INPUT
Level : REAL;
Inflow : REAL;
TotalDischarge : REAL;
END_VAR
VAR_OUTPUT
InflowFilt : REAL;
NetInflow : REAL;
VolToSpill : REAL;
VolToLSHH : REAL;
TimeToSpill : INT;
TimeToLSHH : INT;
END_VAR
VAR
SCAN_S : REAL := 0.1;
TAU_S : REAL := 30.0;
AREA_M2 : REAL := 120.0;
SPILL_M : REAL := 6.000;
LSHH_M : REAL := 5.500;
MIN_NET : REAL := 0.5;
NO_TIME : INT := 32767;
MAX_TIME : REAL := 32767.0;
Primed : BOOL := FALSE;
t : REAL;
END_VAR
(* --- Inflow filter. First-order lag, 30 s.
Primed on the first scan rather than ramping from zero: without
this the reference figure in test 14 would take ~2 minutes to
settle and would not reproduce exactly on demand. ---------------- *)
IF NOT Primed THEN
InflowFilt := Inflow;
Primed := TRUE;
ELSE
InflowFilt := InflowFilt + (Inflow - InflowFilt) * SCAN_S / TAU_S;
END_IF;
NetInflow := InflowFilt - TotalDischarge;
VolToSpill := (SPILL_M - Level) * AREA_M2;
VolToLSHH := (LSHH_M - Level) * AREA_M2;
IF VolToSpill < 0.0 THEN
VolToSpill := 0.0;
END_IF;
IF VolToLSHH < 0.0 THEN
VolToLSHH := 0.0;
END_IF;
(* --- Time to spill weir ------------------------------------------- *)
IF NetInflow <= MIN_NET THEN
TimeToSpill := NO_TIME;
ELSE
t := VolToSpill * 1000.0 / NetInflow;
IF t >= MAX_TIME THEN
TimeToSpill := NO_TIME;
ELSIF t < 0.0 THEN
TimeToSpill := 0;
ELSE
TimeToSpill := REAL_TO_INT(t);
END_IF;
END_IF;
(* --- Time to LSHH -------------------------------------------------- *)
IF NetInflow <= MIN_NET THEN
TimeToLSHH := NO_TIME;
ELSE
t := VolToLSHH * 1000.0 / NetInflow;
IF t >= MAX_TIME THEN
TimeToLSHH := NO_TIME;
ELSIF t < 0.0 THEN
TimeToLSHH := 0;
ELSE
TimeToLSHH := REAL_TO_INT(t);
END_IF;
END_IF;
END_FUNCTION_BLOCK
FUNCTION_BLOCK FB_LEVEL_CTRL
VAR_INPUT
Level : REAL;
Setpoint : REAL;
Enable : BOOL;
MinSpeed : REAL;
MaxSpeed : REAL;
END_VAR
VAR_OUTPUT
Speed : REAL;
END_VAR
VAR
SCAN_S : REAL := 0.1;
KP : REAL := 12.0;
TI : REAL := 120.0;
Integ : REAL := 38.0;
Err : REAL;
Raw : REAL;
Integrate : BOOL;
END_VAR
IF NOT Enable THEN
(* Hold at minimum and reset the integrator, so that a restart does
not inherit stale integral action. *)
Integ := MinSpeed;
Speed := MinSpeed;
ELSE
Err := Level - Setpoint;
Raw := KP * Err + Integ;
(* Anti-windup: freeze the integrator whenever the output is
clamped, except when the error would drive it back into range. *)
Integrate := FALSE;
IF (Raw > MinSpeed) AND (Raw < MaxSpeed) THEN
Integrate := TRUE;
ELSIF (Raw >= MaxSpeed) AND (Err < 0.0) THEN
Integrate := TRUE;
ELSIF (Raw <= MinSpeed) AND (Err > 0.0) THEN
Integrate := TRUE;
END_IF;
IF Integrate THEN
Integ := Integ + (KP / TI) * Err * SCAN_S;
END_IF;
Raw := KP * Err + Integ;
IF Raw > MaxSpeed THEN
Speed := MaxSpeed;
ELSIF Raw < MinSpeed THEN
Speed := MinSpeed;
ELSE
Speed := Raw;
END_IF;
END_IF;
END_FUNCTION_BLOCK
FUNCTION_BLOCK FB_PUMP
VAR_INPUT
RunRequest : BOOL;
SpeedRef : REAL;
ThermalOK : BOOL;
SealLeak : BOOL;
Vibration : REAL;
DischPressure : REAL;
ResetTrip : BOOL;
Lockout : BOOL;
MinOffBypass : BOOL;
ServiceInterval : REAL;
ResetHours : BOOL;
END_VAR
VAR_OUTPUT
RunCmd : BOOL;
Running : BOOL;
Available : BOOL;
Tripped : BOOL;
State : INT;
RunHours : REAL;
ServiceDue : BOOL;
VibAlarm : BOOL;
SealAlarm : BOOL;
END_VAR
VAR
SCAN_S : REAL := 0.1;
VIB_ALARM : REAL := 7.1;
VIB_TRIP : REAL := 11.0;
NOFLOW_KPA : REAL := 150.0;
MinRunTmr : TON;
MinOffTmr : TON;
NoFlowTmr : TON;
HasRun : BOOL;
StartOK : BOOL;
END_VAR
(* --- 1. Trip reset. Runs first so that a reset issued while the
initiating condition is still present re-trips immediately
rather than latching clear. --------------------------------- *)
IF ResetTrip THEN
Tripped := FALSE;
END_IF;
(* --- 2. Trip conditions. All latch; they clear only on reset. ---- *)
IF NOT ThermalOK THEN
Tripped := TRUE; (* TE-31x, section 5 *)
END_IF;
IF Vibration > VIB_TRIP THEN
Tripped := TRUE; (* VE-31x > 11.0 mm/s *)
END_IF;
(* No-flow: 20 s after RunCmd goes true, low discharge pressure trips
the unit. Monitored continuously once the window has elapsed, not
sampled once, so a loss of flow while running is also caught. *)
NoFlowTmr(IN := RunCmd, PT := T#20s);
IF NoFlowTmr.Q AND (DischPressure < NOFLOW_KPA) THEN
Tripped := TRUE;
END_IF;
(* --- 3. Alarms that do not affect availability, section 4.1 ------- *)
VibAlarm := Vibration > VIB_ALARM;
SealAlarm := SealLeak;
(* --- 4. Availability. A seal leak is deliberately absent here:
it raises an alarm only, per WRPS-PRO-001 5.5. --------------- *)
Available := ThermalOK AND NOT Tripped AND NOT Lockout;
(* --- 5. Minimum run / minimum off timers.
MinOff is gated on HasRun so that a cold start is not blocked
for 5 minutes after a runtime restart. ---------------------- *)
MinRunTmr(IN := RunCmd, PT := T#5m);
MinOffTmr(IN := (NOT RunCmd) AND HasRun, PT := T#5m);
StartOK := (NOT HasRun) OR MinOffTmr.Q OR MinOffBypass;
(* --- 6. Run command ---------------------------------------------- *)
IF Tripped OR Lockout OR NOT ThermalOK THEN
RunCmd := FALSE;
ELSIF RunCmd THEN
(* running: honour minimum run before accepting a stop *)
IF (NOT RunRequest) AND MinRunTmr.Q THEN
RunCmd := FALSE;
END_IF;
ELSE
IF RunRequest AND StartOK THEN
RunCmd := TRUE;
HasRun := TRUE;
END_IF;
END_IF;
Running := RunCmd;
(* --- 7. Run hours. Scan-time increments while running only. ------ *)
IF ResetHours THEN
RunHours := 0.0;
END_IF;
IF Running THEN
RunHours := RunHours + SCAN_S / 3600.0;
END_IF;
ServiceDue := RunHours >= ServiceInterval;
(* --- 8. Published state, section 3.2 ------------------------------ *)
IF Tripped THEN
State := 6; (* Tripped *)
ELSIF Lockout THEN
State := 7; (* Maintenance lockout *)
ELSIF NOT ThermalOK THEN
State := 0; (* Unavailable *)
ELSIF Running AND NOT RunRequest THEN
State := 4; (* Min-run inhibit *)
ELSIF RunCmd AND NOT NoFlowTmr.Q THEN
State := 2; (* Start delay *)
ELSIF Running THEN
State := 3; (* Running *)
ELSIF HasRun AND NOT MinOffTmr.Q THEN
State := 5; (* Min-off inhibit *)
ELSE
State := 1; (* Available, stopped *)
END_IF;
END_FUNCTION_BLOCK
PROGRAM CONTROL
VAR_EXTERNAL
g_LevelRaw_mm : INT;
g_Level_mm : INT;
g_Level_m : REAL;
g_Inflow_Lps : REAL;
g_Disch_Lps : REAL;
g_PumpP_kPa : ARRAY [1..3] OF REAL;
g_Vib_mms : ARRAY [1..3] OF REAL;
g_LSHH : BOOL;
g_LSLL_Wet : BOOL;
g_SpillDetected : BOOL;
g_ThermalOK : ARRAY [1..3] OF BOOL;
g_SealLeak : ARRAY [1..3] OF BOOL;
g_MainsOK : BOOL;
g_cmd_Mode : INT;
g_cmd_Word : INT;
g_cmd_Param : INT;
g_sp_Level : INT;
g_sp_StartDuty : INT;
g_sp_StartP2 : INT;
g_sp_StartP3 : INT;
g_sp_StopAll : INT;
g_sp_HighAlarm : INT;
g_sp_MinSpeed : INT;
g_sp_ServiceHrs : INT;
g_o_RunCmd : ARRAY [1..3] OF BOOL;
g_o_Running : ARRAY [1..3] OF BOOL;
g_o_Available : ARRAY [1..3] OF BOOL;
g_o_Tripped : ARRAY [1..3] OF BOOL;
g_o_InAuto : BOOL;
g_o_HighLevel : BOOL;
g_o_SpillActive : BOOL;
g_o_Level_mm : INT;
g_o_Inflow_x10 : INT;
g_o_Disch_x10 : INT;
g_o_PumpsRun : INT;
g_o_Speed_x10 : INT;
g_o_TimeToSpill : INT;
g_o_TimeToLSHH : INT;
g_o_NetAccum : INT;
g_o_RunHours : ARRAY [1..3] OF INT;
g_o_VolToSpill : INT;
g_o_StationState : INT;
g_o_PumpState : ARRAY [1..3] OF INT;
g_o_DutyPump : INT;
g_o_AlarmWord : INT;
g_o_CmdAck : INT;
END_VAR
VAR
SPILL_MM : INT := 6000;
LEVEL_MAX_MM : INT := 7000;
HARD_MIN_HZ : REAL := 38.0;
HARD_MAX_HZ : REAL := 50.0;
Pump1 : FB_PUMP;
Pump2 : FB_PUMP;
Pump3 : FB_PUMP;
Duty : FB_DUTY_SELECT;
LvlCtl : FB_LEVEL_CTRL;
Head : FB_HEADROOM;
v_Mode : INT := 1;
v_SpLevel : INT := 4200;
v_StartDuty : INT := 4000;
v_StartP2 : INT := 4500;
v_StartP3 : INT := 5000;
v_StopAll : INT := 1000;
v_HighAlarm : INT := 5200;
v_MinSpeed : INT := 380;
v_ServiceHrs : INT := 4000;
SpRejected : BOOL;
SpOK : BOOL;
CmdBusy : BOOL;
ResetTrip : ARRAY [1..3] OF BOOL;
ResetHours : ARRAY [1..3] OF BOOL;
Lockout : ARRAY [1..3] OF BOOL;
AckAlarms : BOOL;
p : INT;
PumpsRequired : INT;
PumpsAllowed : INT;
StaggerTmr : TON;
StaggerArm : BOOL;
DryRun : BOOL;
DryLockout : BOOL;
LevelRangeFault : BOOL;
LevelFrozen : BOOL;
LevelFault : BOOL;
LevelRef : INT;
LevelMoved : BOOL;
FrozenTmr : TON;
AnyRunning : BOOL;
Avail : ARRAY [1..3] OF BOOL;
Hours : ARRAY [1..3] OF REAL;
SvcDue : ARRAY [1..3] OF BOOL;
RunNow : ARRAY [1..3] OF BOOL;
Req : ARRAY [1..3] OF BOOL;
Speed : REAL;
MinSpeedHz : REAL;
SpLevel_m : REAL;
HighLevel : BOOL;
PumpsRun : INT;
Alarm : DINT;
i : INT;
r : REAL;
Primed : BOOL := FALSE;
END_VAR
(* =====================================================================
Step 1 - read and clamp setpoints, section 2.3
Every setpoint is validated as a set, not individually: the start
levels only make sense in order. A rejected write holds the last
good value and raises bit 15 rather than acting on it.
===================================================================== *)
(* Mode *)
IF (g_cmd_Mode = 1) OR (g_cmd_Mode = 2) THEN
v_Mode := g_cmd_Mode;
ELSE
SpRejected := TRUE;
END_IF;
(* Level setpoints. A start level at or above the spill weir must
never be accepted, section 2.3. *)
SpOK := TRUE;
IF (g_sp_StopAll < 0) OR (g_sp_StopAll >= g_sp_StartDuty) THEN
SpOK := FALSE;
END_IF;
IF (g_sp_StartDuty >= g_sp_StartP2) OR (g_sp_StartDuty >= SPILL_MM) THEN
SpOK := FALSE;
END_IF;
IF (g_sp_StartP2 >= g_sp_StartP3) OR (g_sp_StartP2 >= SPILL_MM) THEN
SpOK := FALSE;
END_IF;
IF (g_sp_StartP3 >= SPILL_MM) THEN
SpOK := FALSE;
END_IF;
IF (g_sp_Level <= g_sp_StopAll) OR (g_sp_Level >= SPILL_MM) THEN
SpOK := FALSE;
END_IF;
IF (g_sp_HighAlarm <= 0) OR (g_sp_HighAlarm > SPILL_MM) THEN
SpOK := FALSE;
END_IF;
IF SpOK THEN
v_SpLevel := g_sp_Level;
v_StartDuty := g_sp_StartDuty;
v_StartP2 := g_sp_StartP2;
v_StartP3 := g_sp_StartP3;
v_StopAll := g_sp_StopAll;
v_HighAlarm := g_sp_HighAlarm;
ELSE
SpRejected := TRUE;
END_IF;
(* Minimum drive speed, Hz x 10, bounded by the hard physical limits *)
IF (g_sp_MinSpeed >= 380) AND (g_sp_MinSpeed <= 500) THEN
v_MinSpeed := g_sp_MinSpeed;
ELSE
SpRejected := TRUE;
END_IF;
(* Service interval *)
IF g_sp_ServiceHrs > 0 THEN
v_ServiceHrs := g_sp_ServiceHrs;
ELSE
SpRejected := TRUE;
END_IF;
MinSpeedHz := INT_TO_REAL(v_MinSpeed) / 10.0;
IF MinSpeedHz < HARD_MIN_HZ THEN
MinSpeedHz := HARD_MIN_HZ;
END_IF;
SpLevel_m := INT_TO_REAL(v_SpLevel) / 1000.0;
(* =====================================================================
Step 2 - command word and acknowledge, section 3.3
Executes on the rising edge of a non-zero %MW1, echoes the value to
%QW20, then takes no further action until %MW1 returns to 0.
===================================================================== *)
(* one-shot pulses, consumed by the FB_PUMP calls later this scan *)
FOR i := 1 TO 3 DO
ResetTrip[i] := FALSE;
ResetHours[i] := FALSE;
END_FOR;
AckAlarms := FALSE;
IF (g_cmd_Word <> 0) AND NOT CmdBusy THEN
CmdBusy := TRUE;
p := g_cmd_Param;
CASE g_cmd_Word OF
1: (* reset all trips *)
FOR i := 1 TO 3 DO
ResetTrip[i] := TRUE;
END_FOR;
(* the dry run lockout is manual-reset and only clears once
the level has actually recovered, section 5 *)
IF g_Level_mm > v_StopAll THEN
DryLockout := FALSE;
END_IF;
2: (* reset trip on pump in %MW2 *)
IF (p >= 1) AND (p <= 3) THEN
ResetTrip[p] := TRUE;
END_IF;
3: (* lock out pump in %MW2 *)
IF (p >= 1) AND (p <= 3) THEN
Lockout[p] := TRUE;
END_IF;
4: (* release lockout on pump in %MW2 *)
IF (p >= 1) AND (p <= 3) THEN
Lockout[p] := FALSE;
END_IF;
5: (* reset run hours on pump in %MW2 - service done *)
IF (p >= 1) AND (p <= 3) THEN
ResetHours[p] := TRUE;
END_IF;
6: (* acknowledge alarms *)
AckAlarms := TRUE;
SpRejected := FALSE;
END_CASE;
g_o_CmdAck := g_cmd_Word;
ELSIF g_cmd_Word = 0 THEN
CmdBusy := FALSE;
g_o_CmdAck := 0;
END_IF;
(* =====================================================================
Level signal integrity, section 5
A frozen transmitter reading a plausible value is the failure that
actually causes spills, and a range check alone cannot see it.
===================================================================== *)
LevelRangeFault := (g_LevelRaw_mm < 0) OR (g_LevelRaw_mm > LEVEL_MAX_MM);
IF NOT Primed THEN
LevelRef := g_LevelRaw_mm;
Primed := TRUE;
END_IF;
IF ABS(g_LevelRaw_mm - LevelRef) > 1 THEN
LevelRef := g_LevelRaw_mm;
LevelMoved := TRUE;
ELSE
LevelMoved := FALSE;
END_IF;
AnyRunning := Pump1.Running OR Pump2.Running OR Pump3.Running;
FrozenTmr(IN := AnyRunning AND NOT LevelMoved, PT := T#10m);
LevelFrozen := FrozenTmr.Q;
LevelFault := LevelRangeFault OR LevelFrozen;
(* =====================================================================
Step 3 - determine PumpsRequired from level
The band between StopAll and StartDuty holds the previous value.
That hysteresis is the whole point; it is never recomputed from
scratch.
===================================================================== *)
IF NOT LevelFault THEN
IF g_Level_mm >= v_StartP3 THEN
PumpsRequired := 3;
ELSIF g_Level_mm >= v_StartP2 THEN
PumpsRequired := 2;
ELSIF g_Level_mm >= v_StartDuty THEN
PumpsRequired := 1;
ELSIF g_Level_mm <= v_StopAll THEN
PumpsRequired := 0;
END_IF;
(* otherwise: hold *)
ELSE
(* Fall back to discrete level control, section 5. LSHH and LSLL
are independent instruments and remain trustworthy. *)
IF g_LSHH THEN
PumpsRequired := 3;
END_IF;
(* otherwise: hold, and let the LSLL override below stop the
station if the well is actually dry *)
END_IF;
(* =====================================================================
Step 4 - LSHH override. Start all available, bypass min-off.
===================================================================== *)
IF g_LSHH THEN
PumpsRequired := 3;
END_IF;
(* =====================================================================
Step 5 - LSLL override. Fail-safe: the instrument reads TRUE when
wet, so a broken wire reads dry and stops the station.
===================================================================== *)
DryRun := NOT g_LSLL_Wet;
IF DryRun THEN
PumpsRequired := 0;
DryLockout := TRUE; (* latched, manual reset via command 1 *)
END_IF;
IF DryLockout THEN
PumpsRequired := 0;
END_IF;
(* =====================================================================
Step 6 - station mode off
===================================================================== *)
IF v_Mode = 2 THEN
PumpsRequired := 0;
END_IF;
(* =====================================================================
Step 11 (applied here, before selection) - stagger starts
Held second and third starts by 30 s each, to limit inrush and the
hydraulic transient. Applied before FB_DUTY_SELECT because it
limits how many units may start, which is an input to selection,
not a correction applied afterwards. Stops are never staggered.
LSHH bypasses the stagger as well as the min-off timers, so that
the emergency response is immediate.
===================================================================== *)
IF g_LSHH THEN
PumpsAllowed := PumpsRequired;
StaggerArm := FALSE;
ELSE
StaggerTmr(IN := StaggerArm, PT := T#30s);
IF PumpsAllowed < PumpsRequired THEN
IF PumpsAllowed = 0 THEN
PumpsAllowed := 1; (* first unit starts at once *)
StaggerArm := FALSE;
ELSIF StaggerTmr.Q THEN
PumpsAllowed := PumpsAllowed + 1;
StaggerArm := FALSE;
ELSE
StaggerArm := TRUE;
END_IF;
ELSE
IF PumpsAllowed > PumpsRequired THEN
PumpsAllowed := PumpsRequired;
END_IF;
StaggerArm := FALSE;
END_IF;
END_IF;
(* =====================================================================
Step 7 - duty selection
===================================================================== *)
Avail[1] := Pump1.Available; Avail[2] := Pump2.Available; Avail[3] := Pump3.Available;
Hours[1] := Pump1.RunHours; Hours[2] := Pump2.RunHours; Hours[3] := Pump3.RunHours;
SvcDue[1] := Pump1.ServiceDue; SvcDue[2] := Pump2.ServiceDue; SvcDue[3] := Pump3.ServiceDue;
RunNow[1] := Pump1.Running; RunNow[2] := Pump2.Running; RunNow[3] := Pump3.Running;
Duty(Available := Avail,
RunHours := Hours,
ServiceDue := SvcDue,
RunningNow := RunNow,
PumpsRequired := PumpsAllowed);
Req[1] := Duty.RunRequest[1];
Req[2] := Duty.RunRequest[2];
Req[3] := Duty.RunRequest[3];
(* =====================================================================
Step 8 - level control. On LSHH force 50.0 Hz.
===================================================================== *)
LvlCtl(Level := g_Level_m,
Setpoint := SpLevel_m,
Enable := (PumpsAllowed > 0),
MinSpeed := MinSpeedHz,
MaxSpeed := HARD_MAX_HZ);
Speed := LvlCtl.Speed;
IF g_LSHH THEN
Speed := HARD_MAX_HZ;
END_IF;
(* =====================================================================
Step 9 - the pumps
===================================================================== *)
Pump1(RunRequest := Req[1],
SpeedRef := Speed,
ThermalOK := g_ThermalOK[1],
SealLeak := g_SealLeak[1],
Vibration := g_Vib_mms[1],
DischPressure := g_PumpP_kPa[1],
ResetTrip := ResetTrip[1],
Lockout := Lockout[1],
MinOffBypass := g_LSHH,
ServiceInterval := INT_TO_REAL(v_ServiceHrs),
ResetHours := ResetHours[1]);
Pump2(RunRequest := Req[2],
SpeedRef := Speed,
ThermalOK := g_ThermalOK[2],
SealLeak := g_SealLeak[2],
Vibration := g_Vib_mms[2],
DischPressure := g_PumpP_kPa[2],
ResetTrip := ResetTrip[2],
Lockout := Lockout[2],
MinOffBypass := g_LSHH,
ServiceInterval := INT_TO_REAL(v_ServiceHrs),
ResetHours := ResetHours[2]);
Pump3(RunRequest := Req[3],
SpeedRef := Speed,
ThermalOK := g_ThermalOK[3],
SealLeak := g_SealLeak[3],
Vibration := g_Vib_mms[3],
DischPressure := g_PumpP_kPa[3],
ResetTrip := ResetTrip[3],
Lockout := Lockout[3],
MinOffBypass := g_LSHH,
ServiceInterval := INT_TO_REAL(v_ServiceHrs),
ResetHours := ResetHours[3]);
(* =====================================================================
Step 10 - headroom
===================================================================== *)
Head(Level := g_Level_m,
Inflow := g_Inflow_Lps,
TotalDischarge := g_Disch_Lps);
(* =====================================================================
Step 12 - publish
===================================================================== *)
PumpsRun := 0;
IF Pump1.Running THEN PumpsRun := PumpsRun + 1; END_IF;
IF Pump2.Running THEN PumpsRun := PumpsRun + 1; END_IF;
IF Pump3.Running THEN PumpsRun := PumpsRun + 1; END_IF;
HighLevel := g_Level_mm >= v_HighAlarm;
g_o_RunCmd[1] := Pump1.RunCmd;
g_o_RunCmd[2] := Pump2.RunCmd;
g_o_RunCmd[3] := Pump3.RunCmd;
g_o_Running[1] := Pump1.Running;
g_o_Running[2] := Pump2.Running;
g_o_Running[3] := Pump3.Running;
g_o_Available[1] := Pump1.Available;
g_o_Available[2] := Pump2.Available;
g_o_Available[3] := Pump3.Available;
g_o_Tripped[1] := Pump1.Tripped;
g_o_Tripped[2] := Pump2.Tripped;
g_o_Tripped[3] := Pump3.Tripped;
g_o_PumpState[1] := Pump1.State;
g_o_PumpState[2] := Pump2.State;
g_o_PumpState[3] := Pump3.State;
g_o_InAuto := (v_Mode = 1);
g_o_HighLevel := HighLevel;
g_o_SpillActive := g_SpillDetected;
g_o_Level_mm := g_Level_mm;
g_o_PumpsRun := PumpsRun;
g_o_DutyPump := Duty.DutyPump;
(* scaled analogues, clamped into 16-bit signed range *)
r := g_Inflow_Lps * 10.0;
IF r > 32767.0 THEN r := 32767.0; ELSIF r < -32768.0 THEN r := -32768.0; END_IF;
g_o_Inflow_x10 := REAL_TO_INT(r);
r := g_Disch_Lps * 10.0;
IF r > 32767.0 THEN r := 32767.0; ELSIF r < -32768.0 THEN r := -32768.0; END_IF;
g_o_Disch_x10 := REAL_TO_INT(r);
r := Speed * 10.0;
IF r > 32767.0 THEN r := 32767.0; ELSIF r < 0.0 THEN r := 0.0; END_IF;
g_o_Speed_x10 := REAL_TO_INT(r);
r := Head.NetInflow * 10.0;
IF r > 32767.0 THEN r := 32767.0; ELSIF r < -32768.0 THEN r := -32768.0; END_IF;
g_o_NetAccum := REAL_TO_INT(r);
r := Head.VolToSpill;
IF r > 32767.0 THEN r := 32767.0; ELSIF r < 0.0 THEN r := 0.0; END_IF;
g_o_VolToSpill := REAL_TO_INT(r);
g_o_TimeToSpill := Head.TimeToSpill;
g_o_TimeToLSHH := Head.TimeToLSHH;
r := Pump1.RunHours;
IF r > 32767.0 THEN r := 32767.0; END_IF;
g_o_RunHours[1] := REAL_TO_INT(r);
r := Pump2.RunHours;
IF r > 32767.0 THEN r := 32767.0; END_IF;
g_o_RunHours[2] := REAL_TO_INT(r);
r := Pump3.RunHours;
IF r > 32767.0 THEN r := 32767.0; END_IF;
g_o_RunHours[3] := REAL_TO_INT(r);
(* --- station state, section 3.1 ---------------------------------- *)
IF v_Mode = 2 THEN
g_o_StationState := 0; (* Off *)
ELSIF g_LSHH THEN
g_o_StationState := 4; (* Emergency *)
ELSIF DryLockout THEN
g_o_StationState := 5; (* Dry run lockout *)
ELSIF LevelFault THEN
g_o_StationState := 6; (* Fault *)
ELSIF HighLevel THEN
g_o_StationState := 3; (* High level *)
ELSIF PumpsRun > 0 THEN
g_o_StationState := 2; (* Pumping *)
ELSE
g_o_StationState := 1; (* Idle *)
END_IF;
(* --- alarm bitmask, section 6.
Accumulated in a DINT because bit 15 does not fit a signed INT.
Values at or above 32768 are folded into the negative half of the
16-bit word; CI Server must read %QW17 as UNSIGNED. ------------- *)
Alarm := 0;
IF HighLevel THEN Alarm := Alarm + 1; END_IF; (* bit0 *)
IF g_LSHH THEN Alarm := Alarm + 2; END_IF; (* bit1 *)
IF DryRun OR DryLockout THEN Alarm := Alarm + 4; END_IF; (* bit2 *)
IF g_SpillDetected THEN Alarm := Alarm + 8; END_IF; (* bit3 *)
IF Pump1.Tripped THEN Alarm := Alarm + 16; END_IF; (* bit4 *)
IF Pump2.Tripped THEN Alarm := Alarm + 32; END_IF; (* bit5 *)
IF Pump3.Tripped THEN Alarm := Alarm + 64; END_IF; (* bit6 *)
IF Pump1.SealAlarm THEN Alarm := Alarm + 128; END_IF; (* bit7 *)
IF Pump2.SealAlarm THEN Alarm := Alarm + 256; END_IF; (* bit8 *)
IF Pump3.SealAlarm THEN Alarm := Alarm + 512; END_IF; (* bit9 *)
IF Pump1.VibAlarm THEN Alarm := Alarm + 1024; END_IF; (* bit10 *)
IF Pump2.VibAlarm THEN Alarm := Alarm + 2048; END_IF; (* bit11 *)
IF Pump3.VibAlarm THEN Alarm := Alarm + 4096; END_IF; (* bit12 *)
IF LevelFault THEN Alarm := Alarm + 8192; END_IF; (* bit13 *)
IF NOT g_MainsOK THEN Alarm := Alarm + 16384; END_IF; (* bit14 *)
IF SpRejected THEN Alarm := Alarm + 32768; END_IF; (* bit15 *)
IF Alarm >= 32768 THEN
g_o_AlarmWord := DINT_TO_INT(Alarm - 65536);
ELSE
g_o_AlarmWord := DINT_TO_INT(Alarm);
END_IF;
END_PROGRAM
PROGRAM SIMULATION
VAR_EXTERNAL
g_simcmd_Inflow : INT;
g_simcmd_Mode : INT;
g_simcmd_Reset : INT;
g_simcmd_TimeScale : INT;
g_o_RunCmd : ARRAY [1..3] OF BOOL;
g_o_Speed_x10 : INT;
g_SimActive : BOOL;
g_sim_ClearReset : BOOL;
g_sim_Level_mm : INT;
g_sim_Inflow_x10 : INT;
g_sim_Disch_x10 : INT;
g_sim_ManifoldP : INT;
g_sim_PumpP : ARRAY [1..3] OF INT;
g_sim_Vib_x10 : ARRAY [1..3] OF INT;
g_sim_LSHH : BOOL;
g_sim_LSLL_Wet : BOOL;
g_sim_Spill : BOOL;
g_sim_ThermalOK : ARRAY [1..3] OF BOOL;
g_sim_SealLeak : ARRAY [1..3] OF BOOL;
g_sim_MainsOK : BOOL;
END_VAR
VAR
SCAN_S : REAL := 0.1;
AREA_M2 : REAL := 120.0;
SPILL_M : REAL := 6.0;
LSHH_M : REAL := 5.5;
LSLL_M : REAL := 0.30;
START_DLY_S : REAL := 3.0;
HZ_LO : REAL := 38.0;
HZ_HI : REAL := 50.0;
Q_LO : REAL := 65.0;
Q_HI : REAL := 120.0;
P_IDLE : REAL := 80.0;
P_BASE : REAL := 220.0;
P_PER_LPS : REAL := 1.4;
VIB_IDLE : REAL := 0.2;
VIB_BASE : REAL := 1.5;
VIB_PER_LPS : REAL := 0.02;
Init : BOOL := FALSE;
Volume_m3 : REAL;
Level_m : REAL;
Inflow_Lps : REAL;
SumFlow_Lps : REAL;
SimClock_s : REAL;
StartDly_s : ARRAY [1..3] OF REAL;
Delivering : ARRAY [1..3] OF BOOL;
Flow_Lps : ARRAY [1..3] OF REAL;
Press_kPa : ARRAY [1..3] OF REAL;
Vib_mms : ARRAY [1..3] OF REAL;
TimeScale : REAL;
dt_s : REAL;
Speed_Hz : REAL;
UnitQ_Lps : REAL;
Derate : REAL;
nDelivering : INT;
i : INT;
Rnd : DINT := 12345;
Noise : REAL;
END_VAR
(* ---------------------------------------------------------------------
Reset / first scan.
%MW22 = 1 restores the initial conditions of the selected mode
(section 8.2). The acknowledgement is a global; IO_MUX clears %MW22,
because this program may not touch located variables.
--------------------------------------------------------------------- *)
g_sim_ClearReset := FALSE;
IF (NOT Init) OR (g_simcmd_Reset = 1) THEN
Init := TRUE;
g_sim_ClearReset := TRUE;
SimClock_s := 0.0;
IF g_simcmd_Mode = 3 THEN
Level_m := 4.000; (* section 7.3 reference condition *)
ELSE
Level_m := 3.500; (* below start duty, station idle *)
END_IF;
Volume_m3 := Level_m * AREA_M2;
FOR i := 1 TO 3 DO
StartDly_s[i] := 0.0;
Delivering[i] := FALSE;
Flow_Lps[i] := 0.0;
Press_kPa[i] := P_IDLE;
Vib_mms[i] := VIB_IDLE;
END_FOR;
END_IF;
(* ---------------------------------------------------------------------
Time base. %MW23 = 1..120, anything outside that is treated as 1.
--------------------------------------------------------------------- *)
IF (g_simcmd_TimeScale >= 1) AND (g_simcmd_TimeScale <= 120) THEN
TimeScale := INT_TO_REAL(g_simcmd_TimeScale);
ELSE
TimeScale := 1.0;
END_IF;
dt_s := SCAN_S * TimeScale;
SimClock_s := SimClock_s + dt_s;
(* ---------------------------------------------------------------------
Inflow generator, section 8.2. %MW21 selects the mode.
--------------------------------------------------------------------- *)
CASE g_simcmd_Mode OF
1: (* diurnal dry weather: 40-110 L/s over a 24 h simulated period *)
Inflow_Lps := 75.0 + 35.0 * SIN(6.283185 * SimClock_s / 86400.0);
2: (* wet weather: ramp 20 min to 300, hold 40 min, decay over 90 min *)
IF SimClock_s < 1200.0 THEN
Inflow_Lps := 75.0 + (300.0 - 75.0) * SimClock_s / 1200.0;
ELSIF SimClock_s < 3600.0 THEN
Inflow_Lps := 300.0;
ELSIF SimClock_s < 9000.0 THEN
Inflow_Lps := 300.0 - (300.0 - 75.0) * (SimClock_s - 3600.0) / 5400.0;
ELSE
Inflow_Lps := 75.0;
END_IF;
3: (* demo reference, section 7.3: held at exactly 165 L/s *)
Inflow_Lps := 165.0;
ELSE (* 0 and anything unrecognised: manual, %MW20 in L/s x 10 *)
Inflow_Lps := INT_TO_REAL(g_simcmd_Inflow) / 10.0;
END_CASE;
IF Inflow_Lps < 0.0 THEN
Inflow_Lps := 0.0;
END_IF;
(* ---------------------------------------------------------------------
Per-pump flow model, section 8.2.
Speed comes from CONTROL's published common drive speed, which is
this scan's value because SIMULATION runs after CONTROL.
--------------------------------------------------------------------- *)
Speed_Hz := INT_TO_REAL(g_o_Speed_x10) / 10.0;
IF Speed_Hz < HZ_LO THEN
UnitQ_Lps := 0.0; (* static lift cutoff *)
ELSE
IF Speed_Hz > HZ_HI THEN
Speed_Hz := HZ_HI;
END_IF;
UnitQ_Lps := Q_LO + (Speed_Hz - HZ_LO) * (Q_HI - Q_LO) / (HZ_HI - HZ_LO);
END_IF;
(* Start delay on REAL time - see the header comment. *)
nDelivering := 0;
FOR i := 1 TO 3 DO
IF g_o_RunCmd[i] THEN
IF StartDly_s[i] < START_DLY_S THEN
StartDly_s[i] := StartDly_s[i] + SCAN_S;
END_IF;
Delivering[i] := (StartDly_s[i] >= START_DLY_S) AND (UnitQ_Lps > 0.0);
ELSE
StartDly_s[i] := 0.0;
Delivering[i] := FALSE;
END_IF;
IF Delivering[i] THEN
nDelivering := nDelivering + 1;
END_IF;
END_FOR;
(* Parallel derating: 3 units give ~360 L/s, not a naive 3 x 120. *)
CASE nDelivering OF
1: Derate := 1.00;
2: Derate := 0.94;
3: Derate := 0.88;
ELSE Derate := 1.00;
END_CASE;
SumFlow_Lps := 0.0;
FOR i := 1 TO 3 DO
IF Delivering[i] THEN
Flow_Lps[i] := UnitQ_Lps * Derate;
Press_kPa[i] := P_BASE + P_PER_LPS * Flow_Lps[i];
Vib_mms[i] := VIB_BASE + VIB_PER_LPS * Flow_Lps[i];
SumFlow_Lps := SumFlow_Lps + Flow_Lps[i];
ELSE
Flow_Lps[i] := 0.0;
Press_kPa[i] := P_IDLE;
IF g_o_RunCmd[i] THEN
Vib_mms[i] := VIB_BASE; (* spinning up, no flow yet *)
ELSE
Vib_mms[i] := VIB_IDLE;
END_IF;
END_IF;
END_FOR;
(* ---------------------------------------------------------------------
Wet well integration, section 8.2.
On reaching the weir the level holds at 6.00 m and the excess is
discarded - the station is seen to spill rather than running the
level off scale.
--------------------------------------------------------------------- *)
Volume_m3 := Volume_m3 + (Inflow_Lps - SumFlow_Lps) * dt_s / 1000.0;
IF Volume_m3 < 0.0 THEN
Volume_m3 := 0.0;
END_IF;
IF Volume_m3 > SPILL_M * AREA_M2 THEN
Volume_m3 := SPILL_M * AREA_M2;
END_IF;
Level_m := Volume_m3 / AREA_M2;
(* ---------------------------------------------------------------------
Publish, section 8.3: small noise so the trends are not perfectly
smooth and FB_HEADROOM's inflow filter has something to filter.
LCG kept small enough that the DINT multiply cannot overflow.
--------------------------------------------------------------------- *)
Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.01; (* +/-0.5% *)
g_sim_Level_mm := REAL_TO_INT(Level_m * 1000.0 * (1.0 + Noise));
Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.04; (* +/-2% *)
g_sim_Inflow_x10 := REAL_TO_INT(Inflow_Lps * 10.0 * (1.0 + Noise));
Rnd := (Rnd * 75 + 74) MOD 65537;
Noise := (DINT_TO_REAL(Rnd) / 65536.0 - 0.5) * 0.04;
g_sim_Disch_x10 := REAL_TO_INT(SumFlow_Lps * 10.0 * (1.0 + Noise));
FOR i := 1 TO 3 DO
g_sim_PumpP[i] := REAL_TO_INT(Press_kPa[i]);
g_sim_Vib_x10[i] := REAL_TO_INT(Vib_mms[i] * 10.0);
END_FOR;
(* Manifold: the highest delivering unit's pressure, idle if none. *)
g_sim_ManifoldP := REAL_TO_INT(P_IDLE);
IF nDelivering > 0 THEN
g_sim_ManifoldP := REAL_TO_INT(P_BASE + P_PER_LPS * UnitQ_Lps * Derate);
END_IF;
(* --- level switches. Sense conventions are section 2.1's. --------- *)
g_sim_LSHH := Level_m >= LSHH_M;
g_sim_LSLL_Wet := Level_m > LSLL_M; (* FALSE = dry *)
g_sim_Spill := Level_m >= (SPILL_M - 0.001);
(* --- plant health. Healthy unless a fault is injected; injection is
not modelled yet, so these are constant. ---------------------- *)
FOR i := 1 TO 3 DO
g_sim_ThermalOK[i] := TRUE;
g_sim_SealLeak[i] := FALSE;
END_FOR;
g_sim_MainsOK := TRUE;
(* Tell IO_MUX to take its process image from here, not from %IW/%IX. *)
g_SimActive := TRUE;
END_PROGRAM
PROGRAM IO_MUX
VAR_EXTERNAL
IW_LIT101 : INT;
IW_FIT201 : INT;
IW_FIT301 : INT;
IW_PIT302 : INT;
IW_PIT311 : INT;
IW_PIT321 : INT;
IW_PIT331 : INT;
IW_VE314 : INT;
IW_VE324 : INT;
IW_VE334 : INT;
IX_LSHH102 : BOOL;
IX_LSLL103 : BOOL;
IX_LSH104 : BOOL;
IX_TE312 : BOOL;
IX_TE322 : BOOL;
IX_TE332 : BOOL;
IX_MSE313 : BOOL;
IX_MSE323 : BOOL;
IX_MSE333 : BOOL;
IX_XA502 : BOOL;
QX_RunCmd1 : BOOL;
QX_RunCmd2 : BOOL;
QX_RunCmd3 : BOOL;
QX_Running1 : BOOL;
QX_Running2 : BOOL;
QX_Running3 : BOOL;
QX_Avail1 : BOOL;
QX_Avail2 : BOOL;
QX_Avail3 : BOOL;
QX_InAuto : BOOL;
QX_HighLevel : BOOL;
QX_SpillActive : BOOL;
QX_Tripped1 : BOOL;
QX_Tripped2 : BOOL;
QX_Tripped3 : BOOL;
QW_Level : INT;
QW_Inflow : INT;
QW_Discharge : INT;
QW_PumpsRunning : INT;
QW_Speed : INT;
QW_TimeToSpill : INT;
QW_TimeToLSHH : INT;
QW_NetAccum : INT;
QW_RunHours1 : INT;
QW_RunHours2 : INT;
QW_RunHours3 : INT;
QW_VolToSpill : INT;
QW_StationState : INT;
QW_PumpState1 : INT;
QW_PumpState2 : INT;
QW_PumpState3 : INT;
QW_DutyPump : INT;
QW_AlarmWord : INT;
QW_CmdAck : INT;
MW_Mode : INT;
MW_CmdWord : INT;
MW_CmdParam : INT;
MW_SpLevel : INT;
MW_StartDuty : INT;
MW_StartP2 : INT;
MW_StartP3 : INT;
MW_StopAll : INT;
MW_HighAlarm : INT;
MW_MinSpeed : INT;
MW_ServiceHrs : INT;
MW_SimInflow : INT;
MW_SimMode : INT;
MW_SimReset : INT;
MW_SimTimeScale : INT;
g_LevelRaw_mm : INT;
g_Level_mm : INT;
g_Level_m : REAL;
g_Inflow_Lps : REAL;
g_Disch_Lps : REAL;
g_ManifoldP_kPa : REAL;
g_PumpP_kPa : ARRAY [1..3] OF REAL;
g_Vib_mms : ARRAY [1..3] OF REAL;
g_LSHH : BOOL;
g_LSLL_Wet : BOOL;
g_SpillDetected : BOOL;
g_ThermalOK : ARRAY [1..3] OF BOOL;
g_SealLeak : ARRAY [1..3] OF BOOL;
g_MainsOK : BOOL;
g_cmd_Mode : INT;
g_cmd_Word : INT;
g_cmd_Param : INT;
g_sp_Level : INT;
g_sp_StartDuty : INT;
g_sp_StartP2 : INT;
g_sp_StartP3 : INT;
g_sp_StopAll : INT;
g_sp_HighAlarm : INT;
g_sp_MinSpeed : INT;
g_sp_ServiceHrs : INT;
g_o_RunCmd : ARRAY [1..3] OF BOOL;
g_o_Running : ARRAY [1..3] OF BOOL;
g_o_Available : ARRAY [1..3] OF BOOL;
g_o_Tripped : ARRAY [1..3] OF BOOL;
g_o_InAuto : BOOL;
g_o_HighLevel : BOOL;
g_o_SpillActive : BOOL;
g_o_Level_mm : INT;
g_o_Inflow_x10 : INT;
g_o_Disch_x10 : INT;
g_o_PumpsRun : INT;
g_o_Speed_x10 : INT;
g_o_TimeToSpill : INT;
g_o_TimeToLSHH : INT;
g_o_NetAccum : INT;
g_o_RunHours : ARRAY [1..3] OF INT;
g_o_VolToSpill : INT;
g_o_StationState : INT;
g_o_PumpState : ARRAY [1..3] OF INT;
g_o_DutyPump : INT;
g_o_AlarmWord : INT;
g_o_CmdAck : INT;
DEF_MODE : INT;
DEF_SP_LEVEL : INT;
DEF_START_DUTY : INT;
DEF_START_P2 : INT;
DEF_START_P3 : INT;
DEF_STOP_ALL : INT;
DEF_HIGH_ALARM : INT;
DEF_MIN_SPEED : INT;
DEF_SERVICE_HRS : INT;
g_SimActive : BOOL;
g_sim_ClearReset : BOOL;
g_simcmd_Inflow : INT;
g_simcmd_Mode : INT;
g_simcmd_Reset : INT;
g_simcmd_TimeScale : INT;
g_sim_Level_mm : INT;
g_sim_Inflow_x10 : INT;
g_sim_Disch_x10 : INT;
g_sim_ManifoldP : INT;
g_sim_PumpP : ARRAY [1..3] OF INT;
g_sim_Vib_x10 : ARRAY [1..3] OF INT;
g_sim_LSHH : BOOL;
g_sim_LSLL_Wet : BOOL;
g_sim_Spill : BOOL;
g_sim_ThermalOK : ARRAY [1..3] OF BOOL;
g_sim_SealLeak : ARRAY [1..3] OF BOOL;
g_sim_MainsOK : BOOL;
END_VAR
VAR
Seeded : BOOL := FALSE;
END_VAR
(* ---------------------------------------------------------------------
Seed the %MW setpoint defaults once, at first scan.
Section 9 forbids relying on retained variables, so after a runtime
restart every %MW reads 0. Writing the defaults once here means
CI Server sees real values rather than zeros, and CONTROL's
validation does not reject an all-zero image on every scan.
This is a one-shot write, not a per-scan overwrite: everything
CI Server writes afterwards survives, per section 2.
--------------------------------------------------------------------- *)
IF NOT Seeded THEN
Seeded := TRUE;
MW_Mode := DEF_MODE;
MW_CmdWord := 0;
MW_CmdParam := 0;
MW_SpLevel := DEF_SP_LEVEL;
MW_StartDuty := DEF_START_DUTY;
MW_StartP2 := DEF_START_P2;
MW_StartP3 := DEF_START_P3;
MW_StopAll := DEF_STOP_ALL;
MW_HighAlarm := DEF_HIGH_ALARM;
MW_MinSpeed := DEF_MIN_SPEED;
MW_ServiceHrs := DEF_SERVICE_HRS;
(* Simulation defaults, section 8.2: manual inflow at the dry
weather average, time scale 1. Unused in the field build. *)
MW_SimInflow := 750;
MW_SimMode := 0;
MW_SimReset := 0;
MW_SimTimeScale := 1;
END_IF;
(* ---------------------------------------------------------------------
Publish the previous scan's results to %QW / %QX
--------------------------------------------------------------------- *)
QX_RunCmd1 := g_o_RunCmd[1];
QX_RunCmd2 := g_o_RunCmd[2];
QX_RunCmd3 := g_o_RunCmd[3];
QX_Running1 := g_o_Running[1];
QX_Running2 := g_o_Running[2];
QX_Running3 := g_o_Running[3];
QX_Avail1 := g_o_Available[1];
QX_Avail2 := g_o_Available[2];
QX_Avail3 := g_o_Available[3];
QX_InAuto := g_o_InAuto;
QX_HighLevel := g_o_HighLevel;
QX_SpillActive := g_o_SpillActive;
QX_Tripped1 := g_o_Tripped[1];
QX_Tripped2 := g_o_Tripped[2];
QX_Tripped3 := g_o_Tripped[3];
QW_Level := g_o_Level_mm;
QW_Inflow := g_o_Inflow_x10;
QW_Discharge := g_o_Disch_x10;
QW_PumpsRunning := g_o_PumpsRun;
QW_Speed := g_o_Speed_x10;
QW_TimeToSpill := g_o_TimeToSpill;
QW_TimeToLSHH := g_o_TimeToLSHH;
QW_NetAccum := g_o_NetAccum;
QW_RunHours1 := g_o_RunHours[1];
QW_RunHours2 := g_o_RunHours[2];
QW_RunHours3 := g_o_RunHours[3];
QW_VolToSpill := g_o_VolToSpill;
QW_StationState := g_o_StationState;
QW_PumpState1 := g_o_PumpState[1];
QW_PumpState2 := g_o_PumpState[2];
QW_PumpState3 := g_o_PumpState[3];
QW_DutyPump := g_o_DutyPump;
QW_AlarmWord := g_o_AlarmWord;
QW_CmdAck := g_o_CmdAck;
(* ---------------------------------------------------------------------
Commands and setpoints in
--------------------------------------------------------------------- *)
g_cmd_Mode := MW_Mode;
g_cmd_Word := MW_CmdWord;
g_cmd_Param := MW_CmdParam;
g_sp_Level := MW_SpLevel;
g_sp_StartDuty := MW_StartDuty;
g_sp_StartP2 := MW_StartP2;
g_sp_StartP3 := MW_StartP3;
g_sp_StopAll := MW_StopAll;
g_sp_HighAlarm := MW_HighAlarm;
g_sp_MinSpeed := MW_MinSpeed;
g_sp_ServiceHrs := MW_ServiceHrs;
(* ---------------------------------------------------------------------
Simulation control out, and the reset acknowledgement.
SIMULATION may not touch located variables, so clearing %MW22 after
a reset happens here.
--------------------------------------------------------------------- *)
g_simcmd_Inflow := MW_SimInflow;
g_simcmd_Mode := MW_SimMode;
g_simcmd_Reset := MW_SimReset;
g_simcmd_TimeScale := MW_SimTimeScale;
IF g_sim_ClearReset THEN
MW_SimReset := 0;
END_IF;
(* ---------------------------------------------------------------------
THE MUX, section 8.1.
Field build: g_SimActive is FALSE (SIMULATION is not compiled
in and nothing ever sets it), so the located
inputs are used.
Simulation build: SIMULATION sets it TRUE every scan and the
process image comes from g_sim_* instead.
CONTROL sees identical globals either way and cannot tell which
source it is running on - that is the point of section 8.1.
--------------------------------------------------------------------- *)
IF g_SimActive THEN
g_LevelRaw_mm := g_sim_Level_mm;
g_Level_mm := g_sim_Level_mm;
g_Level_m := INT_TO_REAL(g_sim_Level_mm) / 1000.0;
g_Inflow_Lps := INT_TO_REAL(g_sim_Inflow_x10) / 10.0;
g_Disch_Lps := INT_TO_REAL(g_sim_Disch_x10) / 10.0;
g_ManifoldP_kPa := INT_TO_REAL(g_sim_ManifoldP);
g_PumpP_kPa[1] := INT_TO_REAL(g_sim_PumpP[1]);
g_PumpP_kPa[2] := INT_TO_REAL(g_sim_PumpP[2]);
g_PumpP_kPa[3] := INT_TO_REAL(g_sim_PumpP[3]);
g_Vib_mms[1] := INT_TO_REAL(g_sim_Vib_x10[1]) / 10.0;
g_Vib_mms[2] := INT_TO_REAL(g_sim_Vib_x10[2]) / 10.0;
g_Vib_mms[3] := INT_TO_REAL(g_sim_Vib_x10[3]) / 10.0;
g_LSHH := g_sim_LSHH;
g_LSLL_Wet := g_sim_LSLL_Wet;
g_SpillDetected := g_sim_Spill;
g_ThermalOK[1] := g_sim_ThermalOK[1];
g_ThermalOK[2] := g_sim_ThermalOK[2];
g_ThermalOK[3] := g_sim_ThermalOK[3];
g_SealLeak[1] := g_sim_SealLeak[1];
g_SealLeak[2] := g_sim_SealLeak[2];
g_SealLeak[3] := g_sim_SealLeak[3];
g_MainsOK := g_sim_MainsOK;
ELSE
(* ---------------------------------------------------------------------
FIELD SOURCE - analogue inputs, scaled to engineering units
--------------------------------------------------------------------- *)
g_LevelRaw_mm := IW_LIT101;
g_Level_mm := IW_LIT101;
g_Level_m := INT_TO_REAL(IW_LIT101) / 1000.0;
g_Inflow_Lps := INT_TO_REAL(IW_FIT201) / 10.0;
g_Disch_Lps := INT_TO_REAL(IW_FIT301) / 10.0;
g_ManifoldP_kPa := INT_TO_REAL(IW_PIT302);
g_PumpP_kPa[1] := INT_TO_REAL(IW_PIT311);
g_PumpP_kPa[2] := INT_TO_REAL(IW_PIT321);
g_PumpP_kPa[3] := INT_TO_REAL(IW_PIT331);
g_Vib_mms[1] := INT_TO_REAL(IW_VE314) / 10.0;
g_Vib_mms[2] := INT_TO_REAL(IW_VE324) / 10.0;
g_Vib_mms[3] := INT_TO_REAL(IW_VE334) / 10.0;
(* --- discrete inputs. Sense conventions are section 2.1's, and are
applied here so that CONTROL never has to know them. --------- *)
g_LSHH := IX_LSHH102; (* TRUE = wet *)
g_LSLL_Wet := IX_LSLL103; (* TRUE = wet; FALSE = dry, so a
broken wire stops the station *)
g_SpillDetected := IX_LSH104;
g_ThermalOK[1] := IX_TE312; (* TRUE = healthy *)
g_ThermalOK[2] := IX_TE322;
g_ThermalOK[3] := IX_TE332;
g_SealLeak[1] := IX_MSE313; (* TRUE = leak *)
g_SealLeak[2] := IX_MSE323;
g_SealLeak[3] := IX_MSE333;
g_MainsOK := IX_XA502; (* TRUE = healthy *)
END_IF;
END_PROGRAM
CONFIGURATION Config0
VAR_GLOBAL
CFG_AREA_M2 : REAL := 120.0;
CFG_SPILL_M : REAL := 6.000;
CFG_LSHH_M : REAL := 5.500;
CFG_MIN_HZ : REAL := 38.0;
CFG_MAX_HZ : REAL := 50.0;
DEF_MODE : INT := 1;
DEF_SP_LEVEL : INT := 4200;
DEF_START_DUTY : INT := 4000;
DEF_START_P2 : INT := 4500;
DEF_START_P3 : INT := 5000;
DEF_STOP_ALL : INT := 1000;
DEF_HIGH_ALARM : INT := 5200;
DEF_MIN_SPEED : INT := 380;
DEF_SERVICE_HRS : INT := 4000;
CFG_NO_TIME : INT := 32767;
IW_LIT101 AT %IW0 : INT;
IW_FIT201 AT %IW1 : INT;
IW_FIT301 AT %IW2 : INT;
IW_PIT302 AT %IW3 : INT;
IW_PIT311 AT %IW4 : INT;
IW_PIT321 AT %IW5 : INT;
IW_PIT331 AT %IW6 : INT;
IW_VE314 AT %IW7 : INT;
IW_VE324 AT %IW8 : INT;
IW_VE334 AT %IW9 : INT;
IX_LSHH102 AT %IX0.0 : BOOL;
IX_LSLL103 AT %IX0.1 : BOOL;
IX_LSH104 AT %IX0.2 : BOOL;
IX_TE312 AT %IX0.3 : BOOL;
IX_TE322 AT %IX0.4 : BOOL;
IX_TE332 AT %IX0.5 : BOOL;
IX_MSE313 AT %IX0.6 : BOOL;
IX_MSE323 AT %IX0.7 : BOOL;
IX_MSE333 AT %IX1.0 : BOOL;
IX_XA502 AT %IX1.1 : BOOL;
QX_RunCmd1 AT %QX0.0 : BOOL;
QX_RunCmd2 AT %QX0.1 : BOOL;
QX_RunCmd3 AT %QX0.2 : BOOL;
QX_Running1 AT %QX0.3 : BOOL;
QX_Running2 AT %QX0.4 : BOOL;
QX_Running3 AT %QX0.5 : BOOL;
QX_Avail1 AT %QX0.6 : BOOL;
QX_Avail2 AT %QX0.7 : BOOL;
QX_Avail3 AT %QX1.0 : BOOL;
QX_InAuto AT %QX1.1 : BOOL;
QX_HighLevel AT %QX1.2 : BOOL;
QX_SpillActive AT %QX1.3 : BOOL;
QX_Tripped1 AT %QX1.4 : BOOL;
QX_Tripped2 AT %QX1.5 : BOOL;
QX_Tripped3 AT %QX1.6 : BOOL;
QW_Level AT %QW0 : INT;
QW_Inflow AT %QW1 : INT;
QW_Discharge AT %QW2 : INT;
QW_PumpsRunning AT %QW3 : INT;
QW_Speed AT %QW4 : INT;
QW_TimeToSpill AT %QW5 : INT;
QW_TimeToLSHH AT %QW6 : INT;
QW_NetAccum AT %QW7 : INT;
QW_RunHours1 AT %QW8 : INT;
QW_RunHours2 AT %QW9 : INT;
QW_RunHours3 AT %QW10 : INT;
QW_VolToSpill AT %QW11 : INT;
QW_StationState AT %QW12 : INT;
QW_PumpState1 AT %QW13 : INT;
QW_PumpState2 AT %QW14 : INT;
QW_PumpState3 AT %QW15 : INT;
QW_DutyPump AT %QW16 : INT;
QW_AlarmWord AT %QW17 : INT;
QW_CmdAck AT %QW20 : INT;
MW_Mode AT %MW0 : INT;
MW_CmdWord AT %MW1 : INT;
MW_CmdParam AT %MW2 : INT;
MW_SpLevel AT %MW3 : INT;
MW_StartDuty AT %MW4 : INT;
MW_StartP2 AT %MW5 : INT;
MW_StartP3 AT %MW6 : INT;
MW_StopAll AT %MW7 : INT;
MW_HighAlarm AT %MW8 : INT;
MW_MinSpeed AT %MW9 : INT;
MW_ServiceHrs AT %MW10 : INT;
MW_SimInflow AT %MW20 : INT;
MW_SimMode AT %MW21 : INT;
MW_SimReset AT %MW22 : INT;
MW_SimTimeScale AT %MW23 : INT;
g_LevelRaw_mm : INT;
g_Level_mm : INT;
g_Level_m : REAL;
g_Inflow_Lps : REAL;
g_Disch_Lps : REAL;
g_ManifoldP_kPa : REAL;
g_PumpP_kPa : ARRAY [1..3] OF REAL;
g_Vib_mms : ARRAY [1..3] OF REAL;
g_LSHH : BOOL;
g_LSLL_Wet : BOOL;
g_SpillDetected : BOOL;
g_ThermalOK : ARRAY [1..3] OF BOOL;
g_SealLeak : ARRAY [1..3] OF BOOL;
g_MainsOK : BOOL;
g_cmd_Mode : INT;
g_cmd_Word : INT;
g_cmd_Param : INT;
g_sp_Level : INT;
g_sp_StartDuty : INT;
g_sp_StartP2 : INT;
g_sp_StartP3 : INT;
g_sp_StopAll : INT;
g_sp_HighAlarm : INT;
g_sp_MinSpeed : INT;
g_sp_ServiceHrs : INT;
g_o_RunCmd : ARRAY [1..3] OF BOOL;
g_o_Running : ARRAY [1..3] OF BOOL;
g_o_Available : ARRAY [1..3] OF BOOL;
g_o_Tripped : ARRAY [1..3] OF BOOL;
g_o_InAuto : BOOL;
g_o_HighLevel : BOOL;
g_o_SpillActive : BOOL;
g_o_Level_mm : INT;
g_o_Inflow_x10 : INT;
g_o_Disch_x10 : INT;
g_o_PumpsRun : INT;
g_o_Speed_x10 : INT;
g_o_TimeToSpill : INT;
g_o_TimeToLSHH : INT;
g_o_NetAccum : INT;
g_o_RunHours : ARRAY [1..3] OF INT;
g_o_VolToSpill : INT;
g_o_StationState : INT;
g_o_PumpState : ARRAY [1..3] OF INT;
g_o_DutyPump : INT;
g_o_AlarmWord : INT;
g_o_CmdAck : INT;
g_SimActive : BOOL;
g_sim_ClearReset : BOOL;
g_simcmd_Inflow : INT;
g_simcmd_Mode : INT;
g_simcmd_Reset : INT;
g_simcmd_TimeScale : INT;
g_sim_Level_mm : INT;
g_sim_Inflow_x10 : INT;
g_sim_Disch_x10 : INT;
g_sim_ManifoldP : INT;
g_sim_PumpP : ARRAY [1..3] OF INT;
g_sim_Vib_x10 : ARRAY [1..3] OF INT;
g_sim_LSHH : BOOL;
g_sim_LSLL_Wet : BOOL;
g_sim_Spill : BOOL;
g_sim_ThermalOK : ARRAY [1..3] OF BOOL;
g_sim_SealLeak : ARRAY [1..3] OF BOOL;
g_sim_MainsOK : BOOL;
END_VAR
RESOURCE Res0 ON PLC
TASK plc_task(INTERVAL := T#100ms,PRIORITY := 0);
PROGRAM inst_sim WITH plc_task : SIMULATION;
PROGRAM inst_mux WITH plc_task : IO_MUX;
PROGRAM inst_ctl WITH plc_task : CONTROL;
END_RESOURCE
END_CONFIGURATION