(* ===================================================================== 30_prog_control.st - PROGRAM CONTROL, section 4.5 Contains no located variable reference of any kind. Every input arrives through the process image globals written by IO_MUX, and every output leaves the same way. This is what makes the pass 2 simulation a mux change rather than a control change. Execution order below follows section 4.5 step for step. ===================================================================== *) PROGRAM CONTROL VAR_EXTERNAL (* process image in *) 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; (* commands and setpoints in, unvalidated *) 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; (* published out *) 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 CONSTANT SPILL_MM : INT := 6000; (* spill weir, mm *) LEVEL_MAX_MM : INT := 7000; (* LIT-101 range top *) HARD_MIN_HZ : REAL := 38.0; HARD_MAX_HZ : REAL := 50.0; END_VAR VAR (* --- function block instances. Three explicit pump instances rather than an ARRAY OF FB_PUMP: arrays of function blocks are the kind of construct section 9 warns about. --------- *) Pump1 : FB_PUMP; Pump2 : FB_PUMP; Pump3 : FB_PUMP; Duty : FB_DUTY_SELECT; LvlCtl : FB_LEVEL_CTRL; Head : FB_HEADROOM; (* --- validated setpoints, seeded with the section 2.3 defaults and held at the last good value on a bad write ---------- *) 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; (* latched, bit 15, cleared by cmd 6 *) SpOK : BOOL; (* --- command handshake ------------------------------------------ *) CmdBusy : BOOL; ResetTrip : ARRAY[1..3] OF BOOL; ResetHours : ARRAY[1..3] OF BOOL; Lockout : ARRAY[1..3] OF BOOL; AckAlarms : BOOL; p : INT; (* --- staging ---------------------------------------------------- *) PumpsRequired : INT; (* held across scans - the hysteresis *) PumpsAllowed : INT; (* after start stagger *) StaggerTmr : TON; StaggerArm : BOOL; (* --- interlocks -------------------------------------------------- *) DryRun : BOOL; DryLockout : BOOL; LevelRangeFault : BOOL; LevelFrozen : BOOL; LevelFault : BOOL; LevelRef : INT; LevelMoved : BOOL; FrozenTmr : TON; AnyRunning : BOOL; (* --- duty selector interface ------------------------------------ *) 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; (* --- misc -------------------------------------------------------- *) 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