|
22 | 22 | const byte inputPin = 0;
|
23 | 23 | const byte outputPin = 3;
|
24 | 24 |
|
25 |
| -int Print = 0; // on(1) monitor, off(0) plotter |
26 |
| -int tuningRule = 1; // see above table |
27 |
| -float POn = 1.0; // mix of PonE to PonM (0.0-1.0) |
28 |
| -unsigned long timeout = 120; // AutoTune timeout (sec) |
| 25 | +int Print = 0; // on(1) monitor, off(0) plotter |
| 26 | +int tuningRule = 1; // see above table |
| 27 | +float POn = 1.0; // mix of PonE to PonM (0.0-1.0) |
| 28 | +byte aTune = 0; // autoTune status, done = 10 |
29 | 29 |
|
30 | 30 | float Input, Output, Setpoint;
|
31 | 31 | float Kp = 0, Ki = 0, Kd = 0;
|
32 | 32 |
|
33 |
| -// choose controller direction: |
34 |
| -// DIRECT: Input increases when the output is increased or the error is positive (heating). |
35 |
| -// REVERSE: Input decreases when the output is increased or when the error is positive (cooling). |
36 | 33 | QuickPID myQuickPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, POn, QuickPID::DIRECT);
|
37 |
| -//QuickPID myQuickPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, POn, QuickPID::REVERSE); |
38 | 34 |
|
39 |
| -void setup() |
40 |
| -{ |
| 35 | +void setup() { |
41 | 36 | Serial.begin(115200);
|
42 | 37 | Serial.println();
|
| 38 | +} |
43 | 39 |
|
44 |
| - myQuickPID.AutoTune(inputPin, outputPin, tuningRule, Print, timeout); |
45 |
| - myQuickPID.SetTunings(myQuickPID.GetKp(), myQuickPID.GetKi(), myQuickPID.GetKd()); |
46 |
| - myQuickPID.SetSampleTimeUs(4000); // 4ms |
47 |
| - myQuickPID.SetMode(QuickPID::AUTOMATIC); |
48 |
| - Setpoint = 700; |
49 |
| - |
50 |
| - if (Print == 1) { |
51 |
| - // Controllability https://blog.opticontrols.com/wp-content/uploads/2011/06/td-versus-tau.png |
52 |
| - if (float(myQuickPID.GetTu() / myQuickPID.GetTd() + 0.0001) > 0.75) Serial.println("This process is easy to control."); |
53 |
| - else if (float(myQuickPID.GetTu() / myQuickPID.GetTd() + 0.0001) > 0.25) Serial.println("This process has average controllability."); |
54 |
| - else Serial.println("This process is difficult to control."); |
55 |
| - Serial.print("Tu: "); Serial.print(myQuickPID.GetTu()); // Ultimate Period (sec) |
56 |
| - Serial.print(" td: "); Serial.print(myQuickPID.GetTd()); // Dead Time (sec) |
57 |
| - Serial.print(" Ku: "); Serial.print(myQuickPID.GetKu()); // Ultimate Gain |
58 |
| - Serial.print(" Kp: "); Serial.print(myQuickPID.GetKp()); |
59 |
| - Serial.print(" Ki: "); Serial.print(myQuickPID.GetKi()); |
60 |
| - Serial.print(" Kd: "); Serial.println(myQuickPID.GetKd()); |
61 |
| - Serial.println(); |
62 |
| - delay(5000); //view results |
| 40 | +void loop() { |
| 41 | + Input = avg(myQuickPID.analogReadFast(inputPin)); |
| 42 | + if (aTune < 10) autoTune(); |
| 43 | + if (aTune == 9) { // apply new tunings |
| 44 | + myQuickPID.SetTunings(Kp, Ki, Kd); |
| 45 | + myQuickPID.SetMode(QuickPID::AUTOMATIC); |
| 46 | + Setpoint = 700; |
| 47 | + } |
| 48 | + analogWrite(outputPin, Output); |
| 49 | + if (aTune == 10) { // compute |
| 50 | + if (Print == 0) { // serial plotter |
| 51 | + Serial.print("Setpoint:"); Serial.print(Setpoint); Serial.print(","); |
| 52 | + Serial.print("Input:"); Serial.print(Input); Serial.print(","); |
| 53 | + Serial.print("Output:"); Serial.print(Output); Serial.print(","); |
| 54 | + Serial.println(","); |
| 55 | + } |
| 56 | + Input = avg(myQuickPID.analogReadFast(inputPin)); |
| 57 | + myQuickPID.Compute();; |
| 58 | + analogWrite(outputPin, Output); |
63 | 59 | }
|
64 | 60 | }
|
65 | 61 |
|
66 |
| -void loop() |
67 |
| -{ // plotter |
68 |
| - Serial.print("Setpoint:"); Serial.print(Setpoint); Serial.print(","); |
69 |
| - Serial.print("Input:"); Serial.print(Input); Serial.print(","); |
70 |
| - Serial.print("Output:"); Serial.print(Output); Serial.print(","); |
71 |
| - Serial.println(","); |
| 62 | +void autoTune() { |
| 63 | + const int Rule[10][3] = |
| 64 | + { //ckp, cki, ckd x 1000 |
| 65 | + { 450, 540, 0 }, // ZIEGLER_NICHOLS_PI |
| 66 | + { 600, 176, 75 }, // ZIEGLER_NICHOLS_PID |
| 67 | + { 313, 142, 0 }, // TYREUS_LUYBEN_PI |
| 68 | + { 454, 206, 72 }, // TYREUS_LUYBEN_PID |
| 69 | + { 303, 1212, 0 }, // CIANCONE_MARLIN_PI |
| 70 | + { 303, 1333, 37 }, // CIANCONE_MARLIN_PID |
| 71 | + { 0, 0, 0 }, // AMIGOF_PID |
| 72 | + { 700, 1750, 105 }, // PESSEN_INTEGRAL_PID |
| 73 | + { 333, 667, 111 }, // SOME_OVERSHOOT_PID |
| 74 | + { 200, 400, 67 }, // NO_OVERSHOOT_PID |
| 75 | + }; |
| 76 | + const byte ckp = 0, cki = 1, ckd = 2; //c = column |
| 77 | + |
| 78 | + const byte outputStep = 1; |
| 79 | + const byte hysteresis = 1; |
| 80 | + const int atSetpoint = 341; // 1/3 of 10-bit ADC matches 8-bit PWM value of 85 for symetrical waveform |
| 81 | + const int atOutput = 85; |
72 | 82 |
|
73 |
| - if (myQuickPID.GetDirection() == QuickPID::REVERSE) { |
74 |
| - Input = (1023 - myQuickPID.analogReadFast(inputPin)); // simulate reverse acting controller (cooling) |
75 |
| - } else { |
76 |
| - Input = (myQuickPID.analogReadFast(inputPin)); |
| 83 | + static uint32_t t0, t1, t2, t3; |
| 84 | + static float Ku, Tu, td, kp, ki, kd, rdAvg, peakHigh, peakLow; |
| 85 | + |
| 86 | + switch (aTune) { |
| 87 | + case 0: |
| 88 | + peakHigh = atSetpoint; |
| 89 | + peakLow = atSetpoint; |
| 90 | + if (Print == 1) Serial.print("Stabilizing (33%) →"); |
| 91 | + for (int i = 0; i < 16; i++) avg(Input); // initialize |
| 92 | + Output = 0; |
| 93 | + aTune++; |
| 94 | + break; |
| 95 | + case 1: // start coarse adjust |
| 96 | + if (avg(Input) < (atSetpoint - hysteresis)) { |
| 97 | + Output = atOutput + 20; |
| 98 | + aTune++; |
| 99 | + } |
| 100 | + break; |
| 101 | + case 2: // start fine adjust |
| 102 | + if (avg(Input) > atSetpoint) { |
| 103 | + Output = atOutput - outputStep; |
| 104 | + aTune++; |
| 105 | + } |
| 106 | + break; |
| 107 | + case 3: // run AutoTune |
| 108 | + if (avg(Input) < atSetpoint) { |
| 109 | + if (Print == 1) Serial.print(" Running AutoTune"); |
| 110 | + Output = atOutput + outputStep; |
| 111 | + aTune++; |
| 112 | + } |
| 113 | + break; |
| 114 | + case 4: // get t0 |
| 115 | + if (avg(Input) > atSetpoint) { |
| 116 | + t0 = micros(); |
| 117 | + if (Print == 1) Serial.print(" ↑"); |
| 118 | + aTune++; |
| 119 | + } |
| 120 | + break; |
| 121 | + case 5: // get t1 |
| 122 | + if (avg(Input) > atSetpoint + 0.2) { |
| 123 | + t1 = micros(); |
| 124 | + aTune++; |
| 125 | + } |
| 126 | + break; |
| 127 | + case 6: // get t2 |
| 128 | + rdAvg = avg(Input); |
| 129 | + if (rdAvg > peakHigh) peakHigh = rdAvg; |
| 130 | + if ((rdAvg < peakLow) && (peakHigh >= (atSetpoint + hysteresis))) peakLow = rdAvg; |
| 131 | + |
| 132 | + if (rdAvg > atSetpoint + hysteresis) { |
| 133 | + t2 = micros(); |
| 134 | + if (Print == 1) Serial.print(" ↓"); |
| 135 | + Output = atOutput - outputStep; |
| 136 | + aTune++; |
| 137 | + } |
| 138 | + break; |
| 139 | + case 7: // begin t3 |
| 140 | + rdAvg = avg(Input); |
| 141 | + if (rdAvg > peakHigh) peakHigh = rdAvg; |
| 142 | + if ((rdAvg < peakLow) && (peakHigh >= (atSetpoint + hysteresis))) peakLow = rdAvg; |
| 143 | + if (rdAvg < atSetpoint - hysteresis) { |
| 144 | + if (Print == 1) Serial.print(" ↑"); |
| 145 | + Output = atOutput + outputStep; |
| 146 | + aTune++; |
| 147 | + } |
| 148 | + break; |
| 149 | + case 8: // get t3 |
| 150 | + rdAvg = avg(Input); |
| 151 | + if (rdAvg > peakHigh) peakHigh = rdAvg; |
| 152 | + if ((rdAvg < peakLow) && (peakHigh >= (atSetpoint + hysteresis))) peakLow = rdAvg; |
| 153 | + if (rdAvg > atSetpoint + hysteresis) { |
| 154 | + t3 = micros(); |
| 155 | + if (Print == 1) Serial.println(" Done."); |
| 156 | + aTune++; |
| 157 | + td = (float)(t1 - t0) / 1000000.0; // dead time (seconds) |
| 158 | + Tu = (float)(t3 - t2) / 1000000.0; // ultimate period (seconds) |
| 159 | + Ku = (float)(4 * outputStep * 2) / (float)(3.14159 * sqrt (sq (peakHigh - peakLow) - sq (hysteresis))); // ultimate gain |
| 160 | + if (tuningRule == 6) { //AMIGO_PID |
| 161 | + (td < 10) ? td = 10 : td = td; // 10µs minimum |
| 162 | + kp = (0.2 + 0.45 * (Tu / td)) / Ku; |
| 163 | + float Ti = (((0.4 * td) + (0.8 * Tu)) / (td + (0.1 * Tu)) * td); |
| 164 | + float Td = (0.5 * td * Tu) / ((0.3 * td) + Tu); |
| 165 | + ki = kp / Ti; |
| 166 | + kd = Td * kp; |
| 167 | + } else { //other rules |
| 168 | + kp = Rule[tuningRule][ckp] / 1000.0 * Ku; |
| 169 | + ki = Rule[tuningRule][cki] / 1000.0 * Ku / Tu; |
| 170 | + kd = Rule[tuningRule][ckd] / 1000.0 * Ku * Tu; |
| 171 | + } |
| 172 | + Kp = kp; |
| 173 | + Ki = ki; |
| 174 | + Kd = kd; |
| 175 | + if (Print == 1) { |
| 176 | + // Controllability https://blog.opticontrols.com/wp-content/uploads/2011/06/td-versus-tau.png |
| 177 | + if ((Tu / td + 0.0001) > 0.75) Serial.println("This process is easy to control."); |
| 178 | + else if ((Tu / td + 0.0001) > 0.25) Serial.println("This process has average controllability."); |
| 179 | + else Serial.println("This process is difficult to control."); |
| 180 | + Serial.print("Tu: "); Serial.print(Tu); // Ultimate Period (sec) |
| 181 | + Serial.print(" td: "); Serial.print(td); // Dead Time (sec) |
| 182 | + Serial.print(" Ku: "); Serial.print(Ku); // Ultimate Gain |
| 183 | + Serial.print(" Kp: "); Serial.print(Kp); |
| 184 | + Serial.print(" Ki: "); Serial.print(Ki); |
| 185 | + Serial.print(" Kd: "); Serial.println(Kd); |
| 186 | + Serial.println(); |
| 187 | + } |
| 188 | + } |
| 189 | + break; |
| 190 | + case 9: // ready to set tunings |
| 191 | + aTune++; |
| 192 | + break; |
77 | 193 | }
|
78 |
| - myQuickPID.Compute(); |
79 |
| - analogWrite(outputPin, Output); |
| 194 | +} |
| 195 | + |
| 196 | +float avg(int inputVal) { |
| 197 | + static int arrDat[16]; |
| 198 | + static int pos; |
| 199 | + static long sum; |
| 200 | + pos++; |
| 201 | + if (pos >= 16) pos = 0; |
| 202 | + sum = sum - arrDat[pos] + inputVal; |
| 203 | + arrDat[pos] = inputVal; |
| 204 | + return (float)sum / 16.0; |
80 | 205 | }
|
0 commit comments