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