Controls both heat and pump pressure based on a temperature probe and a scale- ie, it does temperature and flow profiling. Should work with any vibratory pump machine.
Dependencies: Adafruit_RTCLib FastPWM TSI mbed
main.cpp@2:22d9c714b511, 2013-08-11 (annotated)
- Committer:
- jzeeff
- Date:
- Sun Aug 11 20:39:57 2013 +0000
- Revision:
- 2:22d9c714b511
- Parent:
- 1:b5abc8ddd567
- Child:
- 3:eb60e36b03f6
PWM during closed loop brew
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
jzeeff | 0:24cdf76455c4 | 1 | |
jzeeff | 0:24cdf76455c4 | 2 | // Program to control espresso maker boiler temperatures |
jzeeff | 2:22d9c714b511 | 3 | // Similar to multiple PID control, but uses a flexible open or closed loop table during brew |
jzeeff | 0:24cdf76455c4 | 4 | // Used with a Gaggia Classic, FreeScale FRDM-KL25Z computer, PT1000 RTD, SSR |
jzeeff | 0:24cdf76455c4 | 5 | // Jon Zeeff, 2013 |
jzeeff | 0:24cdf76455c4 | 6 | // Public Domain |
jzeeff | 0:24cdf76455c4 | 7 | |
jzeeff | 0:24cdf76455c4 | 8 | #include "mbed.h" |
jzeeff | 0:24cdf76455c4 | 9 | #include "TSISensor.h" |
jzeeff | 0:24cdf76455c4 | 10 | |
jzeeff | 1:b5abc8ddd567 | 11 | DigitalOut ssr(PTA1); // Solid State Relay |
jzeeff | 1:b5abc8ddd567 | 12 | AnalogIn adc(PTE20); // A/D converter reads temperature |
jzeeff | 0:24cdf76455c4 | 13 | |
jzeeff | 0:24cdf76455c4 | 14 | #define OFF 0 |
jzeeff | 0:24cdf76455c4 | 15 | #define RED 1 |
jzeeff | 0:24cdf76455c4 | 16 | #define GREEN 2 |
jzeeff | 0:24cdf76455c4 | 17 | #define BLUE 3 |
jzeeff | 0:24cdf76455c4 | 18 | #define WHITE 4 |
jzeeff | 0:24cdf76455c4 | 19 | #define YELLOW 5 |
jzeeff | 0:24cdf76455c4 | 20 | |
jzeeff | 2:22d9c714b511 | 21 | // PT1000 RTD ohms (use Google to find a full table) |
jzeeff | 0:24cdf76455c4 | 22 | // 1360 ohms = 94C |
jzeeff | 0:24cdf76455c4 | 23 | // 1000 ohms = too cold (0C) |
jzeeff | 0:24cdf76455c4 | 24 | // 1520 ohms = too hot (136C) |
jzeeff | 0:24cdf76455c4 | 25 | |
jzeeff | 1:b5abc8ddd567 | 26 | // note: assume a 2.2K divider resistor, a PT1000 RTD and a 16 bit A/D result |
jzeeff | 2:22d9c714b511 | 27 | // use this formula: A/D = (RTD_OHMS/(RTD_OHMS+2200)) * 65536 |
jzeeff | 0:24cdf76455c4 | 28 | |
jzeeff | 1:b5abc8ddd567 | 29 | // desired A/D value for boiler temp while idling |
jzeeff | 1:b5abc8ddd567 | 30 | // note: there is an offset between boiler wall temp sensors and actual water temp |
jzeeff | 2:22d9c714b511 | 31 | #define TARGET_TEMP 25900 // CHANGE THIS |
jzeeff | 1:b5abc8ddd567 | 32 | |
jzeeff | 2:22d9c714b511 | 33 | // Table of adjustments (degrees C) to TARGET_TEMP vs time (seconds) into brew cycle (including preheat period) |
jzeeff | 2:22d9c714b511 | 34 | // The idea is that extra heat is needed as cool water comes into the boiler during brew. |
jzeeff | 2:22d9c714b511 | 35 | // Extra heat is provided by a higher than normal boiler wall temp. |
jzeeff | 2:22d9c714b511 | 36 | // NOTE: the decimal portion of the value is used as the PWM value to be applied if more heat is needed. |
jzeeff | 2:22d9c714b511 | 37 | // This can prevent overshoot. |
jzeeff | 2:22d9c714b511 | 38 | |
jzeeff | 2:22d9c714b511 | 39 | const double table[40] = { |
jzeeff | 0:24cdf76455c4 | 40 | // preheat up to 10 seconds |
jzeeff | 2:22d9c714b511 | 41 | 0,0,0,0,18.99,18.99,0,0,0,0, // CHANGE THIS |
jzeeff | 0:24cdf76455c4 | 42 | // brewing (pump is on) up to 30 seconds |
jzeeff | 2:22d9c714b511 | 43 | 18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7,18.7, // CHANGE THIS |
jzeeff | 2:22d9c714b511 | 44 | 15.7,15.7,15.7,15.7,15.7,15.7,15.7,15.7,15.7,15.7,15.7,0,0,0,0 // CHANGE THIS |
jzeeff | 0:24cdf76455c4 | 45 | }; |
jzeeff | 0:24cdf76455c4 | 46 | |
jzeeff | 1:b5abc8ddd567 | 47 | // these probably don't need to be changed if you are using a Gaggia Classic |
jzeeff | 2:22d9c714b511 | 48 | #define CLOSE 30 // how close in A/D value before switching to learned value control |
jzeeff | 1:b5abc8ddd567 | 49 | #define INITIAL_POWER .05 // initial guess for steady state power needed (try .05 = 5%) |
jzeeff | 0:24cdf76455c4 | 50 | #define MIN_TEMP 21000 // below this is an error |
jzeeff | 2:22d9c714b511 | 51 | #define MAX_TEMP 29000 // above this is an error |
jzeeff | 2:22d9c714b511 | 52 | #define ROOM_TEMP 22000 // A/D value at standard ambient room temp |
jzeeff | 1:b5abc8ddd567 | 53 | #define MAX_ROOM_TEMP 22500 // above this means ambient isn't valid |
jzeeff | 0:24cdf76455c4 | 54 | #define SLEEP_TIME 3600 // turn off heat after this many seconds |
jzeeff | 0:24cdf76455c4 | 55 | #define BREW_TIME 30 // max brew time |
jzeeff | 0:24cdf76455c4 | 56 | #define BREW_PREHEAT 10 // max preheat time |
jzeeff | 1:b5abc8ddd567 | 57 | #define AD_PER_DEGREE 44 // how many A/D counts equal a 1 degree C change |
jzeeff | 0:24cdf76455c4 | 58 | #define debug if (1) printf // use if (1) or if (0) |
jzeeff | 0:24cdf76455c4 | 59 | |
jzeeff | 0:24cdf76455c4 | 60 | void brew(void); |
jzeeff | 0:24cdf76455c4 | 61 | void set_color(int color); |
jzeeff | 0:24cdf76455c4 | 62 | unsigned read_ad(void); |
jzeeff | 1:b5abc8ddd567 | 63 | |
jzeeff | 1:b5abc8ddd567 | 64 | unsigned ambient_temp; // room or water tank temp |
jzeeff | 1:b5abc8ddd567 | 65 | double heat = INITIAL_POWER; // initial fractional heat needed while idle |
jzeeff | 1:b5abc8ddd567 | 66 | |
jzeeff | 0:24cdf76455c4 | 67 | int main() |
jzeeff | 0:24cdf76455c4 | 68 | { |
jzeeff | 0:24cdf76455c4 | 69 | time_t prev_time = 0; |
jzeeff | 0:24cdf76455c4 | 70 | TSISensor tsi; // used as a start button |
jzeeff | 1:b5abc8ddd567 | 71 | ambient_temp = read_ad(); // save temp on startup |
jzeeff | 0:24cdf76455c4 | 72 | |
jzeeff | 0:24cdf76455c4 | 73 | set_time(0); // start clock at zero |
jzeeff | 0:24cdf76455c4 | 74 | |
jzeeff | 0:24cdf76455c4 | 75 | debug("starting A/D value/temp = %u\r\n",ambient_temp); |
jzeeff | 0:24cdf76455c4 | 76 | |
jzeeff | 0:24cdf76455c4 | 77 | // loop forever, controlling boiler temperature |
jzeeff | 0:24cdf76455c4 | 78 | |
jzeeff | 0:24cdf76455c4 | 79 | for (;;) { |
jzeeff | 0:24cdf76455c4 | 80 | // read temp from A/D |
jzeeff | 0:24cdf76455c4 | 81 | // note: in A/D counts, not degrees |
jzeeff | 2:22d9c714b511 | 82 | unsigned temp = read_ad(); |
jzeeff | 0:24cdf76455c4 | 83 | |
jzeeff | 0:24cdf76455c4 | 84 | // bang/bang when far away, PWM to learned value when close |
jzeeff | 0:24cdf76455c4 | 85 | if (temp > TARGET_TEMP + CLOSE) { |
jzeeff | 0:24cdf76455c4 | 86 | ssr = 0; // turn off heater |
jzeeff | 0:24cdf76455c4 | 87 | set_color(GREEN); // set LED to green |
jzeeff | 0:24cdf76455c4 | 88 | } else if (temp < TARGET_TEMP - CLOSE) { |
jzeeff | 0:24cdf76455c4 | 89 | ssr = 1; // turn on heater |
jzeeff | 0:24cdf76455c4 | 90 | set_color(RED); // set LED to red |
jzeeff | 0:24cdf76455c4 | 91 | } else { // close to target temp |
jzeeff | 0:24cdf76455c4 | 92 | // learning mode - adjust heat, the fraction of time power should be on |
jzeeff | 1:b5abc8ddd567 | 93 | |
jzeeff | 0:24cdf76455c4 | 94 | if (temp > TARGET_TEMP) // adjust best guess for % heat needed |
jzeeff | 1:b5abc8ddd567 | 95 | heat *= .98; |
jzeeff | 0:24cdf76455c4 | 96 | else |
jzeeff | 1:b5abc8ddd567 | 97 | heat *= 1.02; |
jzeeff | 0:24cdf76455c4 | 98 | |
jzeeff | 0:24cdf76455c4 | 99 | debug("learned heat = %F, temp = %u\r\n",heat, temp); |
jzeeff | 0:24cdf76455c4 | 100 | ssr = 1; // turn on heater for PWM |
jzeeff | 0:24cdf76455c4 | 101 | set_color(RED); |
jzeeff | 0:24cdf76455c4 | 102 | wait(heat); |
jzeeff | 0:24cdf76455c4 | 103 | ssr = 0; // turn off heater |
jzeeff | 0:24cdf76455c4 | 104 | set_color(GREEN); |
jzeeff | 0:24cdf76455c4 | 105 | wait(1-heat); |
jzeeff | 0:24cdf76455c4 | 106 | } // if |
jzeeff | 0:24cdf76455c4 | 107 | |
jzeeff | 0:24cdf76455c4 | 108 | // the user must press a button 10 seconds prior to brewing to start preheat |
jzeeff | 0:24cdf76455c4 | 109 | if (tsi.readPercentage() > .5) |
jzeeff | 0:24cdf76455c4 | 110 | brew(); |
jzeeff | 0:24cdf76455c4 | 111 | |
jzeeff | 0:24cdf76455c4 | 112 | // check for idle, sleep till tomorrow if it occurs |
jzeeff | 0:24cdf76455c4 | 113 | if (time(NULL) > SLEEP_TIME){ // save power |
jzeeff | 0:24cdf76455c4 | 114 | static time_t wakeup_time = (24 * 60 * 60) - (20 * 60); // 24 hours minus 20 min |
jzeeff | 0:24cdf76455c4 | 115 | |
jzeeff | 0:24cdf76455c4 | 116 | ssr = 0; // turn off heater |
jzeeff | 2:22d9c714b511 | 117 | set_color(OFF); |
jzeeff | 2:22d9c714b511 | 118 | printf("sleep\r\n"); |
jzeeff | 0:24cdf76455c4 | 119 | while (time(NULL) < wakeup_time) // wait till tomorrow |
jzeeff | 0:24cdf76455c4 | 120 | wait(1); |
jzeeff | 0:24cdf76455c4 | 121 | set_time(0); // clock runs zero to 24 hours |
jzeeff | 1:b5abc8ddd567 | 122 | wakeup_time = (24 * 60 * 60); // no 20 min offset needed now |
jzeeff | 1:b5abc8ddd567 | 123 | ambient_temp = read_ad(); // save temp on startup |
jzeeff | 0:24cdf76455c4 | 124 | } |
jzeeff | 0:24cdf76455c4 | 125 | |
jzeeff | 0:24cdf76455c4 | 126 | // check for errors (incorrect boiler temp can be dangerous) |
jzeeff | 0:24cdf76455c4 | 127 | if (temp > MAX_TEMP || temp < MIN_TEMP) { |
jzeeff | 0:24cdf76455c4 | 128 | ssr = 0; // turn off heater |
jzeeff | 0:24cdf76455c4 | 129 | set_color(YELLOW); // set LED to indicate error |
jzeeff | 1:b5abc8ddd567 | 130 | debug("error A/D = %u\r\n",temp); |
jzeeff | 0:24cdf76455c4 | 131 | for (;;); // reset needed to exit this |
jzeeff | 0:24cdf76455c4 | 132 | } |
jzeeff | 0:24cdf76455c4 | 133 | |
jzeeff | 1:b5abc8ddd567 | 134 | if (time(NULL) > prev_time) debug("A/D value = %u\r\n",temp); // every second |
jzeeff | 1:b5abc8ddd567 | 135 | prev_time = time(NULL); |
jzeeff | 0:24cdf76455c4 | 136 | |
jzeeff | 1:b5abc8ddd567 | 137 | } // for (;;) |
jzeeff | 0:24cdf76455c4 | 138 | |
jzeeff | 0:24cdf76455c4 | 139 | } // main() |
jzeeff | 0:24cdf76455c4 | 140 | |
jzeeff | 0:24cdf76455c4 | 141 | |
jzeeff | 0:24cdf76455c4 | 142 | //================================================================= |
jzeeff | 0:24cdf76455c4 | 143 | // This subroutine is called when the button is pressed, 10 seconds |
jzeeff | 1:b5abc8ddd567 | 144 | // before the pump is started. It does open loop PWM power/heat control. |
jzeeff | 0:24cdf76455c4 | 145 | //================================================================= |
jzeeff | 0:24cdf76455c4 | 146 | |
jzeeff | 0:24cdf76455c4 | 147 | void brew(void) |
jzeeff | 0:24cdf76455c4 | 148 | { |
jzeeff | 1:b5abc8ddd567 | 149 | unsigned start_time = time(NULL); |
jzeeff | 1:b5abc8ddd567 | 150 | |
jzeeff | 1:b5abc8ddd567 | 151 | double adjust = 1; // default is no adjustment |
jzeeff | 1:b5abc8ddd567 | 152 | |
jzeeff | 2:22d9c714b511 | 153 | // adjust for higher or lower tank temp (assumed to be equal to ambient at startup) |
jzeeff | 1:b5abc8ddd567 | 154 | if (ambient_temp < MAX_ROOM_TEMP) // sanity check |
jzeeff | 1:b5abc8ddd567 | 155 | adjust = (double)(ROOM_TEMP - TARGET_TEMP) / (double)(ambient_temp - TARGET_TEMP); |
jzeeff | 1:b5abc8ddd567 | 156 | |
jzeeff | 1:b5abc8ddd567 | 157 | debug("preheat/brew start, adjust = %F\r\n", adjust); |
jzeeff | 0:24cdf76455c4 | 158 | set_color(WHITE); |
jzeeff | 0:24cdf76455c4 | 159 | |
jzeeff | 1:b5abc8ddd567 | 160 | for (;;) { |
jzeeff | 2:22d9c714b511 | 161 | unsigned brew_time; |
jzeeff | 2:22d9c714b511 | 162 | static unsigned prev_brew_time; |
jzeeff | 1:b5abc8ddd567 | 163 | |
jzeeff | 2:22d9c714b511 | 164 | brew_time = time(NULL) - start_time; |
jzeeff | 2:22d9c714b511 | 165 | |
jzeeff | 1:b5abc8ddd567 | 166 | if (brew_time >= BREW_PREHEAT + BREW_TIME) |
jzeeff | 1:b5abc8ddd567 | 167 | break; |
jzeeff | 1:b5abc8ddd567 | 168 | |
jzeeff | 1:b5abc8ddd567 | 169 | if (brew_time == BREW_PREHEAT) |
jzeeff | 0:24cdf76455c4 | 170 | set_color(BLUE); // set LED color to blue for start brew/pump now |
jzeeff | 2:22d9c714b511 | 171 | |
jzeeff | 2:22d9c714b511 | 172 | double pwm = table[brew_time] - (int)table[brew_time]; // decimal part only |
jzeeff | 2:22d9c714b511 | 173 | |
jzeeff | 2:22d9c714b511 | 174 | // if too cold, apply the PWM value, if too hot, do nothing |
jzeeff | 2:22d9c714b511 | 175 | if (read_ad() < (TARGET_TEMP + (table[brew_time] * AD_PER_DEGREE)) * adjust) { |
jzeeff | 2:22d9c714b511 | 176 | ssr = 1; |
jzeeff | 2:22d9c714b511 | 177 | wait(pwm / 2); |
jzeeff | 2:22d9c714b511 | 178 | ssr = 0; |
jzeeff | 2:22d9c714b511 | 179 | wait((1 - pwm) / 2); |
jzeeff | 2:22d9c714b511 | 180 | } // if PWM |
jzeeff | 0:24cdf76455c4 | 181 | |
jzeeff | 2:22d9c714b511 | 182 | if (brew_time != prev_brew_time) // print status every second |
jzeeff | 2:22d9c714b511 | 183 | debug("target temp %u = %F, temp = %u\r\n",brew_time,table[brew_time],read_ad()); |
jzeeff | 1:b5abc8ddd567 | 184 | prev_brew_time = brew_time; |
jzeeff | 0:24cdf76455c4 | 185 | |
jzeeff | 1:b5abc8ddd567 | 186 | } // for |
jzeeff | 1:b5abc8ddd567 | 187 | |
jzeeff | 1:b5abc8ddd567 | 188 | ssr = 0; |
jzeeff | 0:24cdf76455c4 | 189 | debug("brew done\r\n"); |
jzeeff | 0:24cdf76455c4 | 190 | |
jzeeff | 0:24cdf76455c4 | 191 | } // brew() |
jzeeff | 0:24cdf76455c4 | 192 | |
jzeeff | 0:24cdf76455c4 | 193 | |
jzeeff | 0:24cdf76455c4 | 194 | // ============================================= |
jzeeff | 0:24cdf76455c4 | 195 | // set multi color LED state |
jzeeff | 0:24cdf76455c4 | 196 | // ============================================= |
jzeeff | 0:24cdf76455c4 | 197 | |
jzeeff | 0:24cdf76455c4 | 198 | DigitalOut r (LED_RED); |
jzeeff | 0:24cdf76455c4 | 199 | DigitalOut g (LED_GREEN); |
jzeeff | 0:24cdf76455c4 | 200 | DigitalOut b (LED_BLUE); |
jzeeff | 0:24cdf76455c4 | 201 | |
jzeeff | 0:24cdf76455c4 | 202 | void set_color(int color) |
jzeeff | 0:24cdf76455c4 | 203 | { |
jzeeff | 0:24cdf76455c4 | 204 | |
jzeeff | 0:24cdf76455c4 | 205 | // turn off |
jzeeff | 0:24cdf76455c4 | 206 | r = g = b = 1; |
jzeeff | 0:24cdf76455c4 | 207 | |
jzeeff | 0:24cdf76455c4 | 208 | switch (color) { |
jzeeff | 0:24cdf76455c4 | 209 | case OFF: |
jzeeff | 0:24cdf76455c4 | 210 | break; |
jzeeff | 0:24cdf76455c4 | 211 | case GREEN: |
jzeeff | 0:24cdf76455c4 | 212 | g = 0; |
jzeeff | 0:24cdf76455c4 | 213 | break; |
jzeeff | 0:24cdf76455c4 | 214 | case BLUE: |
jzeeff | 0:24cdf76455c4 | 215 | b = 0; |
jzeeff | 0:24cdf76455c4 | 216 | break; |
jzeeff | 0:24cdf76455c4 | 217 | case RED: |
jzeeff | 0:24cdf76455c4 | 218 | r = 0; |
jzeeff | 0:24cdf76455c4 | 219 | break; |
jzeeff | 0:24cdf76455c4 | 220 | case YELLOW: |
jzeeff | 0:24cdf76455c4 | 221 | r = g = 0; |
jzeeff | 0:24cdf76455c4 | 222 | break; |
jzeeff | 0:24cdf76455c4 | 223 | case WHITE: |
jzeeff | 0:24cdf76455c4 | 224 | r = g = b = 0; |
jzeeff | 0:24cdf76455c4 | 225 | break; |
jzeeff | 0:24cdf76455c4 | 226 | } // switch |
jzeeff | 0:24cdf76455c4 | 227 | |
jzeeff | 0:24cdf76455c4 | 228 | } // set_color() |
jzeeff | 0:24cdf76455c4 | 229 | |
jzeeff | 0:24cdf76455c4 | 230 | //======================================= |
jzeeff | 0:24cdf76455c4 | 231 | // read A/D value from RTD |
jzeeff | 0:24cdf76455c4 | 232 | // median for accuracy |
jzeeff | 0:24cdf76455c4 | 233 | //======================================= |
jzeeff | 0:24cdf76455c4 | 234 | |
jzeeff | 0:24cdf76455c4 | 235 | unsigned read_ad(void) |
jzeeff | 0:24cdf76455c4 | 236 | { |
jzeeff | 1:b5abc8ddd567 | 237 | |
jzeeff | 1:b5abc8ddd567 | 238 | uint32_t sum=0; |
jzeeff | 1:b5abc8ddd567 | 239 | int i; |
jzeeff | 0:24cdf76455c4 | 240 | |
jzeeff | 1:b5abc8ddd567 | 241 | for (i = 0; i < 3; ++i) { // average multiple for more accuracy |
jzeeff | 1:b5abc8ddd567 | 242 | unsigned a, b, c; |
jzeeff | 1:b5abc8ddd567 | 243 | |
jzeeff | 1:b5abc8ddd567 | 244 | a = adc.read_u16(); // take median of 3 values |
jzeeff | 0:24cdf76455c4 | 245 | b = adc.read_u16(); |
jzeeff | 0:24cdf76455c4 | 246 | c = adc.read_u16(); |
jzeeff | 0:24cdf76455c4 | 247 | |
jzeeff | 1:b5abc8ddd567 | 248 | if ((a >= b && a <= c) || (a >= c && a <= b)) sum += a; |
jzeeff | 1:b5abc8ddd567 | 249 | else if ((b >= a && b <= c) || (b >= c && b <= a)) sum += b; |
jzeeff | 1:b5abc8ddd567 | 250 | else sum += c; |
jzeeff | 1:b5abc8ddd567 | 251 | } // for |
jzeeff | 1:b5abc8ddd567 | 252 | |
jzeeff | 1:b5abc8ddd567 | 253 | return sum / 3; |
jzeeff | 0:24cdf76455c4 | 254 | |
jzeeff | 0:24cdf76455c4 | 255 | } // read_ad() |
jzeeff | 0:24cdf76455c4 | 256 |