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

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()      
+     
 
 
 
 
+