├── README.md ├── Toast-R-Reflow.ino ├── Toast-R-Reflow (84).ino ├── LICENSE └── Toast-R-Reflow (II).ino /README.md: -------------------------------------------------------------------------------- 1 | Toast-R-Reflow 2 | ============== 3 | 4 | Yet another attempt to convert a Toaster-oven into a SMD Reflow oven. 5 | 6 | This code is going to be paired with an ATTiny 85 based controller board to run the process. An AD854x thermocouple 7 | amplifier will allow a thermocouple to drive an analog input pin on the controller. Two of the pins will be used to 8 | independently control the two heating elements of the oven. The last two available pins will be used with TinyWireM 9 | as an i2c bus to drive an Adafruit 2 line display shield or backpack. 10 | 11 | This project contains two versions of the code: 12 | 13 | Toast-R-Reflow.ino is the orignal version, intended for the ATTiny85 + i2c display. This is intended for the original standalone controller as well as backpack controllers with versions less than 1.0. 14 | 15 | Toast-R-Reflow (84).ino is the version for ATTiny84 backpacks, which is backpacks starting with version 1.0. 16 | 17 | ORIGINAL CODE 18 | ============= 19 | 20 | This project requires the following libraries: 21 | 22 | * TinyWireM: https://github.com/adafruit/TinyWireM 23 | * PID: https://github.com/br3ttb/Arduino-PID-Library 24 | * LiquidTWI2: https://github.com/lincomatic/LiquidTWI2 25 | 26 | It also will require some sort of Arduino IDE support for ATTiny controllers. I used the MIT patch at 27 | http://hlt.media.mit.edu/?p=1695 originally, but with newer Arduino packages, you should be able to 28 | use the board manager and find support for raw ATTiny chips. 29 | 30 | Note that as of version 0.5, this code tickles a bug in Arduino that you need to work around. 31 | 32 | You need to find the file hardware/arduino/cores/arduino/wiring_analog.c and apply this patch: 33 | 34 | ``` 35 | --- wiring_analog.c.orig 2013-12-12 12:44:11.000000000 -0800 36 | +++ wiring_analog.c 2013-12-12 12:16:16.000000000 -0800 37 | @@ -66,7 +66,7 @@ 38 | // channel (low 4 bits). this also sets ADLAR (left-adjust result) 39 | // to 0 (the default). 40 | #if defined(ADMUX) 41 | - ADMUX = (analog_reference << 6) | (pin & 0x07); 42 | + ADMUX = (analog_reference << 6) | (pin & 0x07) | ((analog_reference & 0x4)?0x10:0); 43 | #endif 44 | 45 | // without a delay, we seem to read from the wrong channel 46 | ``` 47 | 48 | That will allow analogReference() to set the correct ADMUX bits when the value is greater than 4, which is 49 | what turns on the 2.56 volt reference. 50 | 51 | 52 | ATTiny84 VERSION 53 | ================ 54 | 55 | This version requires: 56 | 57 | * PID: https://github.com/br3ttb/Arduino-PID-Library 58 | * LiquidCrystal (built-in) 59 | 60 | This version also requires ATTiny support, but does not require the patch to wiring_analog.c. 61 | 62 | You should configure the IDE for a raw ATTiny84 running at 8 MHz from its internal oscillator. The fuse settings 63 | for that are: low = 0xE2, high = 0xDF, extended = 0xFF 64 | 65 | Model II VERSION 66 | ================ 67 | 68 | This version has the same library requirements as the ATTiny84 version, but does not require ATTiny support. Instead, 69 | it requires a patch to boards.txt to add support for "raw" ATMega328P chips without the Arduino bootloader and 70 | programming using an AVR ISP programmer. 71 | 72 | Here's a suitable addition: 73 | 74 | ``` 75 | ############################################################## 76 | 77 | usbtiny328.name=[usbtinyisp]ATmega328 78 | 79 | usbtiny328.upload.using=usbtinyisp 80 | usbtiny328.upload.maximum_size=32768 81 | 82 | usbtiny328.build.mcu=atmega328p 83 | usbtiny328.build.f_cpu=16000000L 84 | usbtiny328.build.core=arduino 85 | usbtiny328.build.variant=standard 86 | ``` 87 | 88 | As with the model I controller, with newer Arduino IDEs, the board manager has options for "raw" ATMega chips. 89 | 90 | Alternatively, you can load an Arduino bootloader in. If you do this, you can pretend that the board is an Arduino UNO 91 | for the purposes of bootloading and compiling the firmware. To load the firmware this way, use an FTDI cable and connect 92 | to the 6 pin serial header on the board and upload the firmware in the usual Arduino manner. 93 | -------------------------------------------------------------------------------- /Toast-R-Reflow.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Toast-R-Reflow 4 | Copyright 2013 Nicholas W. Sayer 5 | 6 | This program is free software; you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation; either version 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with this program; if not, write to the Free Software Foundation, Inc., 18 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | // This is the analogReference value for the ATTiny 2.56v internal 27 | // reference, with no external cap bypass. 28 | // 29 | // Note that for this to work, you need to patch wiring_analog.c to set 30 | // the REFS2 bit in ADMUX when bit 2 is set. 31 | #define INTERNAL2V56_NO_CAP 6 32 | 33 | #define LCD_I2C_ADDR 0x20 // for adafruit shield or backpack 34 | 35 | // This is the temperature reading analog pin. 36 | #define TEMP_SENSOR_PIN A3 37 | 38 | // These pins turn on the two heating elements. 39 | #define ELEMENT_ONE_PIN 1 40 | #define ELEMENT_TWO_PIN 4 41 | 42 | // This value is A/D units per degree C 43 | // 44 | // The magic here is that with a 2.56 volt analog reference, 45 | // the mV per units is *exactly* 2.5. Since the AD8495 output is 46 | // 5 mV per degree, then we just need to divide the reading by 2 47 | // to get the temp. 48 | #define TEMP_SCALING_FACTOR 2.0 49 | 50 | // How often do we update the displayed temp? 51 | #define DISPLAY_UPDATE_INTERVAL 500 52 | 53 | // A reasonable approximation of room temperature. 54 | #define AMBIENT_TEMP 25.0 55 | 56 | // fiddle these knobs 57 | #define K_P 150 58 | #define K_I 0.1 59 | #define K_D 10 60 | 61 | // The number of milliseconds for each cycle of the control output. 62 | // The duty cycle is adjusted by the PID. 63 | #define PWM_PULSE_WIDTH 1000 64 | 65 | // To get a temperature reading, read it a bunch of times and take the average. 66 | // This will cut down on the noise. 67 | #define SAMPLE_COUNT 3 68 | 69 | // Which button do we use? 70 | #define BUTTON BUTTON_SELECT 71 | // If we see any state change on the button, we ignore all changes for this long 72 | #define BUTTON_DEBOUNCE_INTERVAL 50 73 | // How long does the button have to stay down before we call it a LONG push? 74 | #define BUTTON_LONG_START 250 75 | 76 | #define EVENT_NONE 0 77 | #define EVENT_SHORT_PUSH 1 78 | #define EVENT_LONG_PUSH 2 79 | 80 | #define VERSION "0.5" 81 | 82 | struct curve_point { 83 | // Display this string on the display during this phase. Maximum 8 characters long. 84 | const char *phase_name; 85 | // The duration of this phase, in milliseconds 86 | unsigned long duration_millis; 87 | // The setpoint will drift smoothly across the phase from the last 88 | // set point to this temperature, arriving there at the very end. 89 | double target_temp; 90 | }; 91 | 92 | // This table is the complete operational profile of the oven. 93 | // This example is intended for tin-lead based paste. For RoHS 94 | // solder, you'll need to adjust it. 95 | const struct curve_point profile[] = { 96 | // Drift from the ambient temperature to 150 deg C over 90 seconds. 97 | { "Preheat", 90000, 150.0 }, 98 | // Drift more slowly up to 180 deg C over 60 seconds. 99 | { "Soak", 60000, 180.0 }, 100 | // This entry will cause the setpoint to "snap" to the next temperature rather 101 | // than drift over the course of an interval. The name won't be displayed because the duration is 0, 102 | // but a NULL name will end the table, so use an empty string instead. 103 | // This will force the oven to move to the reflow temperature as quickly as possible. 104 | { "", 0, 225.0 }, 105 | // It's going to take around 60 seconds to get to peak, but then we're done. 106 | { "Reflow", 75000, 225.0 }, 107 | // There is a maximum cooling rate to avoid thermal shock. The oven will likely cool slower than 108 | // this on its own anyway. It might be a good idea to open the door a bit, but if you get over-agressive 109 | // with cooling, then this entry will compensate for that. 110 | { "Cool", 60000, 150.0 }, 111 | // This entry ends the table. Don't leave it out! 112 | { NULL, 0, 0.0 } 113 | }; 114 | 115 | LiquidTWI2 display(LCD_I2C_ADDR, 0); 116 | 117 | unsigned long start_time, pwm_time, lastDisplayUpdate, button_debounce_time, button_press_time; 118 | unsigned int display_mode; 119 | 120 | double setPoint, currentTemp, outputDuty; 121 | 122 | PID pid(¤tTemp, &outputDuty, &setPoint, K_P, K_I, K_D, DIRECT); 123 | 124 | // Look for button events. We support "short" pushes and "long" pushes. 125 | // This method is responsible for debouncing and timing the pushes. 126 | unsigned int checkEvent() { 127 | unsigned long now = millis(); 128 | if (button_debounce_time != 0) { 129 | if (now - button_debounce_time < BUTTON_DEBOUNCE_INTERVAL) { 130 | // debounce is in progress 131 | return EVENT_NONE; 132 | } else { 133 | // debounce is over 134 | button_debounce_time = 0; 135 | } 136 | } 137 | unsigned int buttons = display.readButtons(); 138 | if ((buttons & BUTTON) != 0) { 139 | // Button is down 140 | if (button_press_time == 0) { // this is the start of a press. 141 | button_debounce_time = button_press_time = now; 142 | } 143 | return EVENT_NONE; // We don't know what this button-push is going to be yet 144 | } else { 145 | // Button released 146 | if (button_press_time == 0) return EVENT_NONE; // It wasn't down anyway. 147 | // We are now ending a button-push. First, start debuncing. 148 | button_debounce_time = now; 149 | unsigned long push_duration = now - button_press_time; 150 | button_press_time = 0; 151 | if (push_duration > BUTTON_LONG_START) { 152 | return EVENT_LONG_PUSH; 153 | } else { 154 | return EVENT_SHORT_PUSH; 155 | } 156 | } 157 | } 158 | 159 | // Format and display a temperature value. 160 | void displayTemp(double temp) { 161 | if (temp < 10) display.print(' '); 162 | if (temp < 100) display.print(' '); 163 | display.print((int)temp); 164 | display.print('.'); 165 | display.print(((int)(temp * 10.0)) % 10); 166 | display.print((char)0xDF); // magic "degree" character 167 | display.print('C'); 168 | } 169 | 170 | // Sample the temperature pin a few times and take an average. 171 | // Figure out the voltage, and then the temperature from that. 172 | void updateTemp() { 173 | unsigned long sum = 0, mv; 174 | analogRead(TEMP_SENSOR_PIN); // throw this one away 175 | for(int i = 0; i < SAMPLE_COUNT; i++) { 176 | delay(10); 177 | mv = analogRead(TEMP_SENSOR_PIN); 178 | sum += mv; 179 | } 180 | mv = sum / SAMPLE_COUNT; 181 | currentTemp = ((double)mv) / TEMP_SCALING_FACTOR; 182 | } 183 | 184 | // Call this when the cycle is finished. Also, call it at 185 | // startup to initialize everything. 186 | void finish() { 187 | start_time = 0; 188 | pwm_time = 0; 189 | pid.SetMode(MANUAL); 190 | digitalWrite(ELEMENT_ONE_PIN, LOW); 191 | digitalWrite(ELEMENT_TWO_PIN, LOW); 192 | display.setBacklight(YELLOW); 193 | display.clear(); 194 | display.print("Waiting"); 195 | } 196 | 197 | // Which phase are we in now? (or -1 for finished) 198 | static int getCurrentPhase(unsigned long time) { 199 | unsigned long so_far = 0; 200 | for(int i = 0; profile[i].phase_name != NULL; i++) { 201 | if (so_far + profile[i].duration_millis > time) { // we're in THIS portion of the profile 202 | return i; 203 | } 204 | so_far += profile[i].duration_millis; 205 | } 206 | return -1; 207 | } 208 | 209 | // How many milliseconds into a cycle does the given phase number start? 210 | static unsigned long phaseStartTime(int phase) { 211 | unsigned long so_far = 0; 212 | for(int i = 0; i < phase; i++) 213 | so_far += profile[i].duration_millis; 214 | return so_far; 215 | } 216 | 217 | void setup() { 218 | display.setMCPType(LTI_TYPE_MCP23017); 219 | display.begin(16, 2); 220 | 221 | analogReference(INTERNAL2V56_NO_CAP); 222 | pinMode(ELEMENT_ONE_PIN, OUTPUT); 223 | pinMode(ELEMENT_TWO_PIN, OUTPUT); 224 | digitalWrite(ELEMENT_ONE_PIN, LOW); 225 | digitalWrite(ELEMENT_TWO_PIN, LOW); 226 | 227 | pid.SetOutputLimits(0.0, PWM_PULSE_WIDTH); 228 | pid.SetMode(MANUAL); 229 | 230 | lastDisplayUpdate = 0; 231 | start_time = 0; 232 | button_debounce_time = 0; 233 | button_press_time = 0; 234 | display_mode = 0; 235 | 236 | display.clear(); 237 | display.setBacklight(WHITE); 238 | 239 | display.setCursor(0,0); 240 | display.print("Toast-R-Reflow"); 241 | display.setCursor(0, 1); 242 | display.print(VERSION); 243 | 244 | delay(2000); 245 | finish(); 246 | } 247 | 248 | void loop() { 249 | updateTemp(); 250 | boolean doDisplayUpdate = false; 251 | { 252 | unsigned long now = millis(); 253 | if (lastDisplayUpdate == 0 || now - lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) { 254 | doDisplayUpdate = true; 255 | lastDisplayUpdate = now; 256 | display.setCursor(0, 1); 257 | displayTemp(currentTemp); 258 | } 259 | } 260 | if (start_time == 0) { 261 | // We're not running. Wait for the button. 262 | unsigned int event = checkEvent(); 263 | switch(event) { 264 | case EVENT_SHORT_PUSH: 265 | case EVENT_LONG_PUSH: 266 | display.setBacklight(GREEN); 267 | // We really want to just re-initialize the PID. 268 | // The only way to do that with the existing API 269 | // is to transition from MANUAL to AUTOMATIC. 270 | // For this reason, setup() and finish() will 271 | // set the mode to MANUAL (which is otherwise 272 | // pointless because we don't call Compute() when 273 | // the oven isn't running). 274 | pid.SetMode(AUTOMATIC); 275 | start_time = millis(); 276 | return; 277 | } 278 | 279 | } else { 280 | // We're running. 281 | unsigned long now = millis(); 282 | unsigned long profile_time = now - start_time; 283 | int currentPhase = getCurrentPhase(profile_time); 284 | if (currentPhase < 0) { 285 | // All done! 286 | finish(); 287 | return; 288 | } 289 | 290 | unsigned int event = checkEvent(); 291 | switch(event) { 292 | case EVENT_SHORT_PUSH: 293 | case EVENT_LONG_PUSH: 294 | display_mode ^= 1; // pick the other mode 295 | break; 296 | } 297 | if (doDisplayUpdate) { 298 | // more display updates to do. 299 | 300 | // The time 301 | unsigned long profile_time = now - start_time; 302 | unsigned int profile_sec = profile_time / 1000; 303 | unsigned int profile_min = profile_sec / 60; 304 | profile_sec %= 60; 305 | display.setCursor(10, 0); 306 | if (profile_min < 10) display.print('0'); 307 | display.print(profile_min); 308 | display.print(':'); 309 | if (profile_sec < 10) display.print('0'); 310 | display.print(profile_sec); 311 | 312 | // The phase name 313 | display.setCursor(0, 0); 314 | display.print(profile[currentPhase].phase_name); 315 | for(unsigned int j = 0; j < 8 - strlen(profile[currentPhase].phase_name); j++) display.print(' '); 316 | 317 | display.setCursor(8, 1); 318 | switch(display_mode) { 319 | case 0: 320 | // the setpoint 321 | displayTemp(setPoint); 322 | break; 323 | case 1: 324 | // the oven power 325 | int mils = (outputDuty * 1000) / PWM_PULSE_WIDTH; 326 | if (mils < 1000) display.print(' '); 327 | display.print(mils / 10); 328 | display.print('.'); 329 | display.print(mils % 10); 330 | display.print('%'); 331 | display.print(' '); 332 | display.print(' '); 333 | break; 334 | } 335 | } 336 | // The concept here is that we have two heating elements 337 | // that we can independently control. 338 | // 339 | // We could just turn them on and off at the same time, but 340 | // then why did we go to the trouble of buying two triacs? 341 | // 342 | // Instead, we can try and arrange them to pulse at different times. 343 | // This will help encourage convection (hopefully), as well as 344 | // reducing the instantaneous power demand (at least when the duty cycle 345 | // is less than 50%). 346 | // 347 | // So start one of them (#2) at the beginning of the interval, and end the other (#1) 348 | // at the end of the interval. 349 | if (pwm_time == 0 || now - pwm_time > PWM_PULSE_WIDTH) { 350 | // Time to start a new PWM interval. 351 | pwm_time = now; 352 | // Turn element one off. We may turn it on later. 353 | digitalWrite(ELEMENT_ONE_PIN, LOW); 354 | // Only start element two if we're actually going to do *anything* 355 | // We will turn it off later. 356 | digitalWrite(ELEMENT_TWO_PIN, (outputDuty > 0.0)?HIGH:LOW); 357 | } else { 358 | // We're somewhere in the middle of the current interval. 359 | unsigned long place_in_pulse = now - pwm_time; 360 | if (place_in_pulse >= outputDuty) 361 | digitalWrite(ELEMENT_TWO_PIN, LOW); // their pulse is over - turn the juice off 362 | if (place_in_pulse >= (PWM_PULSE_WIDTH - outputDuty)) 363 | digitalWrite(ELEMENT_ONE_PIN, HIGH); // their pulse is ready to begin - turn the juice on 364 | } 365 | 366 | // Now update the set point. 367 | // What was the last target temp? That's where we're coming *from* in this phase 368 | double last_temp = (currentPhase == 0)?AMBIENT_TEMP:profile[currentPhase - 1].target_temp; 369 | // Where are we in this phase? 370 | unsigned long position_in_phase = profile_time - phaseStartTime(currentPhase); 371 | // What fraction of the current phase is that? 372 | double fraction_of_phase = ((double)position_in_phase) / ((double) profile[currentPhase].duration_millis); 373 | // How much is the temperature going to change during this phase? 374 | double temp_delta = profile[currentPhase].target_temp - last_temp; 375 | // The set point is the fraction of the delta that's the same as the fraction of the complete phase. 376 | setPoint = temp_delta * fraction_of_phase + last_temp; 377 | 378 | pid.Compute(); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /Toast-R-Reflow (84).ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Toast-R-Reflow (84) 4 | Copyright 2013 Nicholas W. Sayer 5 | 6 | This program is free software; you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation; either version 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with this program; if not, write to the Free Software Foundation, Inc., 18 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | */ 21 | 22 | /* 23 | 24 | This version of the code is designed for the ATTiny84 variant of the controller 25 | board, which is hardware versions > 1.0 26 | 27 | */ 28 | 29 | // Versions 1.2 and beyond include an external reference voltage. For versions 1.0 and 1.1 30 | // comment out this line 31 | #define EXTERNAL_REF 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | // The pins connected up to the LCD. 39 | #define LCD_D4 7 40 | #define LCD_D5 6 41 | #define LCD_D6 5 42 | #define LCD_D7 4 43 | #define LCD_RS 3 44 | #ifdef EXTERNAL_REF 45 | #define LCD_E 2 46 | #define LCD_RW 255 // This tells LiquidCrystal that there is no RW connection 47 | #else 48 | #define LCD_RW 2 49 | #define LCD_E 1 50 | #endif 51 | 52 | // This is the temperature reading analog pin. 53 | #ifdef EXTERNAL_REF 54 | #define TEMP_SENSOR_PIN A1 55 | #else 56 | #define TEMP_SENSOR_PIN A0 57 | #endif 58 | 59 | // These pins turn on the two heating elements. 60 | #define ELEMENT_ONE_PIN 9 61 | #define ELEMENT_TWO_PIN 8 62 | 63 | // This is the pin with the button. 64 | #define BUTTON_SELECT 10 65 | 66 | // This value is the full-scale temperature. It's the reference voltage 67 | // divided by the 5 mV/degC scaling of the AD849x. 68 | // 69 | #ifdef EXTERNAL_REF 70 | // 1.8 volts is 360 degrees C 71 | #define FULL_SCALE_TEMP 360.0 72 | #else 73 | // 5 volts is 1000 degrees C 74 | #define FULL_SCALE_TEMP 1000.0 75 | #endif 76 | 77 | // How often do we update the displayed temp? 78 | #define DISPLAY_UPDATE_INTERVAL 500 79 | 80 | // A reasonable approximation of room temperature. 81 | #define AMBIENT_TEMP 25.0 82 | 83 | // fiddle these knobs 84 | #define K_P 500 85 | #define K_I 0.1 86 | #define K_D 5 87 | 88 | // The number of milliseconds for each cycle of the control output. 89 | // The duty cycle is adjusted by the PID. 90 | #define PWM_PULSE_WIDTH 1000 91 | 92 | // To get a temperature reading, read it a bunch of times and take the average. 93 | // This will cut down on the noise. 94 | #define SAMPLE_COUNT 3 95 | 96 | // If we see any state change on the button, we ignore all changes for this long 97 | #define BUTTON_DEBOUNCE_INTERVAL 50 98 | // How long does the button have to stay down before we call it a LONG push? 99 | #define BUTTON_LONG_START 250 100 | 101 | // This is the enumeration of the output values for checkEvent() 102 | #define EVENT_NONE 0 103 | #define EVENT_SHORT_PUSH 1 104 | #define EVENT_LONG_PUSH 2 105 | 106 | // This is the magic value for the degree mark for the display. If your display 107 | // happens to have some sort of whacky Kanji character instead, then you'll need 108 | // to look up in your display's datasheet. You might try 0xD4 as a second choice. 109 | #define DEGREE_CHAR (0xDF) 110 | 111 | #define VERSION "(84) 0.5.1" 112 | 113 | char p_buffer[17]; // enough for one line on the LCD. 114 | #define _P(str) (strcpy_P(p_buffer, PSTR(str)), p_buffer) 115 | 116 | // missing from the Arduino IDE 117 | #ifndef pgm_read_ptr 118 | #define pgm_read_ptr(p) ((PGM_VOID_P)pgm_read_word(p)) 119 | #endif 120 | 121 | struct curve_point { 122 | // Display this string on the display during this phase. Maximum 8 characters long. 123 | PGM_P phase_name; 124 | // The duration of this phase, in milliseconds 125 | unsigned long duration_millis; 126 | // The setpoint will drift smoothly across the phase from the last 127 | // set point to this temperature, arriving there at the very end. 128 | double target_temp; 129 | }; 130 | 131 | // This table is the complete operational profile of the oven. 132 | // This example is intended for tin-lead based paste. For RoHS 133 | // solder, you'll need to adjust it. 134 | 135 | // In order to put the operating profile into PROGMEM, we have to "unroll" the entire thing 136 | // so that we can insure that each separate piece makes it into PROGMEM. First, all of the 137 | // curve point name strings. 138 | 139 | const char PH_txt[] PROGMEM = "Preheat"; 140 | const char SK_txt[] PROGMEM = "Soak"; 141 | const char N_txt[] PROGMEM = ""; 142 | const char RF_txt[] PROGMEM = "Reflow"; 143 | const char CL_txt[] PROGMEM = "Cool"; 144 | 145 | // Next, each curve point, which represents a section of time where the oven will 146 | // transition from the previous point to the next. It's defined as how much time we 147 | // will spend, and what the target will be at the end of that time. 148 | 149 | // Drift from the ambient temperature to 150 deg C over 90 seconds. 150 | const struct curve_point PT_1 PROGMEM = { PH_txt, 90000, 150.0 }; 151 | // Drift more slowly up to 180 deg C over 60 seconds. 152 | const struct curve_point PT_2 PROGMEM = { SK_txt, 60000, 180.0 }; 153 | // This entry will cause the setpoint to "snap" to the next temperature rather 154 | // than drift over the course of an interval. The name won't be displayed because the duration is 0, 155 | // but a NULL name will end the table, so use an empty string instead. 156 | // This will force the oven to move to the reflow temperature as quickly as possible. 157 | const struct curve_point PT_3 PROGMEM = { N_txt, 0, 230.0 }; 158 | // It's going to take around 80 seconds to get to peak. Hang out there a bit. 159 | const struct curve_point PT_4 PROGMEM = { RF_txt, 90000, 230.0 }; 160 | // There is a maximum cooling rate to avoid thermal shock. The oven will likely cool slower than 161 | // this on its own anyway. It might be a good idea to open the door a bit, but if you get over-agressive 162 | // with cooling, then this entry will compensate for that. 163 | const struct curve_point PT_5 PROGMEM = { CL_txt, 90000, 100.0 }; 164 | // This entry ends the table. Don't leave it out! 165 | const struct curve_point PT_END PROGMEM = { NULL, 0, 0.0 }; 166 | 167 | // Now the actual table itself. 168 | PGM_VOID_P const profile[] PROGMEM = { &PT_1, &PT_2, &PT_3, &PT_4, &PT_5, &PT_END }; 169 | 170 | LiquidCrystal display(LCD_RS, LCD_RW, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7); 171 | 172 | unsigned long start_time, pwm_time, lastDisplayUpdate, button_debounce_time, button_press_time; 173 | unsigned int display_mode; 174 | 175 | double setPoint, currentTemp, outputDuty; 176 | 177 | PID pid(¤tTemp, &outputDuty, &setPoint, K_P, K_I, K_D, DIRECT); 178 | 179 | static void Delay(unsigned long ms) { 180 | while(ms > 100) { 181 | delay(100); 182 | wdt_reset(); 183 | ms -= 100; 184 | } 185 | delay(ms); 186 | wdt_reset(); 187 | } 188 | // Look for button events. We support "short" pushes and "long" pushes. 189 | // This method is responsible for debouncing and timing the pushes. 190 | unsigned int checkEvent() { 191 | unsigned long now = millis(); 192 | if (button_debounce_time != 0) { 193 | if (now - button_debounce_time < BUTTON_DEBOUNCE_INTERVAL) { 194 | // debounce is in progress 195 | return EVENT_NONE; 196 | } else { 197 | // debounce is over 198 | button_debounce_time = 0; 199 | } 200 | } 201 | boolean button = digitalRead(BUTTON_SELECT) == LOW; 202 | if (button) { 203 | // Button is down 204 | if (button_press_time == 0) { // this is the start of a press. 205 | button_debounce_time = button_press_time = now; 206 | } 207 | return EVENT_NONE; // We don't know what this button-push is going to be yet 208 | } else { 209 | // Button released 210 | if (button_press_time == 0) return EVENT_NONE; // It wasn't down anyway. 211 | // We are now ending a button-push. First, start debuncing. 212 | button_debounce_time = now; 213 | unsigned long push_duration = now - button_press_time; 214 | button_press_time = 0; 215 | if (push_duration > BUTTON_LONG_START) { 216 | return EVENT_LONG_PUSH; 217 | } else { 218 | return EVENT_SHORT_PUSH; 219 | } 220 | } 221 | } 222 | 223 | // Format and display a temperature value. 224 | void displayTemp(double temp) { 225 | if (temp < 10) display.print(' '); 226 | if (temp < 100) display.print(' '); 227 | display.print((int)temp); 228 | display.print('.'); 229 | display.print(((int)(temp * 10.0)) % 10); 230 | display.print((char)DEGREE_CHAR); 231 | display.print('C'); 232 | } 233 | 234 | // Sample the temperature pin a few times and take an average. 235 | // Figure out the voltage, and then the temperature from that. 236 | void updateTemp() { 237 | unsigned long sum = 0, mv; 238 | analogRead(TEMP_SENSOR_PIN); // throw this one away 239 | for(int i = 0; i < SAMPLE_COUNT; i++) { 240 | delay(10); 241 | mv = analogRead(TEMP_SENSOR_PIN); 242 | sum += mv; 243 | } 244 | mv = sum / SAMPLE_COUNT; 245 | currentTemp = ((double)mv) * (FULL_SCALE_TEMP / 1024.0); 246 | } 247 | 248 | // Call this when the cycle is finished. Also, call it at 249 | // startup to initialize everything. 250 | void finish() { 251 | start_time = 0; 252 | pwm_time = 0; 253 | pid.SetMode(MANUAL); 254 | digitalWrite(ELEMENT_ONE_PIN, LOW); 255 | digitalWrite(ELEMENT_TWO_PIN, LOW); 256 | display.clear(); 257 | display.print(_P("Waiting")); 258 | } 259 | 260 | // Which phase are we in now? (or -1 for finished) 261 | static int getCurrentPhase(unsigned long time) { 262 | unsigned long so_far = 0; 263 | for(int i = 0; true; i++) { 264 | struct curve_point this_point; 265 | memcpy_P(&this_point, pgm_read_ptr(profile + i), sizeof(struct curve_point)); 266 | if (this_point.phase_name == NULL) break; 267 | if (so_far + this_point.duration_millis > time) { // we're in THIS portion of the profile 268 | return i; 269 | } 270 | so_far += this_point.duration_millis; 271 | } 272 | return -1; 273 | } 274 | 275 | // How many milliseconds into a cycle does the given phase number start? 276 | static unsigned long phaseStartTime(int phase) { 277 | unsigned long so_far = 0; 278 | for(int i = 0; i < phase; i++) { 279 | struct curve_point this_point; 280 | memcpy_P(&this_point, pgm_read_ptr(profile + i), sizeof(struct curve_point)); 281 | so_far += this_point.duration_millis; 282 | } 283 | return so_far; 284 | } 285 | 286 | void setup() { 287 | // This must happen as early as possible to prevent the watchdog from biting after a restart. 288 | MCUSR = 0; 289 | wdt_enable(WDTO_500MS); 290 | 291 | display.begin(16, 2); 292 | #ifdef EXTERNAL_REF 293 | analogReference(EXTERNAL); 294 | #else 295 | analogReference(DEFAULT); // Vcc 296 | #endif 297 | pinMode(ELEMENT_ONE_PIN, OUTPUT); 298 | pinMode(ELEMENT_TWO_PIN, OUTPUT); 299 | digitalWrite(ELEMENT_ONE_PIN, LOW); 300 | digitalWrite(ELEMENT_TWO_PIN, LOW); 301 | 302 | pinMode(BUTTON_SELECT, INPUT_PULLUP); 303 | 304 | pid.SetOutputLimits(0.0, PWM_PULSE_WIDTH); 305 | pid.SetMode(MANUAL); 306 | 307 | lastDisplayUpdate = 0; 308 | start_time = 0; 309 | button_debounce_time = 0; 310 | button_press_time = 0; 311 | display_mode = 0; 312 | 313 | display.clear(); 314 | 315 | display.setCursor(0,0); 316 | display.print(_P("Toast-R-Reflow")); 317 | display.setCursor(0, 1); 318 | display.print(_P(VERSION)); 319 | 320 | Delay(2000); 321 | 322 | finish(); 323 | } 324 | 325 | void loop() { 326 | wdt_reset(); 327 | updateTemp(); 328 | boolean doDisplayUpdate = false; 329 | { 330 | unsigned long now = millis(); 331 | if (lastDisplayUpdate == 0 || now - lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) { 332 | doDisplayUpdate = true; 333 | lastDisplayUpdate = now; 334 | display.setCursor(0, 1); 335 | displayTemp(currentTemp); 336 | } 337 | } 338 | if (start_time == 0) { 339 | // We're not running. Wait for the button. 340 | unsigned int event = checkEvent(); 341 | switch(event) { 342 | case EVENT_SHORT_PUSH: 343 | case EVENT_LONG_PUSH: 344 | // We really want to just re-initialize the PID. 345 | // The only way to do that with the existing API 346 | // is to transition from MANUAL to AUTOMATIC. 347 | // For this reason, setup() and finish() will 348 | // set the mode to MANUAL (which is otherwise 349 | // pointless because we don't call Compute() when 350 | // the oven isn't running). 351 | pid.SetMode(AUTOMATIC); 352 | start_time = millis(); 353 | return; 354 | } 355 | 356 | } else { 357 | // We're running. 358 | unsigned long now = millis(); 359 | unsigned long profile_time = now - start_time; 360 | int currentPhase = getCurrentPhase(profile_time); 361 | if (currentPhase < 0) { 362 | // All done! 363 | finish(); 364 | return; 365 | } 366 | struct curve_point this_point; 367 | memcpy_P(&this_point, pgm_read_ptr(profile + currentPhase), sizeof(struct curve_point)); 368 | 369 | unsigned int event = checkEvent(); 370 | switch(event) { 371 | case EVENT_SHORT_PUSH: 372 | display_mode ^= 1; // pick the other mode 373 | break; 374 | case EVENT_LONG_PUSH: 375 | finish(); 376 | break; 377 | } 378 | 379 | if (doDisplayUpdate) { 380 | // more display updates to do. 381 | 382 | // The time 383 | unsigned long profile_time = now - start_time; 384 | unsigned int profile_sec = profile_time / 1000; 385 | unsigned int profile_min = profile_sec / 60; 386 | profile_sec %= 60; 387 | display.setCursor(10, 0); 388 | if (profile_min < 10) display.print('0'); 389 | display.print(profile_min); 390 | display.print(':'); 391 | if (profile_sec < 10) display.print('0'); 392 | display.print(profile_sec); 393 | 394 | // The phase name 395 | display.setCursor(0, 0); 396 | // This is just like the macro, but the string isn't a local constant 397 | strcpy_P(p_buffer, this_point.phase_name); 398 | display.print(p_buffer); 399 | for(unsigned int j = 0; j < 8 - strlen(p_buffer); j++) display.print(' '); 400 | 401 | display.setCursor(8, 1); 402 | switch(display_mode) { 403 | case 0: 404 | // the setpoint 405 | displayTemp(setPoint); 406 | break; 407 | case 1: 408 | // the oven power 409 | int mils = (outputDuty * 1000) / PWM_PULSE_WIDTH; 410 | if (mils < 1000) display.print(' '); 411 | if (mils < 100) display.print(' '); 412 | display.print(mils / 10); 413 | display.print('.'); 414 | display.print(mils % 10); 415 | display.print('%'); 416 | display.print(' '); 417 | display.print(' '); 418 | break; 419 | } 420 | } 421 | // The concept here is that we have two heating elements 422 | // that we can independently control. 423 | // 424 | // We could just turn them on and off at the same time, but 425 | // then why did we go to the trouble of buying two triacs? 426 | // 427 | // Instead, we can try and arrange them to pulse at different times. 428 | // This will help encourage convection (hopefully), as well as 429 | // reducing the instantaneous power demand (at least when the duty cycle 430 | // is less than 50%). 431 | // 432 | // So start one of them (#2) at the beginning of the interval, and end the other (#1) 433 | // at the end of the interval. 434 | if (pwm_time == 0 || now - pwm_time > PWM_PULSE_WIDTH) { 435 | // Time to start a new PWM interval. 436 | pwm_time = now; 437 | // Turn element one off. We may turn it on later. 438 | digitalWrite(ELEMENT_ONE_PIN, LOW); 439 | // Only start element two if we're actually going to do *anything* 440 | // We will turn it off later. 441 | digitalWrite(ELEMENT_TWO_PIN, (outputDuty > 0.0)?HIGH:LOW); 442 | } else { 443 | // We're somewhere in the middle of the current interval. 444 | unsigned long place_in_pulse = now - pwm_time; 445 | if (place_in_pulse >= outputDuty) 446 | digitalWrite(ELEMENT_TWO_PIN, LOW); // their pulse is over - turn the juice off 447 | if (place_in_pulse >= (PWM_PULSE_WIDTH - outputDuty)) 448 | digitalWrite(ELEMENT_ONE_PIN, HIGH); // their pulse is ready to begin - turn the juice on 449 | } 450 | 451 | // Now update the set point. 452 | // What was the last target temp? That's where we're coming *from* in this phase 453 | double last_temp; 454 | { 455 | struct curve_point last_point; 456 | if (currentPhase != 0) 457 | memcpy_P(&last_point, pgm_read_ptr(profile + currentPhase - 1), sizeof(struct curve_point)); 458 | last_temp = (currentPhase == 0)?AMBIENT_TEMP:last_point.target_temp; 459 | } 460 | // Where are we in this phase? 461 | unsigned long position_in_phase = profile_time - phaseStartTime(currentPhase); 462 | // What fraction of the current phase is that? 463 | double fraction_of_phase = ((double)position_in_phase) / ((double) this_point.duration_millis); 464 | // How much is the temperature going to change during this phase? 465 | double temp_delta = this_point.target_temp - last_temp; 466 | // The set point is the fraction of the delta that's the same as the fraction of the complete phase. 467 | setPoint = temp_delta * fraction_of_phase + last_temp; 468 | 469 | pid.Compute(); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Toast-R-Reflow (II).ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Toast-R-Reflow II 4 | Copyright 2013 Nicholas W. Sayer 5 | 6 | This program is free software; you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation; either version 2 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License along 17 | with this program; if not, write to the Free Software Foundation, Inc., 18 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | */ 21 | 22 | /* 23 | 24 | This version of the code is designed for the ATMega328 / MAX31855 variant of the controller 25 | board, which is model II. 26 | 27 | */ 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | // The pins connected to the MAX31855 thermocouple chip. 35 | #define TEMP_CS 7 36 | #define TEMP_DO 12 37 | #define TEMP_CLK 13 38 | 39 | // How many microseconds is the clock width of each half-cycle? 40 | // It's actually 100 nanoseconds, but 1 microsecond will do. 41 | #define MAX31855_BIT_DELAY 1 42 | 43 | // The pins connected up to the LCD. 44 | #define LCD_D4 10 45 | #define LCD_D5 11 46 | #define LCD_D6 12 47 | #define LCD_D7 13 48 | #define LCD_RS 9 49 | #define LCD_E 8 50 | 51 | // These pins turn on the two heating elements. 52 | #define ELEMENT_ONE_PIN 2 53 | #define ELEMENT_TWO_PIN 3 54 | 55 | // This is the pin with the button. 56 | #define BUTTON_SELECT 4 57 | #define BUTTON_UP 5 58 | #define BUTTON_DOWN 6 59 | 60 | // How often do we update the displayed temp? 61 | #define DISPLAY_UPDATE_INTERVAL 500 62 | 63 | // fiddle these knobs 64 | #define K_P 150 65 | #define K_I 0.1 66 | #define K_D 10 67 | 68 | // The number of milliseconds for each cycle of the control output. 69 | // The duty cycle is adjusted by the PID. 70 | #define PWM_PULSE_WIDTH 1000 71 | 72 | // If we see any state change on the button, we ignore all changes for this long 73 | #define BUTTON_DEBOUNCE_INTERVAL 50 74 | 75 | // How long does the button have to stay down before we call it a LONG push? 76 | #define BUTTON_LONG_START 250 77 | 78 | // This is the enumeration of the output values for checkEvent() 79 | #define EVENT_NONE 0 80 | #define EVENT_SHORT_PUSH 1 81 | #define EVENT_LONG_PUSH 2 82 | 83 | // This is the magic value for the degree mark for the display. If your display 84 | // happens to have some sort of whacky Kanji character instead, then you'll need 85 | // to look up in your display's datasheet. You might try 0xD4 as a second choice. 86 | #define DEGREE_CHAR (0xDF) 87 | 88 | // baud rate for the serial port 89 | #define SERIAL_BAUD 9600 90 | 91 | // milliseconds between serial log intervals 92 | #define SERIAL_LOG_INTERVAL 500 93 | 94 | #define SIZE_OF_PROG_POINTER (sizeof(void*)) 95 | 96 | // Thanks to Gareth Evans at http://todbot.com/blog/2008/06/19/how-to-do-big-strings-in-arduino/ 97 | // Note that you must be careful not to use this macro more than once per "statement", lest you 98 | // risk overwriting the buffer before it is used. So no using it inside methods that return 99 | // strings that are then used in snprintf statements that themselves use this macro. 100 | char p_buffer[17]; 101 | #define P(str) (strncpy_P(p_buffer, PSTR(str), sizeof(p_buffer)), p_buffer) 102 | 103 | #define VERSION "(II) 1.2" 104 | 105 | struct curve_point { 106 | // Display this string on the display during this phase. Maximum 8 characters long. 107 | PGM_P phase_name; 108 | // The duration of this phase, in milliseconds 109 | unsigned long duration_millis; 110 | // The setpoint will drift smoothly across the phase from the last 111 | // set point to this temperature, arriving there at the very end. 112 | double target_temp; 113 | }; 114 | // This table is the complete operational profile of the oven. 115 | // This example is intended for tin-lead based paste. For RoHS 116 | // solder, you'll need to adjust it. 117 | 118 | // In order to put the operating profile into PROGMEM, we have to "unroll" the entire thing 119 | // so that we can insure that each separate piece makes it into PROGMEM. First, all of the 120 | // curve point name strings. 121 | 122 | const char PH_txt[] PROGMEM = "Preheat"; 123 | const char SK_txt[] PROGMEM = "Soak"; 124 | const char N_txt[] PROGMEM = ""; 125 | const char RF_txt[] PROGMEM = "Reflow"; 126 | const char CL_txt[] PROGMEM = "Cool"; 127 | 128 | const char name_a_txt[] PROGMEM = "SnPb"; 129 | // Next, each curve point, which represents a section of time where the oven will 130 | // transition from the previous point to the next. It's defined as how much time we 131 | // will spend, and what the target will be at the end of that time. 132 | 133 | // This special entry ends a profile. Always add it to the end! 134 | const struct curve_point PT_END PROGMEM = { NULL, 0, 0.0 }; 135 | 136 | // Drift from the ambient temperature to 150 deg C over 90 seconds. 137 | const struct curve_point PT_A_1 PROGMEM = { PH_txt, 90000, 150.0 }; 138 | // Drift more slowly up to 180 deg C over 60 seconds. 139 | const struct curve_point PT_A_2 PROGMEM = { SK_txt, 60000, 180.0 }; 140 | // This entry will cause the setpoint to "snap" to the next temperature rather 141 | // than drift over the course of an interval. The name won't be displayed because the duration is 0, 142 | // but a NULL name will end the table, so use an empty string instead. 143 | // This will force the oven to move to the reflow temperature as quickly as possible. 144 | const struct curve_point PT_A_3 PROGMEM = { N_txt, 0, 230.0 }; 145 | // It's going to take around 80 seconds to get to peak. Hang out there a bit. 146 | const struct curve_point PT_A_4 PROGMEM = { RF_txt, 90000, 230.0 }; 147 | // There is a maximum cooling rate to avoid thermal shock. The oven will likely cool slower than 148 | // this on its own anyway. It might be a good idea to open the door a bit, but if you get over-agressive 149 | // with cooling, then this entry will compensate for that. 150 | const struct curve_point PT_A_5 PROGMEM = { CL_txt, 90000, 100.0 }; 151 | 152 | // Now the actual table itself. 153 | PGM_VOID_P const profile_a[] PROGMEM = { &PT_A_1, &PT_A_2, &PT_A_3, &PT_A_4, &PT_A_5, &PT_END }; 154 | 155 | const char name_b_txt[] PROGMEM = "RoHS"; 156 | 157 | // Drift from the ambient temperature to 150 deg C over 90 seconds. 158 | const struct curve_point PT_B_1 PROGMEM = { PH_txt, 90000, 150.0 }; 159 | // Drift more slowly up to 180 deg C over 60 seconds. 160 | const struct curve_point PT_B_2 PROGMEM = { SK_txt, 60000, 180.0 }; 161 | // This entry will cause the setpoint to "snap" to the next temperature rather 162 | // than drift over the course of an interval. The name won't be displayed because the duration is 0, 163 | // but a NULL name will end the table, so use an empty string instead. 164 | // This will force the oven to move to the reflow temperature as quickly as possible. 165 | const struct curve_point PT_B_3 PROGMEM = { N_txt, 0, 250.0 }; 166 | // It's going to take around 80 seconds to get to peak. Hang out there a bit. 167 | const struct curve_point PT_B_4 PROGMEM = { RF_txt, 90000, 250.0 }; 168 | // There is a maximum cooling rate to avoid thermal shock. The oven will likely cool slower than 169 | // this on its own anyway. It might be a good idea to open the door a bit, but if you get over-agressive 170 | // with cooling, then this entry will compensate for that. 171 | const struct curve_point PT_B_5 PROGMEM = { CL_txt, 90000, 100.0 }; 172 | 173 | PGM_VOID_P const profile_b[] PROGMEM = { &PT_B_1, &PT_B_2, &PT_B_3, &PT_B_4, &PT_B_5, &PT_END }; 174 | 175 | const char name_c_txt[] PROGMEM = "Bake"; 176 | 177 | // Drift from ambient to 125 deg C over an hour 178 | const struct curve_point PT_C_1 PROGMEM = { PH_txt, 3600000, 125.0 }; 179 | // Stay there for 10 more hours 180 | const struct curve_point PT_C_2 PROGMEM = { name_c_txt, 36000000, 125.0 }; 181 | 182 | PGM_VOID_P const profile_c[] PROGMEM = { &PT_C_1, &PT_C_2, &PT_END }; 183 | 184 | #define PROFILE_COUNT 3 185 | PROGMEM PGM_VOID_P const profiles[] = { profile_a, profile_b, profile_c }; 186 | PROGMEM PGM_P const profile_names[] = { name_a_txt, name_b_txt, name_c_txt }; 187 | 188 | // missing from the Arduino IDE 189 | #ifndef pgm_read_ptr 190 | #define pgm_read_ptr(p) ((void*)pgm_read_word(p)) 191 | #endif 192 | 193 | LiquidCrystal display(LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7); 194 | 195 | unsigned long start_time, pwm_time, lastDisplayUpdate, button_debounce_time, button_press_time, lastSerialLog; 196 | unsigned char active_profile; 197 | unsigned int display_mode; 198 | boolean faulted; // This is whether or not we've *noticed* the fault. 199 | 200 | double setPoint, currentTemp, outputDuty, referenceTemp; 201 | boolean fault; // This is set by updateTemp() 202 | unsigned char fault_bits; // This too. 203 | 204 | PID pid(¤tTemp, &outputDuty, &setPoint, K_P, K_I, K_D, DIRECT); 205 | 206 | // Delay, but pet the watchdog while doing it. 207 | static void Delay(unsigned long ms) { 208 | while(ms > 100) { 209 | delay(100); 210 | wdt_reset(); 211 | ms -= 100; 212 | } 213 | delay(ms); 214 | wdt_reset(); 215 | } 216 | 217 | // Look for button events. We support "short" pushes and "long" pushes. 218 | // This method is responsible for debouncing and timing the pushes. 219 | static unsigned int checkEvent() { 220 | unsigned long now = millis(); 221 | if (button_debounce_time != 0) { 222 | if (now - button_debounce_time < BUTTON_DEBOUNCE_INTERVAL) { 223 | // debounce is in progress 224 | return EVENT_NONE; 225 | } else { 226 | // debounce is over 227 | button_debounce_time = 0; 228 | } 229 | } 230 | boolean button = digitalRead(BUTTON_SELECT) == LOW; 231 | if (button) { 232 | // Button is down 233 | if (button_press_time == 0) { // this is the start of a press. 234 | button_debounce_time = button_press_time = now; 235 | } 236 | return EVENT_NONE; // We don't know what this button-push is going to be yet 237 | } else { 238 | // Button released 239 | if (button_press_time == 0) return EVENT_NONE; // It wasn't down anyway. 240 | // We are now ending a button-push. First, start debuncing. 241 | button_debounce_time = now; 242 | unsigned long push_duration = now - button_press_time; 243 | button_press_time = 0; 244 | if (push_duration > BUTTON_LONG_START) { 245 | return EVENT_LONG_PUSH; 246 | } else { 247 | return EVENT_SHORT_PUSH; 248 | } 249 | } 250 | } 251 | 252 | static inline void formatTemp(double temp) { 253 | int deg = (int)(temp * 10); 254 | sprintf(p_buffer, "%3d.%1d%cC ", deg / 10, deg % 10, DEGREE_CHAR); 255 | } 256 | 257 | // Format and display a temperature value. 258 | static inline void displayTemp(double temp) { 259 | formatTemp(temp); 260 | display.print(p_buffer); 261 | } 262 | 263 | static inline void updateTemp() { 264 | // The DO and CLK pins are shared with the LCD, so we have to... rewire them. 265 | pinMode(TEMP_DO, INPUT); 266 | pinMode(TEMP_CLK, OUTPUT); 267 | digitalWrite(TEMP_CLK, LOW); 268 | delayMicroseconds(MAX31855_BIT_DELAY); 269 | digitalWrite(TEMP_CS, LOW); 270 | delayMicroseconds(MAX31855_BIT_DELAY); 271 | 272 | uint32_t temp_bits = 0; 273 | for(int i = 0; i < 32; i++) { 274 | digitalWrite(TEMP_CLK, HIGH); 275 | delayMicroseconds(MAX31855_BIT_DELAY); 276 | temp_bits <<= 1; 277 | temp_bits |= (digitalRead(TEMP_DO) == HIGH)?1:0; 278 | digitalWrite(TEMP_CLK, LOW); 279 | delayMicroseconds(MAX31855_BIT_DELAY); 280 | } 281 | digitalWrite(TEMP_CS, HIGH); 282 | pinMode(TEMP_DO, OUTPUT); // give the pin back to LiquidCrystal. 283 | 284 | // The format of the read data: 285 | // F E D C B A 9 8 7 6 5 4 3 2 1 0 F E D C B A 9 8 7 6 5 4 3 2 1 0 286 | // x x x x x x x x x x x x x x 0 f y y y y y y y y y y y y 0 a b c 287 | // x = thermocouple temperature 288 | // y = reference temperature 289 | // f = 1 if any of a, b or c are 1 (fault) 290 | // a = 1 if the thermocouple is shorted to Vcc 291 | // b = 1 if the thermocouple is shorted to ground 292 | // c = 1 if the thermocouple is open 293 | // Both temps are 2s compliment signed numbers. 294 | // x is in 1/4 degree units. y is in 1/16 degree units. 295 | 296 | fault = (temp_bits & 0x10000) != 0; 297 | fault_bits = temp_bits & 0x7; 298 | 299 | int16_t x = (int16_t)(temp_bits >> 16); // Take the top word and make it signed. 300 | x >>= 2; // This shift will be sign-extended because we copied it to an int type 301 | currentTemp = x / 4.0; // Now divide and float 302 | int16_t y = (int16_t)(temp_bits); 303 | y >>= 4; 304 | referenceTemp = y / 16.0; 305 | 306 | } 307 | 308 | 309 | // Call this when the cycle is finished. Also, call it at 310 | // startup to initialize everything. 311 | void finish(boolean silent = false) { 312 | start_time = 0; 313 | pwm_time = 0; 314 | pid.SetMode(MANUAL); 315 | digitalWrite(ELEMENT_ONE_PIN, LOW); 316 | digitalWrite(ELEMENT_TWO_PIN, LOW); 317 | if (silent) return; 318 | display.clear(); 319 | display.print(P("Waiting")); 320 | display.setCursor(10, 0); 321 | strncpy_P(p_buffer, (char*)pgm_read_ptr(((uint16_t)profile_names) + SIZE_OF_PROG_POINTER * active_profile), sizeof(p_buffer)); 322 | display.print(p_buffer); 323 | } 324 | 325 | static inline void* currentProfile() { 326 | return pgm_read_ptr(((uint16_t)profiles) + SIZE_OF_PROG_POINTER * active_profile); 327 | } 328 | 329 | // Which phase are we in now? (or -1 for finished) 330 | static inline int getCurrentPhase(unsigned long time) { 331 | unsigned long so_far = 0; 332 | for(int i = 0; true; i++) { 333 | struct curve_point this_point; 334 | memcpy_P(&this_point, pgm_read_ptr(((uint16_t)currentProfile()) + i * SIZE_OF_PROG_POINTER), sizeof(struct curve_point)); 335 | if (this_point.phase_name == NULL) break; 336 | if (so_far + this_point.duration_millis > time) { // we're in THIS portion of the profile 337 | return i; 338 | } 339 | so_far += this_point.duration_millis; 340 | } 341 | return -1; 342 | } 343 | 344 | // How many milliseconds into a cycle does the given phase number start? 345 | static inline unsigned long phaseStartTime(int phase) { 346 | unsigned long so_far = 0; 347 | for(int i = 0; i < phase; i++) { 348 | struct curve_point this_point; 349 | memcpy_P(&this_point, pgm_read_ptr(((uint16_t)currentProfile()) + i * SIZE_OF_PROG_POINTER), sizeof(struct curve_point)); 350 | so_far += this_point.duration_millis; 351 | } 352 | return so_far; 353 | } 354 | 355 | void setup() { 356 | // This must be done as early as possible to prevent the watchdog from biting during reset. 357 | MCUSR = 0; 358 | wdt_enable(WDTO_500MS); 359 | 360 | Serial.begin(SERIAL_BAUD); 361 | display.begin(16, 2); 362 | pinMode(ELEMENT_ONE_PIN, OUTPUT); 363 | pinMode(ELEMENT_TWO_PIN, OUTPUT); 364 | pinMode(TEMP_CS, OUTPUT); 365 | digitalWrite(ELEMENT_ONE_PIN, LOW); 366 | digitalWrite(ELEMENT_TWO_PIN, LOW); 367 | digitalWrite(TEMP_CS, HIGH); 368 | 369 | pinMode(BUTTON_SELECT, INPUT_PULLUP); 370 | 371 | pid.SetOutputLimits(0.0, PWM_PULSE_WIDTH); 372 | pid.SetMode(MANUAL); 373 | 374 | lastDisplayUpdate = 0; 375 | lastSerialLog = 0; 376 | start_time = 0; 377 | button_debounce_time = 0; 378 | button_press_time = 0; 379 | display_mode = 0; 380 | active_profile = 0; 381 | faulted = false; 382 | 383 | display.clear(); 384 | 385 | display.setCursor(0,0); 386 | display.print(P("Toast-R-Reflow")); 387 | display.setCursor(0, 1); 388 | display.print(P(VERSION)); 389 | 390 | Delay(2000); 391 | finish(); 392 | } 393 | 394 | void loop() { 395 | wdt_reset(); 396 | updateTemp(); 397 | if (fault) { 398 | if (!faulted) { 399 | faulted = true; 400 | finish(true); 401 | // complain bitterly 402 | display.clear(); 403 | display.setCursor(2, 0); 404 | display.print(P("THERM. FAULT")); 405 | display.setCursor(0, 1); 406 | display.print(P("Fault bits: ")); 407 | display.print(fault_bits); 408 | } 409 | return; 410 | } else { 411 | if (faulted) { 412 | // If the fault just went away, clean up the display 413 | finish(); 414 | } 415 | faulted = false; 416 | // and carry on... 417 | } 418 | boolean doDisplayUpdate = false; 419 | { 420 | unsigned long now = millis(); 421 | if (lastDisplayUpdate == 0 || now - lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) { 422 | doDisplayUpdate = true; 423 | lastDisplayUpdate = now; 424 | display.setCursor(0, 1); 425 | displayTemp(currentTemp); 426 | } 427 | } 428 | if (start_time == 0) { 429 | // We're not running. Wait for the button. 430 | unsigned int event = checkEvent(); 431 | switch(event) { 432 | case EVENT_LONG_PUSH: 433 | if (++active_profile >= PROFILE_COUNT) active_profile = 0; 434 | display.setCursor(10, 0); 435 | strncpy_P(p_buffer, (char*)pgm_read_ptr(((uint16_t)profile_names) + SIZE_OF_PROG_POINTER * active_profile), sizeof(p_buffer)); 436 | display.print(p_buffer); 437 | break; 438 | case EVENT_SHORT_PUSH: 439 | // We really want to just re-initialize the PID. 440 | // The only way to do that with the existing API 441 | // is to transition from MANUAL to AUTOMATIC. 442 | // For this reason, setup() and finish() will 443 | // set the mode to MANUAL (which is otherwise 444 | // pointless because we don't call Compute() when 445 | // the oven isn't running). 446 | pid.SetMode(AUTOMATIC); 447 | start_time = millis(); 448 | return; 449 | } 450 | 451 | } else { 452 | // We're running. 453 | unsigned long now = millis(); 454 | unsigned long profile_time = now - start_time; 455 | int currentPhase = getCurrentPhase(profile_time); 456 | if (currentPhase < 0) { 457 | // All done! 458 | finish(); 459 | return; 460 | } 461 | 462 | void* profile = currentProfile(); 463 | struct curve_point this_point; 464 | memcpy_P(&this_point, pgm_read_ptr(((uint16_t)profile) + currentPhase * SIZE_OF_PROG_POINTER), sizeof(struct curve_point)); 465 | 466 | unsigned int event = checkEvent(); 467 | switch(event) { 468 | case EVENT_LONG_PUSH: 469 | finish(); 470 | return; 471 | case EVENT_SHORT_PUSH: 472 | display_mode ^= 1; // pick the other mode 473 | break; 474 | } 475 | if (lastSerialLog == 0 || now - lastSerialLog > SERIAL_LOG_INTERVAL) { 476 | lastSerialLog = now; 477 | if (start_time == 0) 478 | Serial.print("Wait "); 479 | else { 480 | int sec = (int)((now - start_time) / 1000); 481 | sprintf(p_buffer, "%02d:%02d:%02d ", sec / 3600, (sec/60) % 60, sec % 60); 482 | Serial.print(p_buffer); 483 | } 484 | formatTemp(currentTemp); 485 | Serial.print(p_buffer); 486 | if (start_time == 0) 487 | Serial.print("\r\n"); 488 | else { 489 | formatTemp(setPoint); 490 | Serial.print(p_buffer); 491 | Serial.print("\r\n"); 492 | } 493 | } 494 | if (doDisplayUpdate) { 495 | // more display updates to do. 496 | 497 | // The time 498 | display.setCursor(8, 0); 499 | unsigned long profile_time = now - start_time; 500 | unsigned int sec = profile_time / 1000; 501 | sprintf(p_buffer, "%02d:%02d:%02d ", sec / 3600, (sec/60) % 60, sec % 60); 502 | display.print(p_buffer); 503 | 504 | // The phase name 505 | display.setCursor(0, 0); 506 | strncpy_P(p_buffer, this_point.phase_name, sizeof(p_buffer)); 507 | display.print(p_buffer); 508 | for(unsigned int j = 0; j < 8 - strlen(p_buffer); j++) display.print(' '); 509 | 510 | display.setCursor(8, 1); 511 | switch(display_mode) { 512 | case 0: 513 | // the setpoint 514 | displayTemp(setPoint); 515 | break; 516 | case 1: 517 | // the oven power 518 | int mils = (outputDuty * 1000) / PWM_PULSE_WIDTH; 519 | sprintf(p_buffer, "%3d.%1d%% ", mils / 10, mils % 10); 520 | display.print(p_buffer); 521 | break; 522 | } 523 | } 524 | // The concept here is that we have two heating elements 525 | // that we can independently control. 526 | // 527 | // We could just turn them on and off at the same time, but 528 | // then why did we go to the trouble of buying two triacs? 529 | // 530 | // Instead, we can try and arrange them to pulse at different times. 531 | // This will help encourage convection (hopefully), as well as 532 | // reducing the instantaneous power demand (at least when the duty cycle 533 | // is less than 50%). 534 | // 535 | // So start one of them (#2) at the beginning of the interval, and end the other (#1) 536 | // at the end of the interval. 537 | if (pwm_time == 0 || now - pwm_time > PWM_PULSE_WIDTH) { 538 | // Time to start a new PWM interval. 539 | pwm_time = now; 540 | // Turn element one off. We may turn it on later. 541 | digitalWrite(ELEMENT_ONE_PIN, LOW); 542 | // Only start element two if we're actually going to do *anything* 543 | // We will turn it off later. 544 | digitalWrite(ELEMENT_TWO_PIN, (outputDuty > 0.0)?HIGH:LOW); 545 | } else { 546 | // We're somewhere in the middle of the current interval. 547 | unsigned long place_in_pulse = now - pwm_time; 548 | if (place_in_pulse >= outputDuty) 549 | digitalWrite(ELEMENT_TWO_PIN, LOW); // their pulse is over - turn the juice off 550 | if (place_in_pulse >= (PWM_PULSE_WIDTH - outputDuty)) 551 | digitalWrite(ELEMENT_ONE_PIN, HIGH); // their pulse is ready to begin - turn the juice on 552 | } 553 | 554 | // Now update the set point. 555 | // What was the last target temp? That's where we're coming *from* in this phase 556 | double last_temp; 557 | { 558 | struct curve_point last_point; 559 | if (currentPhase != 0) { 560 | memcpy_P(&last_point, pgm_read_ptr(((uint16_t)profile) + (currentPhase - 1)*SIZE_OF_PROG_POINTER), sizeof(struct curve_point)); 561 | last_temp = last_point.target_temp; 562 | } else { 563 | last_temp = referenceTemp; // Assume this is the ambient temp. 564 | } 565 | } 566 | // Where are we in this phase? 567 | unsigned long position_in_phase = profile_time - phaseStartTime(currentPhase); 568 | // What fraction of the current phase is that? 569 | double fraction_of_phase = ((double)position_in_phase) / ((double) this_point.duration_millis); 570 | // How much is the temperature going to change during this phase? 571 | double temp_delta = this_point.target_temp - last_temp; 572 | // The set point is the fraction of the delta that's the same as the fraction of the complete phase. 573 | setPoint = temp_delta * fraction_of_phase + last_temp; 574 | 575 | pid.Compute(); 576 | } 577 | } 578 | --------------------------------------------------------------------------------