Controls both heat and pump pressure based on a temperature probe and a scale- ie, it does temperature and flow profiling. Should work with any vibratory pump machine.
Dependencies: Adafruit_RTCLib FastPWM TSI mbed
main.cpp
- Committer:
- jzeeff
- Date:
- 2014-04-14
- Revision:
- 5:0393adfdd439
- Parent:
- 4:3d661b485d59
- Child:
- 6:56b205b46b42
File content as of revision 5:0393adfdd439:
// 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, 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, remember to add offset) // 1362 ohms = 94C // 1374 ohms = 97C // 1000 ohms = too cold (0C) // 1520 ohms = too hot (136C) // 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 (10-15C?) #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. // 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 on a Gaggia Classic takes about 4 seconds before it is seen by the sensor 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 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 1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0 }; // 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.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 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 (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 #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 #define BLUE 3 #define WHITE 4 #define YELLOW 5 #define AQUA 6 #define PINK 7 #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 DigitalOut led_green(LED_GREEN); 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 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 uint16_t slog[5000]; int scount=0; int main() // start of program { time_t prev_time = 0; led_color(OFF); wait(1); // let settle ambient_temp = read_temp(BOILER); // save temp on startup #if 0 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("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 (ambient_temp < MAX_ROOM_TEMP) steam(7 * 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); // 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 if (temp > TARGET_TEMP) // adjust best guess for % heat needed heat *= (1-GAIN); else heat *= (1+GAIN); heater = ON; // turn on heater for PWM led_color(RED); wait(heat * 2.7); // 1.7 to reduce interaction with 50/60Hz power heater = OFF; // turn off heater led_color(GREEN); wait((1-heat) * 2.7); // total time is 2.7 seconds } // if // the user must press a button 10 seconds prior to brewing to start preheat if (tsi.readPercentage() > .5) { brew(); set_time(0); // stay awake for awhile more } // if they signaled for steam //if (tsi.readPercentage() > .1 && tsi.readPercentage() < .5) // steam(120); 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 || 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() && 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; 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) 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); wait(60); 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 prev_time = time(NULL); } // for (;;) } // main() // turn off the heater void heater_off(void) { heater = OFF; } //================================================================= // 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. //================================================================= void brew(void) { 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" 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; // 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; 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 } // *** 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)) { double pwm = table[brew_time] - (int)table[brew_time]; // decimal part of brew heat // adjust? if (pwm > 0.0 && pwm <= 1.0) { heater = ON; heater_timer.attach(&heater_off, pwm); // schedule turn off } else heater = OFF; } // 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 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 scale_log[brew_time] = grams; // record espresso weight // early exit if final weight reached 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); pump = OFF; heater = OFF; debug("brew done, time = %u, grams = %f, target_pump = %F\r\n",brew_time, grams, target_pump); } // brew() //=========================================================== // control to a higher steam temperature for n seconds //=========================================================== void steam(int seconds) { unsigned start_time = time(NULL); 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 led_color(AQUA); // set LED to aqua } else { heater = ON; // turn on heater led_color(PINK); // set LED to pink } if (tsi.readPercentage() > .5) // abort steam break; } // while heater = OFF; // turn off } // steam() // ============================================= // set multi color LED state // ============================================= DigitalOut r (LED_RED); DigitalOut g (LED_GREEN); DigitalOut b (LED_BLUE); void led_color(int color) { // turn off r = g = b = 1; switch (color) { case OFF: break; case GREEN: g = 0; break; case BLUE: b = 0; break; case RED: r = 0; break; case YELLOW: r = g = 0; break; case AQUA: b = g = 0; break; case PINK: r = b = 0; break; case WHITE: r = g = b = 0; break; } // switch } // 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 x11(PTC3); #endif //======================================= // A/D routines //======================================= 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) 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; #define COUNT 77 // number of samples to average 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_ad() // 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 // 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() // 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()