You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

444 lines
18 KiB
Plaintext

<?xml version="1.0" encoding="utf-8"?>
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.12">
<POU Name="fbZone" Id="{9d5420d4-253c-4a37-b217-4ab780f6de05}" SpecialFunc="None">
<Declaration><![CDATA[FUNCTION_BLOCK fbZone
VAR_INPUT
iZoneNo : INT;
END_VAR
VAR_OUTPUT
iTAverage : INT; // Average Zone Temperature
END_VAR
VAR
i,k : INT;
bFSpid : BOOL; // First scan PID block
arT : ARRAY[1..Set.iNoApZoneMax] OF DINT; // Array of Temperatures in Zone
temp,tmp, t : DINT;
Size : INT;
LH : INT;
// PIDControl : FB_BasicPID;
fDutyCycle : LREAL;
fbApartOffset : fbHeatLevel;
fApartOffsetCalculated : LREAL;
iApOffsetBasis : INT; // Appartment correction offeset from the Set.arHeatRoomAdj array
MeasureStart : T_DCTIME64;
fbCTRL_PID: CTRL_PID;
PID_Y : REAL;
bPWM_Q : BOOL; // PWM output
tPIDCycle: TON;
fbCTRL_OUT: CTRL_OUT;
fbZoneHeatLevel: fbHeatLevel;
// fZonePID_LIM_H : REAL;
iHeatLevel : INT; // INT Current heat level according outside temperature
fHeatLevel : REAL; // REAL Current heat level according outside temperature
fbCTRL_PWM: CTRL_PWM;
fAptT_PV_Filtered : REAL;
LowPassFilter: LowPassFilter;
iTDiff : INT; // Temperature SP PV differencial
iHeatLevelIndex: INT; // Current Heat Level Index
iHeatLevelIndexMem: INT; // Heat level Index Old/Memory
bHeatLevelIndexChanged : BOOL; // Heat Level Index changed pulse
bInc_CMD: BOOL;
bDec_CMD: BOOL;
tIncTMR : TON;
tDecTMR : TON;
tHeatLevelActive : TON;
bHeatLevelStable : BOOL;
bHeatLevelInc_RQST: BOOL;
bHeatLevelDec_RQST: BOOL;
END_VAR
]]></Declaration>
<Implementation>
<ST><![CDATA[// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ START OF PREPARATIONS ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
Size := Set.arApInZone[iZoneNo].iApsNo;
LH := Set.arApInZone[iZoneNo].iLowHigh;
// Create an Array if T°C per ZONE to calculate Average T°C x0.1°C
//
FOR i:=1 TO Size DO // Get number of apartments in Zone from the initial array Set.arApInZone
arT[i] := INT_TO_DINT(GVL.arZoneData[iZoneNo].arAp[i].iT_PV); // Create an Array of T°C per ZONE to calculate Average T°C x0.1°C
END_FOR
// Sort Array from Low to High
//
FOR i:=1 TO Size-1 DO
FOR k:=i+1 TO Size DO
IF arT[i] > arT[k] THEN
temp:=arT[i];
arT[i]:=arT[k];
arT[k]:=temp;
END_IF
END_FOR
END_FOR
// Remove the Highest and Lowest numbers and calculate the Average Temperature
//
tmp := 0;
k := 0;
FOR i:=(1+LH) TO (Size-LH) DO
tmp:= (arT[i]) + tmp;
k := k+1;
END_FOR
// Calculate the end result, Average Temperature
//
GVL.arZoneData[iZoneNo].iTAver := DINT_TO_INT(tmp/k);
GVL.arZoneData[iZoneNo].fHMI_TValue := INT_TO_REAL(GVL.arZoneData[iZoneNo].iTAver)/10;
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ END OF PREPARATIONS ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ±
// Find the Limit from the Zone Heat Level Table
fbZoneHeatLevel(inTemp:= GVL.iTempOutsideChill,
inHeatLevelArray:= Set.arHeatLevel[iZoneNo],
outLevel=> iHeatLevel,
outIndex=> iHeatLevelIndex); // Current position of the Heat Level in the Array Set.arHeatLevel
fHeatLevel := INT_TO_REAL(iHeatLevel); // Convert INT to REAL
bHeatLevelIndexChanged := FALSE;
IF iHeatLevelIndex <> iHeatLevelIndexMem THEN
bHeatLevelIndexChanged := TRUE; // Set Flag for one scan to use in tHeatLevelActive Timer
iHeatLevelIndexMem := iHeatLevelIndex;
END_IF
// Run PID control to calculate Duty Cycle
// PID works as a helper to adjust the main heat table
// Settings
//
GVL.arZoneData[iZoneNo].PID.fSetpointValue := INT_TO_REAL(Set.iSetT); // setpoint value
GVL.arZoneData[iZoneNo].PID.fActualValue := INT_TO_REAL(GVL.arZoneData[iZoneNo].iTAver); // actual value
//GVL.arZoneData[iZoneNo].PID.bReset := FALSE; // TRUE at this input resets the internal state variables and the controller output.
GVL.arZoneData[iZoneNo].PID.fCtrlCycleTime := 6.0; // LREAL controller cycle time in seconds [s]
GVL.arZoneData[iZoneNo].PID.fKp := Set.fKp; // 1.5 REAL proportional gain Kp (P)
GVL.arZoneData[iZoneNo].PID.fTn := Set.fTn; // TN = KP/KI = 1.5/10 = 0.15 [s]
GVL.arZoneData[iZoneNo].PID.fTv := Set.fTv; // TV = KD/KP = 200/1.5 = 133.3 [s]
GVL.arZoneData[iZoneNo].PID.fTd := Set.fTd; // 1200.0 LREAL derivative damping time Td (D-T1) [s]
GVL.arZoneData[iZoneNo].PID.fM_In := fHeatLevel; // input value for manual operation
IF Set.HeatEnabled AND Set.arApInZone[iZoneNo].bHeatEnabled THEN
IF bFSpid THEN
GVL.arZoneData[iZoneNo].PID.bReset := FALSE;
ELSE
GVL.arZoneData[iZoneNo].PID.bReset := TRUE; // Reset PID on the First Section Scan
END_IF
bFSpid := TRUE; // Activate first PID scan bit
// If Heat enabled, Execute PID
// OSCAT CTRL_PID function block
// Set Output Low and High Limits
GVL.arZoneData[iZoneNo].PID.fLL := 0.0;
GVL.arZoneData[iZoneNo].PID.fLH := 100.0;
// Execute PID block
// Need to run it once in 6sec
tPIDCycle(IN:= NOT tPIDCycle.Q, PT:= T#6S, Q=> , ET=> ); // Generate a pulse every 6sec
(*
CTRL_PID is a PID controller with dynamic anti-wind up and manual control input. The PID controller operates according to the formula:
Y = KP * (DIFF + 1/Tn * INTEG(DIFF) + TV *DERIV(DIFF)) + OFFSET where DIFF = SET_POINT - ACTUAL
In manual mode (manual = TRUE) is: Y = MANUAL_IN+ OFFSET
ACT is the measured value for the controlled system and SET is the setpoint for the controller. The input values of LH and LL limit the output value Y.
With RST, the internal integrator will always set to 0. The output LIM signals that the controller has reached the limit of LL or LH.
The PID controller operates free-running and uses the trapezoidal rule to calculate with highest accuracy and optimal speed.
The default values of the input parameters are predefned as follows:
KP = 1, TN = 1, TV = 1, LIMIT_L = -1000 and LIMIT_H = +1000. With the input SUP a noise reduction is set, the value on input SUP determines
at which control diference, the controller turns on. With SUP is avoided that the output of the controller wobbles.
The value at the input SUP should be in dimension that it suppresses the noise of the controlled system and the sensors. If the input to SUP is
set to 0.1, the controller is only at deviations greater than 0.1 active.
The ouput DIFF passes the measured and through a noise flter (DEAD_BAND) filtered control deviation.
DIFF is normally not required in a controlled system but can be used to infuence the control parameters.
The input OFS is added as the last value to output, and is used to compensate mainly of noise, whose efect can be estimated on the loop.
The controller works with a dynamic air- Up that prevents that the integrator, when reaching a output limit and further deviation, continues to run
unlimited and afects the properties usually negative. In the introduction chapter of the control technology, more details can be found on anti-windup.
The control parameters are given in the form of KP, TN and TV, and if there are parameters KP, KI and KD they can be converted using the following
formula: TN = KP/KI und TV = KD/KP
*)
IF tPIDCycle.Q THEN
fbCTRL_PID(
ACT:= GVL.arZoneData[iZoneNo].PID.fActualValue, // value measured by the way PV - Process Value
SET:= GVL.arZoneData[iZoneNo].PID.fSetpointValue, // set value, SP - Set Point
SUP:= Set.PID_Noise_SUP, // noise reduction 0.2°C In PID controller, if ABS(SP-PV)<SUP then output value is 0
OFS:= GVL.arZoneData[iZoneNo].PID.fOfs, // ofset for the output
M_I:= GVL.arZoneData[iZoneNo].PID.fM_In, // input value for manual operation
MAN:= GVL.arZoneData[iZoneNo].PID.bManual, // switch to manual mode, MANUAL = TRUE
RST:= GVL.arZoneData[iZoneNo].PID.bReset, // asynchronous reset input
KP:= GVL.arZoneData[iZoneNo].PID.fKp, // controller gain
TN:= GVL.arZoneData[iZoneNo].PID.fTn, // reset of the controller
TV:= GVL.arZoneData[iZoneNo].PID.fTv, // derivative of the controller
LL:= GVL.arZoneData[iZoneNo].PID.fLL, // lower output limit
LH:= GVL.arZoneData[iZoneNo].PID.fLH, // upper output limit
Y=> PID_Y , // REAL, output of the controller
DIFF=> GVL.arZoneData[iZoneNo].PID.DIFF, // (* deviation *)
LIM=> ); // GVL.arZoneData[iZoneNo].PID.LIM Out of limit if active
// PID is a Helper. We increase HeatLevel value on 20% (/100/5=500) of what PID ask
//
GVL.arZoneData[iZoneNo].fDutyCycle := fHeatLevel * (1 + PID_Y/500.0) ;
(* // Limit the value according to Zone Heat Table
fbCTRL_OUT(
CI:= GVL.arZoneData[iZoneNo].fDutyCycle,
OFFSET:= 0.0,
MAN_IN:= 0.0,
LIM_L:= 0.0,
LIM_H:= 100.0,
MANUAL:= FALSE,
Y=> GVL.arZoneData[iZoneNo].PID.fCtrlOutput,
LIM=> GVL.arZoneData[iZoneNo].PID.LIM);
*)
END_IF
ELSE
bFSpid := FALSE; // Reset first PID scan bit
END_IF
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ AUTO TUNING START ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
// Auto tuning to collect data over the season and correct Heat Level Table/Array
//
IF Set.bHMI_AutoCalib_Enable AND Set.HeatEnabled THEN
iTDiff := Set.iSetT - GVL.arZoneData[iZoneNo].iTAver; // Calculate SP PV difference
IF GVL.iTempOutside > 200 OR GVL.iTempOutside < -400 THEN
iHeatLevelIndex := -1;
END_IF
tHeatLevelActive(IN:= NOT bHeatLevelIndexChanged AND NOT bInc_CMD AND NOT bDec_CMD, PT:= T#7200S, Q=> bHeatLevelStable , ET=>); // Check if Heat Level didn't change
bInc_CMD := bHeatLevelStable AND iTDiff > 5; // bHeatLevelInc_RQST;
bDec_CMD := bHeatLevelStable AND iTDiff < 5; // bHeatLevelDec_RQST;
IF bInc_CMD AND iHeatLevelIndex >= 0 AND (Set.arHeatLevel[iZoneNo][iHeatLevelIndex] - Set.arHeatLevelDefault[iHeatLevelIndex]) <5 AND Set.arHeatLevel[iZoneNo][iHeatLevelIndex] <100 THEN
Set.arHeatLevel[iZoneNo][iHeatLevelIndex] := Set.arHeatLevel[iZoneNo][iHeatLevelIndex] + 1;
END_IF
IF bDec_CMD AND iHeatLevelIndex >= 0 AND (Set.arHeatLevelDefault[iHeatLevelIndex] - Set.arHeatLevel[iZoneNo][iHeatLevelIndex]) <5 AND Set.arHeatLevel[iZoneNo][iHeatLevelIndex] >0 THEN
Set.arHeatLevel[iZoneNo][iHeatLevelIndex] := Set.arHeatLevel[iZoneNo][iHeatLevelIndex] - 1;
END_IF
// Set an Error flag if Increase/Decrease limit exeeded , more that 5%/units
IF (Set.arHeatLevel[iZoneNo][iHeatLevelIndex] - Set.arHeatLevelDefault[iHeatLevelIndex]) >= 5 THEN
;
END_IF
IF (Set.arHeatLevelDefault[iHeatLevelIndex] - Set.arHeatLevel[iZoneNo][iHeatLevelIndex]) >= 5 THEN
;
END_IF
END_IF
// ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ AUTO TUNING END ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
// Send PID value to the Apartment output
// Convert Duty Cycle to PWM modulation with time cycle Set.tPWMCycle
//
IF Set.HeatEnabled AND Set.arApInZone[iZoneNo].bHeatEnabled AND GVL.iTempOutsideChill <= (Set.iSetMaxOper+10) THEN
FOR i:= 1 TO Set.arApInZone[iZoneNo].iApsNo DO
// i := 1; // Temporaly to test math
// Find an Apartment DutyCycle offset if required (Default temperature is overwritten
IF GVL.arZoneData[iZoneNo].arAp[i].iT_SP <> Set.iSetT THEN
fbApartOffset(
inTemp:= GVL.iTempOutside,
inHeatLevelArray:= SET.arHeatRoomAdj,
outLevel=> iApOffsetBasis);
GVL.arZoneData[iZoneNo].arAp[i].fDCOffset := GVL.arZoneData[iZoneNo].PID.fCtrlOutput * INT_TO_REAL(iApOffsetBasis)/100 * INT_TO_REAL(GVL.arZoneData[iZoneNo].arAp[i].iT_SP - Set.iSetT)/10.0;
ELSE
GVL.arZoneData[iZoneNo].arAp[i].fDCOffset := 0.0;
END_IF
// Convert Duty Cycle to PWM modulation with time cycle Set.tPWMCycle
Set.arApInZone[iZoneNo].fDutyCycle := (GVL.arZoneData[iZoneNo].PID.fCtrlOutput + GVL.arZoneData[iZoneNo].arAp[i].fDCOffset)/100; // = 0.0 .. 1.0
fbCTRL_PWM(
CI:= Set.arApInZone[iZoneNo].fDutyCycle, // CI = 0.0 .. 1.0
MAN_IN:= fHeatLevel/100.0,
MANUAL:= FALSE,
F:= 1 / Set.tPWMCycle, // Duty Cycle Period
Q=> bPWM_Q);
// Before to write SSR output, Check if Overheat is enabled/active
//
IF Set.bHMI_Overheat_Enabled THEN
IF GVL.arZoneData[iZoneNo].arAp[i].iT_PV > (GVL.arZoneData[iZoneNo].arAp[i].iT_SP + Set.iOverTempSet) THEN // If Temp SP + 0.5°C => cut-off SSR
GVL.arZoneData[iZoneNo].arAp[i].bPWMSSR_ON := FALSE;
ELSE
GVL.arZoneData[iZoneNo].arAp[i].bPWMSSR_ON := bPWM_Q;
END_IF
ELSE
GVL.arZoneData[iZoneNo].arAp[i].bPWMSSR_ON := bPWM_Q;
END_IF
END_FOR
ELSE
FOR i:= 1 TO Set.arApInZone[iZoneNo].iApsNo DO
GVL.arZoneData[iZoneNo].arAp[i].bPWMSSR_ON := FALSE; // Ih Heat is not enabled, turn OFF SSRs
END_FOR
END_IF
// Convert and Transfer Apartment Temperature to HMI, INT to REAL
//
i := iZoneNo;
FOR k := 1 TO Set.arApInZone[i].iApsNo DO
// GVL.arZoneData[i].arAp[k].HMI_Value := INT_TO_REAL(GVL.arZoneData[i].arAp[k].iT_PV)/10; // Convert INT to REAL to show T°C on HMI
// Section below is to apply a low-pass filter to the values to prevent fast temperature change on the HMI screen
GVL.arAptT_PV_Filter[i,k](
Enable:= TRUE,
In:= INT_TO_REAL(GVL.arZoneData[i].arAp[k].iT_PV)/10,
k:= Set.fLowPassFilter_k,
Valid=> ,
Out=> fAptT_PV_Filtered);
GVL.arZoneData[i].arAp[k].HMI_Value := fAptT_PV_Filtered;
END_FOR
]]></ST>
</Implementation>
<LineIds Name="fbZone">
<LineId Id="676" Count="1" />
<LineId Id="44" Count="0" />
<LineId Id="80" Count="0" />
<LineId Id="32" Count="0" />
<LineId Id="22" Count="0" />
<LineId Id="95" Count="0" />
<LineId Id="9" Count="0" />
<LineId Id="587" Count="0" />
<LineId Id="26" Count="0" />
<LineId Id="29" Count="0" />
<LineId Id="28" Count="0" />
<LineId Id="94" Count="0" />
<LineId Id="33" Count="7" />
<LineId Id="30" Count="0" />
<LineId Id="63" Count="0" />
<LineId Id="46" Count="0" />
<LineId Id="96" Count="0" />
<LineId Id="82" Count="0" />
<LineId Id="85" Count="0" />
<LineId Id="45" Count="0" />
<LineId Id="83" Count="0" />
<LineId Id="88" Count="0" />
<LineId Id="84" Count="0" />
<LineId Id="97" Count="0" />
<LineId Id="89" Count="0" />
<LineId Id="98" Count="0" />
<LineId Id="87" Count="0" />
<LineId Id="519" Count="0" />
<LineId Id="674" Count="0" />
<LineId Id="116" Count="0" />
<LineId Id="792" Count="3" />
<LineId Id="675" Count="0" />
<LineId Id="931" Count="0" />
<LineId Id="796" Count="0" />
<LineId Id="932" Count="4" />
<LineId Id="800" Count="0" />
<LineId Id="937" Count="0" />
<LineId Id="86" Count="0" />
<LineId Id="117" Count="0" />
<LineId Id="166" Count="0" />
<LineId Id="168" Count="8" />
<LineId Id="798" Count="0" />
<LineId Id="178" Count="0" />
<LineId Id="135" Count="0" />
<LineId Id="137" Count="0" />
<LineId Id="458" Count="3" />
<LineId Id="457" Count="0" />
<LineId Id="456" Count="0" />
<LineId Id="787" Count="0" />
<LineId Id="136" Count="0" />
<LineId Id="261" Count="0" />
<LineId Id="286" Count="0" />
<LineId Id="285" Count="0" />
<LineId Id="282" Count="0" />
<LineId Id="284" Count="0" />
<LineId Id="287" Count="0" />
<LineId Id="281" Count="0" />
<LineId Id="315" Count="1" />
<LineId Id="801" Count="0" />
<LineId Id="320" Count="0" />
<LineId Id="680" Count="0" />
<LineId Id="682" Count="0" />
<LineId Id="684" Count="1" />
<LineId Id="703" Count="2" />
<LineId Id="702" Count="0" />
<LineId Id="706" Count="1" />
<LineId Id="692" Count="0" />
<LineId Id="708" Count="2" />
<LineId Id="697" Count="1" />
<LineId Id="700" Count="1" />
<LineId Id="678" Count="0" />
<LineId Id="802" Count="0" />
<LineId Id="317" Count="0" />
<LineId Id="319" Count="0" />
<LineId Id="267" Count="13" />
<LineId Id="264" Count="0" />
<LineId Id="803" Count="0" />
<LineId Id="749" Count="0" />
<LineId Id="804" Count="0" />
<LineId Id="750" Count="0" />
<LineId Id="366" Count="0" />
<LineId Id="359" Count="0" />
<LineId Id="351" Count="7" />
<LineId Id="349" Count="0" />
<LineId Id="808" Count="0" />
<LineId Id="321" Count="0" />
<LineId Id="805" Count="0" />
<LineId Id="322" Count="0" />
<LineId Id="455" Count="0" />
<LineId Id="129" Count="0" />
<LineId Id="197" Count="0" />
<LineId Id="847" Count="2" />
<LineId Id="201" Count="0" />
<LineId Id="851" Count="29" />
<LineId Id="850" Count="0" />
<LineId Id="179" Count="0" />
<LineId Id="141" Count="0" />
<LineId Id="180" Count="0" />
<LineId Id="226" Count="1" />
<LineId Id="181" Count="0" />
<LineId Id="214" Count="0" />
<LineId Id="213" Count="0" />
<LineId Id="215" Count="1" />
<LineId Id="191" Count="2" />
<LineId Id="220" Count="1" />
<LineId Id="223" Count="1" />
<LineId Id="189" Count="0" />
<LineId Id="218" Count="0" />
<LineId Id="196" Count="0" />
<LineId Id="553" Count="0" />
<LineId Id="369" Count="4" />
<LineId Id="229" Count="0" />
<LineId Id="481" Count="0" />
<LineId Id="475" Count="3" />
<LineId Id="482" Count="0" />
<LineId Id="484" Count="1" />
<LineId Id="483" Count="0" />
<LineId Id="479" Count="1" />
<LineId Id="473" Count="1" />
<LineId Id="183" Count="0" />
<LineId Id="254" Count="3" />
<LineId Id="211" Count="0" />
<LineId Id="464" Count="0" />
<LineId Id="253" Count="0" />
<LineId Id="406" Count="0" />
<LineId Id="420" Count="0" />
<LineId Id="807" Count="0" />
<LineId Id="410" Count="0" />
<LineId Id="412" Count="0" />
<LineId Id="638" Count="0" />
<LineId Id="809" Count="0" />
<LineId Id="595" Count="3" />
<LineId Id="591" Count="0" />
<LineId Id="590" Count="0" />
<LineId Id="637" Count="0" />
<LineId Id="413" Count="0" />
<LineId Id="407" Count="0" />
<LineId Id="184" Count="0" />
</LineIds>
</POU>
</TcPlcObject>