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) 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 ]]>