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