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.
1687 lines
52 KiB
Smalltalk
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
|