Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
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()