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-07-05
Revision:
6:56b205b46b42
Parent:
5:0393adfdd439

File content as of revision 6:56b205b46b42:


// 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 56.0       // 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 (6*3600)   // turn off heat after this many seconds
#define WAKEUP_TIME 11          // 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

   DateTime dt = rtclock.now();                     // check clock value
   if (dt.year() > 2090 || dt.year() < 2014) {
        DateTime compiled(__DATE__, __TIME__);      // to set RT clock initially
        rtclock.adjust(compiled);
        DateTime dt = rtclock.now();                // check again
    }

    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;               // initial duty cycle (pump off)
    pump.period_us(410);    // period of PWM signal in us

    if (ambient_temp < MAX_ROOM_TEMP)   // check for cold boiler
        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()