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:
- 5:0393adfdd439
- Parent:
- 4:3d661b485d59
- Child:
- 6:56b205b46b42
--- a/main.cpp Wed Oct 02 13:38:23 2013 +0000 +++ b/main.cpp Mon Apr 14 20:35:30 2014 +0000 @@ -19,7 +19,10 @@ // desired A/D value for boiler temp while idling // 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 +#define TARGET_OHMS 1400 // Desired PT1000 RTD Ohms / boiler temp - CHANGE THIS + +#define BREW_TIME 44 // max brew time +#define BREW_PREHEAT 6 // max preheat time (when to open brew valve) // 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. @@ -30,46 +33,55 @@ // Example: 99.99 means (roughly) that the heater should be completely on for the 1 second period // 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[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 - 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 +const double table[BREW_TIME+BREW_PREHEAT] = { // CHANGE THIS + 0,0,0,0, // nothing (pumo is off) + 99.99,99.99, // step heat up before flow + 0,0,0,0, // filling portafilter + 0,99.35,99.35, // preinfusion + 99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.40,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,99.35,99.35, 99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35,99.35 }; -// 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,.80,.80, // CHANGE THIS - // brewing up to 30 seconds - .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 +// pump power over time for preinfusion/slow ramp/pressure profiling +// range: 0 to 1 +const double pump_table[BREW_TIME+BREW_PREHEAT] = { // CHANGE THIS + 0,0,0,0, // nothing (pump is off) + .45,.45, // hold low pressure until valve is opened + .45,.55,.65,.75, // ramp pressure up slowly and fill portafilter + 0,0,0, // preinfusion delay + .75,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0, // brew 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 + 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 }; -// 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 +// table for flow profiling +// desired flow rate of espresso in grams/sec for each second of brew +// starts when the total grams in the cup achieves the first entry, not time zero +const double scale_table[BREW_TIME+BREW_PREHEAT] = { + 1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75, + 1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75, + 1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75, + 1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75, + 1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75,1.75 }; +#define END_GRAMS 30 // end when this many grams total (in 27 secs?) +#define FLOW_PERIOD 23.0 // desired shot duration (not used) +#define START_FLOW_PROF 14 // when (seconds) to start flow profiling + // 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 AD_PER_GRAM 76.70 // 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 (eg 1% percent per 2.7s control period) +#define GAIN .01 // how fast to adjust heat(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 29700 // above this is an error #define STEAM_TEMP 28000 // boiler temp while steaming #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 SLEEP_PERIOD (4*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 @@ -103,14 +115,19 @@ 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 +Serial bluetooth(PTC4,PTC3); // Serial via wireless TX,RX 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(int device); +int read_scale(void); +int read_scale2(void); +int read_scale3(void); unsigned read_ad(AnalogIn adc); void steam(int seconds); +inline int median(int a, int b, int c); unsigned ambient_temp; // room or water tank temp (startup) double heat = INITIAL_POWER; // initial fractional heat needed while idle @@ -118,13 +135,16 @@ unsigned group_log[BREW_TIME+BREW_PREHEAT]; // record basket temp during brew int scale_log[BREW_TIME+BREW_PREHEAT]; // record weight during brew +uint16_t slog[5000]; +int scount=0; + int main() // start of program { time_t prev_time = 0; led_color(OFF); - - wait(1); // let settle + + wait(1); // let settle ambient_temp = read_temp(BOILER); // save temp on startup #if 0 @@ -136,17 +156,14 @@ ,dt.month(),dt.day(),dt.year() ,dt.hour(),dt.minute(),dt.second()); set_time(0); // set active clock - + 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(); if (ambient_temp < MAX_ROOM_TEMP) - steam(5 * 60); // do accelerated warmup by overheating for awhile + steam(7 * 60); // do accelerated warmup by overheating for awhile // loop forever, controlling boiler temperature @@ -190,33 +207,43 @@ //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 + char key = 0; + if (pc.readable()) // Check if data is available on serial port. + key = pc.getc(); + + if (key == 'l') { // debug, print out brew temp log int 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]); + for (i = 0; i < scount; ++i) + printf("%u\r\n",slog[i]); } // if + if (key == 'p') { // cycle pump for flush + pump = 1; + wait(5); + pump = 0; + } + // check for idle shutdown, sleep till tomorrow am if it occurs - if (time(NULL) > SLEEP_PERIOD) { // save power + if (time(NULL) > SLEEP_PERIOD || key == 'i') { // save power heater = OFF; // turn off heater led_color(OFF); printf("sleep\r\n"); - + for (;;) { // loop till wakeup in the morning DateTime dt; - - if (pc.readable()) // user wakeup - break; - + + if (pc.readable() && pc.getc() == ' ') // user wakeup + break; + dt = rtclock.now(); // read real time clock if (dt.hour() == WAKEUP_TIME && dt.minute() == 0) // GMT time to wake up - break; - + break; + wait(30); } // for - + set_time(0); // reset active timer debug("exit idle\r\n"); ambient_temp = read_temp(BOILER); // save temp on startup @@ -231,8 +258,8 @@ temp = read_temp(BOILER); } - 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 + 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 (;;) @@ -240,8 +267,14 @@ } // main() +// turn off the heater +void heater_off(void) +{ + heater = OFF; +} + //================================================================= -// This subroutine is called when the button is pressed, 10 seconds +// This subroutine is called when the button is pressed, n seconds // before the pump is started. It does both open loop and closed // loop PWM power/heat control. //================================================================= @@ -251,76 +284,117 @@ 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"?? + // add in "heat" value as a measure of ambient?? //if (ambient_temp < MAX_ROOM_TEMP) // sanity check // adjust = (double)(ROOM_TEMP - TARGET_TEMP) / (double)(ambient_temp - TARGET_TEMP); - + led_color(WHITE); - unsigned brew_time; // in seconds - double pwm; - unsigned temp; - unsigned scale_zero = read_ad(scale); - int grams; - - debug("preheat/brew start, adjust = %F, zero = %u\r\n", adjust,scale_zero); + unsigned brew_time; // seconds since start of brew + unsigned flow_time = 0; // clock that runs once flow starts + double target_pump = 1; // current value of pump power for desired flow + double grams; + double prev_grams = 0; - for (brew_time = 0; brew_time < BREW_PREHEAT + BREW_TIME; ++brew_time) { // loop until end of brew - + wait(.5); // stabilize + int scale_zero = read_scale2(); // weight of empty cup + debug("preheat/brew start, adjust = %f, scale zero = %u counts\r\n", adjust,scale_zero); + + Timeout heater_timer; // used to schedule off time + + Timer timer; // used to keep syncronized, 1 update per second + timer.start(); + + for (brew_time = 0; brew_time < BREW_PREHEAT + 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 or on/off of pump for this period - - pwm = table[brew_time] - (int)table[brew_time]; // decimal part only - - temp = read_temp(BOILER); - + // *** heat control + unsigned temp = read_temp(BOILER); // minimal time needed for this // if too cold, apply the PWM value, if too hot, do nothing - if (temp < (TARGET_TEMP + (table[brew_time] * AD_PER_DEGREE)) * adjust) { - if (pwm > 0.0 && pwm <= 1.0) { + if (temp < TARGET_TEMP + (table[brew_time] * AD_PER_DEGREE)) { + double pwm = table[brew_time] - (int)table[brew_time]; // decimal part of brew heat + // adjust? + if (pwm > 0.0 && pwm <= 1.0) { heater = ON; - wait(pwm); + heater_timer.attach(&heater_off, pwm); // schedule turn off + } else heater = OFF; - pwm = 1 - pwm; - if (pwm > 0.0 && pwm <= 1.0) - wait(pwm); - } else - wait(1.0); + } // if + + // *** pump power control + +#define MIN_PUMP .4 // below this is effectively zero + grams = ((double)read_scale2() - scale_zero) / AD_PER_GRAM; // current espresso weight + if (grams < 0) // clip impossible result + grams = 0; + double delta_grams = grams - prev_grams; + if (delta_grams < 0) + delta_grams = 0; + prev_grams = grams; + + if (brew_time >= START_FLOW_PROF) { // start flow profiling at specified time + ++flow_time; // seconds of significant flow + // adjust flow rate by changing pump power + // Proportional control + #define UP_GAIN .25 + #define DOWN_GAIN 1.2 + + double error = (delta_grams - scale_table[flow_time]) / scale_table[flow_time]; + + if (error > 0) + target_pump /= 1 + (error * DOWN_GAIN); // too fast + else + target_pump += -error * UP_GAIN; // too slow + + if (target_pump > pump_table[brew_time]) // clip to max allowed + target_pump = pump_table[brew_time]; + else if (target_pump < 0) // clip to min + target_pump = 0; } else - wait(1.0); + target_pump = pump_table[brew_time]; // use pump power profiling + + pump = MIN_PUMP + (1 - MIN_PUMP) * target_pump; // use the flow profiling value + debug("time = %u %u, grams = %F, delta = %F, target_pump = %F\r\n",brew_time,flow_time,grams,delta_grams,target_pump); + //debug("target temp %u = %f, temp = %u %u\r\n",brew_time,table[brew_time],read_temp(BOILER),read_temp(GROUP)); + + // record values for debugging and graphing 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)); - + scale_log[brew_time] = grams; // record espresso weight + // 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; + if (grams >= END_GRAMS) + break; + + // early exit based on user input (read twice for noise) + //if (brew_time > 10 && tsi.readPercentage() > .1 && tsi.readPercentage() < .5) { + // wait_ms(5); + // if (tsi.readPercentage() > .1 && tsi.readPercentage() < .5) + // break; + //} + + // wait till next second + ++brew_time; + int ms = (brew_time * 1000) - timer.read_ms(); + if (ms > 0) + wait_ms(ms); + + // cleanup + heater = OFF; // should be off already, but just in case + heater_timer.detach(); // disable off timer } // for // shut down led_color(OFF); - debug("brew done\r\n"); pump = OFF; heater = OFF; + debug("brew done, time = %u, grams = %f, target_pump = %F\r\n",brew_time, grams, target_pump); + } // brew() //=========================================================== @@ -341,10 +415,10 @@ heater = ON; // turn on heater led_color(PINK); // set LED to pink } - + if (tsi.readPercentage() > .5) // abort steam - break; - + break; + } // while heater = OFF; // turn off @@ -404,45 +478,40 @@ DigitalOut x7(PTD5); DigitalOut x8(PTD6); DigitalOut x9(PTD1); -DigitalOut x10(PTC0); +//DigitalOut x11(PTC3); #endif //======================================= // A/D routines //======================================= -DigitalOut ad_power(PTB9); // used to turn on/off power to resistors +DigitalOut ad_power(PTB9); // used to turn on/off power to resistors (self heating) +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) -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) +inline int median(int a, int b, int c) +{ + if ((a >= b && a <= c) || (a >= c && a <= b)) return a; + else if ((b >= a && b <= c) || (b >= c && b <= a)) return b; + else return c; +} // median() +// heavily averaged A/D reading + 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; + +#define COUNT 77 // number of samples to average - 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; - - } // for + for (int i = 0; i < COUNT; ++i) // average multiple for more accuracy + sum += median(adc.read_u16(),adc.read_u16(),adc.read_u16()); return sum / COUNT; - -} // read_temp() + +} // read_ad() // read a temperature in A/D counts @@ -450,27 +519,207 @@ unsigned read_temp(int device) { -unsigned value; -unsigned max; // A/D reading for the vref supply voltage + unsigned value; + unsigned max; // A/D reading for the vref supply voltage - // send power to analog resistors only when needed - // this limits self heating - + // 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 + + if (device == BOILER) + value = (read_ad(boiler) * 65536) / max; // scale to vref else - value = (read_ad(group) * 65536) / max; // scale to vref - + value = (read_ad(group) * 65536) / max; // scale to vref + ad_power = 0; return value; -} // read_temp() - +} // read_temp() +// scale +#define FILTER .99 +#define SLEW 10 + +// average scale value over 800 msec +// approx 6.6 samples/msec + +int read_scale2() +{ + int sum=0, count=0; + int raw, prev_raw; + + Timer t; + t.start(); + prev_raw = scale.read_u16(); + + scount = 0; + + for (count = 0; t.read_ms() < 800; ++count) { + raw = scale.read_u16(); + + slog[scount] = raw; // log it + if (++scount >= 5000) + scount = 5000; + + // clip to slew rate limits + if (raw > prev_raw + SLEW) + raw = prev_raw + SLEW; + else if (raw < prev_raw - SLEW) + raw = prev_raw - SLEW; + + prev_raw = raw; + + sum += raw; + } // for + + return sum / count; +} + +// take average of scale A/D max and min over multiple inflection points +// this reduces oscillation noise + +int read_scale() +{ + int value, prev_value=0, prev_prev_value, max1, max2, min; + unsigned tmp; + scount = 0; + + Timer t; + Timer total; + t.start(); + total.start(); + + // note: the effectiveness of this HF noise filter is highly dependent on sample rate + +#define update_value() {\ + tmp = scale.read_u16(); \ + slog[scount++] = tmp; \ + value = FILTER * value + (1.0-FILTER) * tmp; \ + prev_prev_value = prev_value; \ + prev_value = value; } + + // get a good filtered value + value = scale.read_u16(); + for (int i = 0; i < 50; ++i) + update_value(); + + // wait for upward slope + while (value < prev_value || prev_value < prev_prev_value) + update_value(); + + // delay for 2 msec + for (t.reset(); t.read_ms()< 2;) + update_value(); + + // find local max + for (;;) { + value = FILTER * value + (1.0-FILTER) * scale.read_u16(); // IIR filter + if (prev_value > value && prev_value > prev_prev_value) { + max1 = prev_value; + break; + } + prev_prev_value = prev_value; + prev_value = value; + } // for + + // delay for 2 msec + for (t.reset(); t.read_ms()< 2;) + update_value(); + + // find local min + for (;;) { + value = FILTER * value + (1.0-FILTER) * scale.read_u16(); // IIR filter + if (prev_value < value && prev_value < prev_prev_value) { + min = prev_value; + break; + } + prev_prev_value = prev_value; + prev_value = value; + } // for + + // delay for 2 msec + for (t.reset(); t.read_ms()< 2;) + update_value(); + + // find another max + for (;;) { + value = FILTER * value + (1.0-FILTER) * scale.read_u16(); // IIR filter + if (prev_value > value && prev_value > prev_prev_value) { + max2 = prev_value; + break; + } + prev_prev_value = prev_value; + prev_value = value; + } // for + + //debug("read scale in %d msec, %d %d %d\r\n",total.read_ms(),max1, max2, min); + + return (((max1 + max2) / 2) + min) / 2; +} // read_scale() + +int read_scale3() +{ + int max=0, min=65535; + int value, prev_raw; + + scount = 0; + +// note: the effectiveness of this HF noise filter is highly dependent on sample rate +// about 6.6 samples/msec + + Timer t; + t.start(); + + prev_raw = value = scale.read_u16(); + + while (t.read_ms() < 40) { // 25 msec should always include a full oscillation + int raw = scale.read_u16(); + slog[scount++] = raw; + + // clip to slew rate limits + if (raw > prev_raw + SLEW) + raw = prev_raw + SLEW; + else if (raw < prev_raw - SLEW) + raw = prev_raw - SLEW; + + prev_raw = raw; + + value = FILTER * value + (1.0-FILTER) * raw; + + if (scount > 50) { // only start after enough data + if (value > max) + max = value; + if (value < min) + min = value; + } // if + } // while + + //debug("%d values, %d %d\r\n",scount,max,min); + return (max + min) / 2; + +} // read_scale3() +// flow meter sends a pulse every .5 ml of flow +InterruptIn flow_meter(PTA4); // digital input pin +Timer flow_timer; +int flow_period; // time between pulses in usec + +void flow_pulse() // interrupt routine +{ + static int prev_time = 0; + int pulse_time = flow_timer.read_us(); + + flow_period = pulse_time - prev_time; + prev_time = pulse_time; +} + +void flow_setup() +{ + flow_timer.start(); + flow_meter.rise(&flow_pulse); // attach the address of the flip function to the rising edge +} // flow_setup()