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
Diff: main.cpp
- Revision:
- 4:3d661b485d59
- Parent:
- 3:eb60e36b03f6
- Child:
- 5:0393adfdd439
--- a/main.cpp Thu Aug 29 14:55:52 2013 +0000 +++ b/main.cpp Wed Oct 02 13:38:23 2013 +0000 @@ -2,68 +2,88 @@ // Program to control espresso maker boiler temperatures // Similar to multiple PID control (pre-brew, brew and steam), // but uses a flexible open and closed loop table during brew -// Used with a Gaggia Classic, FreeScale FRDM-KL25Z computer, PT1000 RTD, heater +// Used with a Gaggia Classic, FreeScale FRDM-KL25Z computer, PT1000 RTD, SSR +// Can also control the pump // See www.coffeegeeks.com for discussion // Jon Zeeff, 2013 // Public Domain -// PT1000 RTD ohms (use Google to find a full table) -// 1360 ohms = 94C +// PT1000 RTD ohms (use Google to find a full table, remember to add offset) +// 1362 ohms = 94C +// 1374 ohms = 97C // 1000 ohms = too cold (0C) // 1520 ohms = too hot (136C) -// note: assume a 2.2K divider resistor, a PT1000 RTD and a 16 bit A/D result +// note: assume a precise 2.2K divider resistor, a PT1000 RTD and a 16 bit A/D result // use this formula: A/D = (RTD_OHMS/(RTD_OHMS+2200)) * 65536 // desired A/D value for boiler temp while idling -// note: there is usually some offset between boiler wall temp sensors and actual water temp -#define TARGET_TEMP 25440 // CHANGE THIS +// note: there is usually some offset between boiler wall temp sensors and actual water temp (10-15C?) +#define TARGET_OHMS 1400 // Desired PT1000 RTD Ohms - CHANGE THIS -// Table of adjustments (degrees C) to TARGET_TEMP vs time (seconds) into brew cycle (including preheat period) +// Table of adjustments (degrees C) to TARGET_TEMP and heat vs time (seconds) into brew cycle (including preheat period) // The idea is that extra heat is needed as cool water comes into the boiler during brew. // Extra heat is provided by a higher than normal boiler wall temp. // NOTE: the fractional portion of the value is used as the PWM value to be applied if more heat is needed. // This can prevent overshoot. // Example: 5.3 means that the boiler wall should be 5 degrees C above normal at this time point. If not, apply 30% power. // Example: 99.99 means (roughly) that the heater should be completely on for the 1 second period -// Note: heat takes at least 4 seconds before it is seen by the sensor +// Note: heat on a Gaggia Classic takes about 4 seconds before it is seen by the sensor + +#define BREW_TIME 44 // max brew time +#define BREW_PREHEAT 6 // max preheat time -const double table[40] = { - // preheat up to 10 seconds - 0.0,0.0,0.0,0.0,0.0,0.0,0.0,99.9,99.9,99.20, // CHANGE THIS +const double table[BREW_TIME+BREW_PREHEAT] = { + // preheat up to 6 seconds + 0,0,0,0,99.99,99.99, // CHANGE THIS // brewing (pump is on) up to 30 seconds - 99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20, // CHANGE THIS - 99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,0,0,0,0 // CHANGE THIS + 0,0,0,0,0,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.40,99.35,99.35, // CHANGE THIS + 99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35, // CHANGE THIS + 99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35 }; -const double pump_table[40] = { +// pump power over time for flush or preinfusion or pressure profiling +const double pump_table[BREW_TIME+BREW_PREHEAT] = { // during pre-brew period - 0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, // CHANGE THIS + 0,0,0,0,.80,.80, // CHANGE THIS // brewing up to 30 seconds - 0.0,0.0,99.99,99.99,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20, // CHANGE THIS - 99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,99.20,0,0,0,0 // CHANGE THIS + .85,.90,1.0,0,0,0,0,0,.80,1.0,1.0,1.0,1.0,1.0,1.0,1.0, // CHANGE THIS + 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0, // CHANGE THIS + .85,.85,.85,.85,.85,.85,.85,.85,.85,.85 +}; + +// desired total weight of espresso over brew period in grams +const int scale_table[BREW_TIME+BREW_PREHEAT] = { + 2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50, + 60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60,60 }; // these probably don't need to be changed if you are using a Gaggia Classic +#define AD_PER_DEGREE 43 // how many A/D counts equal a 1 degree C change +#define AD_PER_GRAM 76.75 // how many A/D count equal 1 gram of weight #define CLOSE 60 // how close in A/D value before switching to learned value control -#define GAIN .01 // how fast to adjust +#define GAIN .01 // how fast to adjust (eg 1% percent per 2.7s control period) #define INITIAL_POWER .03 // initial guess for steady state heater power needed (try .03 = 3%) #define MIN_TEMP 21000 // below this is an error -#define MAX_TEMP 29500 // above this is an error +#define MAX_TEMP 29700 // above this is an error #define STEAM_TEMP 28000 // boiler temp while steaming -#define ROOM_TEMP 22000 // A/D value at standard ambient room temp -#define MAX_ROOM_TEMP 22500 // above this means ambient isn't valid -#define SLEEP_TIME 7200 // turn off heat after this many seconds -#define BREW_TIME 30 // max brew time -#define BREW_PREHEAT 10 // max preheat time -#define AD_PER_DEGREE 44 // how many A/D counts equal a 1 degree C change +#define ROOM_TEMP 21707 // A/D value at standard ambient room temp 23C +#define MAX_ROOM_TEMP (ROOM_TEMP + (10 * AD_PER_DEGREE)) // above this means ambient isn't valid +#define SLEEP_PERIOD (3*3600) // turn off heat after this many seconds +#define WAKEUP_TIME 12 // time in 0-23 hours, GMT to wake up. 99 to disable. Example: 12 for noon GMT + +#define TARGET_TEMP ((TARGET_OHMS*65536)/(TARGET_OHMS+2200)) // how hot the boiler should be in A/D #define debug if (1) printf // use if (1) or if (0) #include "mbed.h" #include "TSISensor.h" // touch sensor #include "DS1307.h" // real-time clock #include "FastPWM.h" // better PWM routine for pump control - + +#define BOILER 0 +#define GROUP 1 +#define SCALE 2 + #define OFF 0 #define RED 1 #define GREEN 2 @@ -76,65 +96,74 @@ #define ON 1 #define OFF 0 +// pin assignments DigitalOut heater(PTD7); // Solid State Relay - PTD6&7 have high drive capability FastPWM pump(PTD4); // Solid State Relay - PTD4 can do PWM @ 10K hz -AnalogIn boiler(PTE20); // A/D converter reads temperature on boiler -AnalogIn group(PTE22); // A/D for group basket temp DigitalOut led_green(LED_GREEN); -I2C gI2c(PTE0, PTE1); // SDA, SCL - use pullups +I2C gI2c(PTE0, PTE1); // SDA, SCL - use pullups somewhere RtcDs1307 rtclock(gI2c); // DS1307 is a real time clock chip Serial pc(USBTX, USBRX); // Serial to pc connection - +TSISensor tsi; // used as a brew start button +AnalogIn scale(PTC2); // A/D converter reads scale + void brew(void); void led_color(int color); -unsigned read_temp(AnalogIn adc); +unsigned read_temp(int device); +unsigned read_ad(AnalogIn adc); void steam(int seconds); unsigned ambient_temp; // room or water tank temp (startup) double heat = INITIAL_POWER; // initial fractional heat needed while idle -unsigned boiler_log[40]; // record boiler temp during brew -unsigned group_log[40]; // record basket temp during brew - +unsigned boiler_log[BREW_TIME+BREW_PREHEAT]; // record boiler temp during brew +unsigned group_log[BREW_TIME+BREW_PREHEAT]; // record basket temp during brew +int scale_log[BREW_TIME+BREW_PREHEAT]; // record weight during brew int main() // start of program { time_t prev_time = 0; - TSISensor tsi; // used as a brew start button - ambient_temp = read_temp(boiler); // save temp on startup + + led_color(OFF); + + wait(1); // let settle + ambient_temp = read_temp(BOILER); // save temp on startup - set_time(0); // start clock at zero #if 0 - DateTime dt = gRtc.now(); + DateTime compiled(__DATE__, __TIME__); // to set RT clock initially + rtclock.adjust(compiled); +#endif + DateTime dt = rtclock.now(); // check clock value + debug("RTC = %u/%u/%02u %2u:%02u:%02u\r\n" + ,dt.month(),dt.day(),dt.year() + ,dt.hour(),dt.minute(),dt.second()); + set_time(0); // set active clock - debug("%u/%u/%02u %2u:%02u:%02u\r\n" - ,dt.month(),dt.day(),dt.year() - ,dt.hour(),dt.minute(),dt.second()); -#endif - debug("starting A/D value/temp = %u\r\n",ambient_temp); - - pump.period_ms(500); // period of PWM signal - //pump = 100; // duty cycle. For DC, use 6 msec pulses - + debug("starting A/D value/temp = %u %u\r\n",ambient_temp,read_temp(GROUP)); + + pump = 0; // duty cycle. + pump.period_us(410); // period of PWM signal in us + if (pc.readable()) // clear any data on serial port - pc.getc(); - + pc.getc(); + if (ambient_temp < MAX_ROOM_TEMP) - steam(180); // do accelerated warmup by overheating + steam(5 * 60); // do accelerated warmup by overheating for awhile // loop forever, controlling boiler temperature for (;;) { // read temp from A/D // note: in A/D counts, not degrees - unsigned temp = read_temp(boiler); + unsigned temp = read_temp(BOILER); // bang/bang when far away, PWM to learned value when close if (temp > TARGET_TEMP + CLOSE) { heater = OFF; // turn off heater led_color(GREEN); // set LED to green + wait(.17); } else if (temp < TARGET_TEMP - CLOSE) { heater = ON; // turn on heater led_color(RED); // set LED to red + wait(.17); } else { // close to target temp // learning mode - adjust heat, the fraction of time power should be on @@ -152,44 +181,58 @@ } // if // the user must press a button 10 seconds prior to brewing to start preheat - if (tsi.readPercentage() > .5) + if (tsi.readPercentage() > .5) { brew(); - + set_time(0); // stay awake for awhile more + } + // if they signaled for steam - if (tsi.readPercentage() > .2 && tsi.readPercentage() < .5) - steam(120); - - if (pc.readable()){ // Check if data is available on serial port. - pc.getc(); - // debug, print out temp log + //if (tsi.readPercentage() > .1 && tsi.readPercentage() < .5) + // steam(120); + + if (pc.readable()) { // Check if data is available on serial port. + pc.getc(); + // debug, print out brew temp log int i; - for (i = 0; i < 40; ++i) - printf("log %d: %u %u\r\n",i,boiler_log[i],group_log[i]); + for (i = 0; i < BREW_TIME+BREW_PREHEAT; ++i) + printf("log %d: %u %u %d\r\n",i,boiler_log[i],group_log[i],scale_log[i]); } // if - - // check for idle shutdown, sleep till tomorrow if it occurs - if (time(NULL) > SLEEP_TIME) { // save power - static time_t wakeup_time = (24 * 60 * 60) - (20 * 60); // 24 hours minus 20 min - heater = OFF; // turn off heater + // check for idle shutdown, sleep till tomorrow am if it occurs + if (time(NULL) > SLEEP_PERIOD) { // save power + heater = OFF; // turn off heater led_color(OFF); printf("sleep\r\n"); - while (time(NULL) < wakeup_time) // wait till tomorrow - wait(1); - set_time(0); // clock runs zero to 24 hours - wakeup_time = (24 * 60 * 60); // no 20 min offset needed now - ambient_temp = read_temp(boiler); // save temp on startup - } + + for (;;) { // loop till wakeup in the morning + DateTime dt; + + if (pc.readable()) // user wakeup + break; + + dt = rtclock.now(); // read real time clock + if (dt.hour() == WAKEUP_TIME && dt.minute() == 0) // GMT time to wake up + break; + + wait(30); + } // for + + set_time(0); // reset active timer + debug("exit idle\r\n"); + ambient_temp = read_temp(BOILER); // save temp on startup + } // if // check for errors (incorrect boiler temp can be dangerous) - if (temp > MAX_TEMP || temp < MIN_TEMP) { - heater = OFF; // turn off heater - led_color(YELLOW); // set LED to indicate error + while (temp > MAX_TEMP || temp < MIN_TEMP) { + heater = OFF; // turn off heater + led_color(YELLOW); // set LED to indicate error debug("error A/D = %u\r\n",temp); - for (;;); // reset needed to exit this + wait(60); + temp = read_temp(BOILER); } - if (time(NULL) > prev_time) debug("A/D value = %u %u, heat = %F\r\n",temp,read_temp(group),heat); // once per second + if (time(NULL) > prev_time) + debug("A/D value = %u %u, heat = %F, scale = %u\r\n",temp,read_temp(GROUP),heat,read_ad(scale)); // once per second prev_time = time(NULL); } // for (;;) @@ -199,62 +242,85 @@ //================================================================= // This subroutine is called when the button is pressed, 10 seconds -// before the pump is started. It does both open loop and closed +// before the pump is started. It does both open loop and closed // loop PWM power/heat control. //================================================================= void brew(void) { - unsigned start_time = time(NULL); - double adjust = 1; // default is no adjustment // adjust for higher or lower tank temp (assumed to be equal to ambient at startup) // add in "heat"?? //if (ambient_temp < MAX_ROOM_TEMP) // sanity check // adjust = (double)(ROOM_TEMP - TARGET_TEMP) / (double)(ambient_temp - TARGET_TEMP); - - debug("preheat/brew start, adjust = %F\r\n", adjust); + led_color(WHITE); - - unsigned prev_brew_time = 999; - unsigned brew_time; + + unsigned brew_time; // in seconds double pwm; + unsigned temp; + unsigned scale_zero = read_ad(scale); + int grams; - for (;;) { - brew_time = time(NULL) - start_time; // seconds into cycle - - if (brew_time >= BREW_PREHEAT + BREW_TIME) - break; // brew is done + debug("preheat/brew start, adjust = %F, zero = %u\r\n", adjust,scale_zero); - if (brew_time == BREW_PREHEAT) + for (brew_time = 0; brew_time < BREW_PREHEAT + BREW_TIME; ++brew_time) { // loop until end of brew + + if (brew_time == BREW_PREHEAT) { led_color(BLUE); // set LED color to blue for start brew/pump now + } - //pump = pump_table[brew_time]; // duty cycle + pump = pump_table[brew_time]; // duty cycle or on/off of pump for this period + + pwm = table[brew_time] - (int)table[brew_time]; // decimal part only - pwm = table[brew_time] - (int)table[brew_time]; // decimal part only + temp = read_temp(BOILER); // if too cold, apply the PWM value, if too hot, do nothing - if (read_temp(boiler) < (TARGET_TEMP + (table[brew_time] * AD_PER_DEGREE)) * adjust) { - if (pwm > 0) { - heater = ON; - wait(pwm/2); - heater = OFF; - wait((1 - pwm)/2); - } - } // if PWM + if (temp < (TARGET_TEMP + (table[brew_time] * AD_PER_DEGREE)) * adjust) { + if (pwm > 0.0 && pwm <= 1.0) { + heater = ON; + wait(pwm); + heater = OFF; + pwm = 1 - pwm; + if (pwm > 0.0 && pwm <= 1.0) + wait(pwm); + } else + wait(1.0); + } else + wait(1.0); - if (brew_time != prev_brew_time){ // every second - group_log[brew_time] = read_temp(group); // record group temp - boiler_log[brew_time] = read_temp(boiler); // record boiler temp - debug("target temp %u = %F, temp = %u %u\r\n",brew_time,table[brew_time],read_temp(boiler),read_temp(group)); - prev_brew_time = brew_time; - } + group_log[brew_time] = read_temp(GROUP); // record group temp + boiler_log[brew_time] = temp; // record boiler temp + grams = ((double)read_ad(scale) - scale_zero) / AD_PER_GRAM; + scale_log[brew_time] = grams; + + if (grams < 2) // scale clock only starts when it hits two grams + scale_time = 0; + else + ++scale_time; + + //if (grams > scale_table[scale_time]) + //else + + //debug("target temp %u = %F, temp = %u %u\r\n",brew_time,table[brew_time],read_temp(BOILER),read_temp(GROUP)); + + // early exit if final weight reached + if (grams >= scale_table[BREW_TIME+BREW_PREHEAT-1]) + break; + + // early exit based on user input + if (tsi.readPercentage() > .1 && tsi.readPercentage() < .5) + break; } // for + // shut down + led_color(OFF); debug("brew done\r\n"); - + pump = OFF; + heater = OFF; } // brew() //=========================================================== @@ -268,13 +334,17 @@ debug("steam start, time = %d\r\n", seconds); while (time(NULL) - start_time < seconds) { - if (read_temp(boiler) > STEAM_TEMP) { - heater = OFF; // turn off heater + if (read_temp(BOILER) > STEAM_TEMP) { + heater = OFF; // turn off heater led_color(AQUA); // set LED to aqua } else { - heater = ON; // turn on heater + heater = ON; // turn on heater led_color(PINK); // set LED to pink } + + if (tsi.readPercentage() > .5) // abort steam + break; + } // while heater = OFF; // turn off @@ -323,32 +393,84 @@ } // led_color() +#if 1 +// reduce noise by making unused A/D into digital outs +DigitalOut x1(PTB0); +DigitalOut x2(PTB1); +DigitalOut x3(PTB2); +DigitalOut x4(PTB3); +DigitalOut x5(PTE21); +DigitalOut x6(PTE23); +DigitalOut x7(PTD5); +DigitalOut x8(PTD6); +DigitalOut x9(PTD1); +DigitalOut x10(PTC0); +#endif + //======================================= -// read A/D value from RTD -// median and average for accuracy +// A/D routines //======================================= -unsigned read_temp(AnalogIn adc) +DigitalOut ad_power(PTB9); // used to turn on/off power to resistors + +AnalogIn boiler(PTE20); // A/D converter reads temperature on boiler +AnalogIn group(PTE22); // A/D for group basket temp +AnalogIn vref(PTE29); // A/D for A/D power supply (ad_power) + + +unsigned read_ad(AnalogIn adc) { uint32_t sum=0; int i; + + adc.read_u16(); // throw away one + + #define COUNT 77 // number of samples to average + + for (i = 0; i < COUNT; ++i) { // average multiple for more accuracy + uint16_t a, b, c; - for (i = 0; i < 33; ++i) { // average multiple for more accuracy - unsigned a, b, c; - - a = adc.read_u16(); // take median of 3 values + a = adc.read_u16(); // take median of 3 values to filter noise b = adc.read_u16(); c = adc.read_u16(); if ((a >= b && a <= c) || (a >= c && a <= b)) sum += a; else if ((b >= a && b <= c) || (b >= c && b <= a)) sum += b; - else sum += c; + else sum += c; + } // for - return sum / 33; + return sum / COUNT; + +} // read_temp() + + +// read a temperature in A/D counts +// adjust it for vref variations + +unsigned read_temp(int device) +{ +unsigned value; +unsigned max; // A/D reading for the vref supply voltage -} // read_temp() + // send power to analog resistors only when needed + // this limits self heating + + ad_power = 1; // turn on supply voltage + max = read_ad(vref); // read supply voltage + + if (device == BOILER) + value = (read_ad(boiler) * 65536) / max; // scale to vref + else + value = (read_ad(group) * 65536) / max; // scale to vref + + ad_power = 0; + + return value; +} // read_temp() + +