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:
- 2013-10-02
- Revision:
- 4:3d661b485d59
- Parent:
- 3:eb60e36b03f6
- Child:
- 5:0393adfdd439
File content as of revision 4:3d661b485d59:
// 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 - CHANGE THIS // 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 #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 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 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 (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 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 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); 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[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; 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 (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 // 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); 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 < 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 am if it occurs if (time(NULL) > SLEEP_PERIOD) { // 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; 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() //================================================================= // This subroutine is called when the button is pressed, 10 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"?? //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); 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 or on/off of pump for this period 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 (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); 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() //=========================================================== // 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 x10(PTC0); #endif //======================================= // A/D routines //======================================= 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; 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 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 // 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()