:: BBCC: Bare Bones (PID) Coffee Controller ::

Intro

The BBCC is designed primarily to provide PID temperature control for Espresso and Coffee machines, but it may be more. It's a general purpose PID control loop at heart. A PID loop is a control algorithm that works off of feedback. In the coffee brewing case, it watches the temperature of a water boiler and controls the amount of time the heating coil is on on order to control that temperature. This code can be the basis for a more complex system, or simply stand alone. It requires an Arduino, a temperature sensor, and a solid state relay. The control interface is available through the Arduino hardware serial/usb (pins 0 and 1). This allows setup, monitoring, and PID tuning from any serial terminal.

This also comes with a partner application that runs in Processing. It is not required, but highly recommended as it will plot temperature over time and greatly aid in the PID tuning process. Download the plotter code BBCC Plotter

Hardware Setup

Digital pin 6 connects to a Solid State Relay that switches the heating coil. Analog input pin 1 connects to the AD595 thermocouple amplifier. This can be swapped with other temperature sensors. Note that the AD595 is for type K thermocouples. Use the AD594 for type J thermocouples. Either type should work fine.

Schematics for hardware installation are available on the schematics page.

For examples of espresso machines using dedicated PID controllers check out the following:
*Murph's Silvia PID page
*A Tail About Silvia

Code

The current version is 1.60
last updated: 09:29 Mar 11 2008
jump to code section:
Main
PID
Heater
EEPROM Floats
Temp Sensing
Serial Interface

Main

// BBCC Main
// Tim Hirzel
// February 2008
//
// Main file for the Bare Bones Coffee Controller PID
// setup for Arduino.
// This project is set up such that each tab acts as a
// "module" or "library" that incporporates some more
// functionality.  Each tab correlates
// to a particular device (Nunchuck),  protocol (ie. SPI),
// or Algorithm (ie. PID).

// The general rule for any of these tabs/sections is that
// if they include a setup* or update* function, those should be added
// into the main setup and main loop functions. Also, in main loop, and in
// extra code, delays should probably be avoided.
// Instead, use millis() and check for a certain interval to have passed.
//
// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0


// These are addresses into EEPROM memory.  The values to be stores are floats which
// need 4 bytes each.  Thus 0,4,8,12,...
#define PGAIN_ADR 0
#define IGAIN_ADR 4
#define DGAIN_ADR 8

#define ESPRESSO_TEMP_ADDRESS 12
//#define STEAM_TEMP_ADDRESS 12  // steam temp currently not used with bare bones setup

#define PID_UPDATE_INTERVAL 200 // milliseconds


float targetTemp;  //current temperature goal
float heatPower; // 0 - 1000  milliseconds on per second

unsigned long lastPIDTime;  // most recent PID update time in ms

void setup()
{

  setupPID(PGAIN_ADR, IGAIN_ADR, DGAIN_ADR ); // Send addresses to the PID module
  targetTemp = readFloat(ESPRESSO_TEMP_ADDRESS); // from EEPROM. load the saved value
  lastPIDTime = millis();
  // module setup calls
  setupHeater();
  setupSerialInterface();
  setupTempSensor();
}

void setTargetTemp(float t) {
  targetTemp = t;
  writeFloat(t, ESPRESSO_TEMP_ADDRESS);
}

float getTargetTemp() {
  return targetTemp;
}


void loop()
{  
  // this call interprets characters from the serial port
  // its a very basic control to allow adjustment of gain values, and set temp
  updateSerialInterface();
  updateTempSensor();

  // every second, udpate the current heat control, and print out current status

  // This checks for rollover with millis()
  if (millis() < lastPIDTime) {
    lastPIDTime = 0;
  }
  if ((millis() - lastPIDTime) > PID_UPDATE_INTERVAL) {
    lastPIDTime +=  PID_UPDATE_INTERVAL;
    heatPower = updatePID(targetTemp, getFreshTemp());
    setHeatPowerPercentage(heatPower);

  }  
  updateHeater();




}

// END BBCC Main
 

PID control algorithm


// PID control code
// Tim Hirzel
// December 2007

// This is a module that implements a PID control loop
// initialize it with 3 values: p,i,d
// and then tune the feedback loop with the setP etc funcs
//
// this was written based on a great PID by Tim Wescott:
// http://www.embedded.com/2000/0010/0010feat3.htm
//
//
// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0



#define WINDUP_GUARD_GAIN 100.0

float iState = 0;
float lastTemp = 0;

float pgain;
float igain;
float dgain;

float pTerm, iTerm, dTerm;

int pgainAddress, igainAddress, dgainAddress;

void setupPID(unsigned int padd, int iadd, int dadd) {
  // with this setup, you pass the addresses for the PID algorithm to use to
  // for storing the gain settings.  This way wastes 6 bytes to store the addresses,
  // but its nice because you can keep all the EEPROM address allocaton in once place.

  pgainAddress = padd;
  igainAddress = iadd;
  dgainAddress = dadd;

  pgain = readFloat(pgainAddress);
  igain = readFloat(igainAddress);
  dgain = readFloat(dgainAddress);
}

float getP() {
  // get the P gain
  return pgain;
}
float getI() {
  // get the I gain
  return igain;
}
float getD() {
  // get the D gain
  return dgain;
}


void setP(float p) {
  // set the P gain and store it to eeprom
  pgain = p;
  writeFloat(p, pgainAddress);
}

void setI(float i) {
  // set the I gain and store it to eeprom
  igain = i;
  writeFloat(i, igainAddress);
}

void setD(float d) {
  // set the D gain and store it to eeprom
  dgain = d;
  writeFloat(d, dgainAddress);
}

float updatePID(float targetTemp, float curTemp)
{
  // these local variables can be factored out if memory is an issue,
  // but they make it more readable
  double result;
  float error;
  float windupGaurd;

  // determine how badly we are doing
  error = targetTemp - curTemp;

  // the pTerm is the view from now, the pgain judges
  // how much we care about error we are this instant.
  pTerm = pgain * error;

  // iState keeps changing over time; it's
  // overall "performance" over time, or accumulated error
  iState += error;

  // to prevent the iTerm getting huge despite lots of
  //  error, we use a "windup guard"
  // (this happens when the machine is first turned on and
  // it cant help be cold despite its best efforts)

  // not necessary, but this makes windup guard values
  // relative to the current iGain
  windupGaurd = WINDUP_GUARD_GAIN / igain;  

  if (iState > windupGaurd)
    iState = windupGaurd;
  else if (iState < -windupGaurd)
    iState = -windupGaurd;
  iTerm = igain * iState;

  // the dTerm, the difference between the temperature now
  //  and our last reading, indicated the "speed,"
  // how quickly the temp is changing. (aka. Differential)
  dTerm = (dgain* (curTemp - lastTemp));

  // now that we've use lastTemp, put the current temp in
  // our pocket until for the next round
  lastTemp = curTemp;

  // the magic feedback bit
  return  pTerm + iTerm - dTerm;
}

void printPIDDebugString() {
  // A  helper function to keep track of the PID algorithm
  Serial.print("PID formula (P + I - D): ");

  printFloat(pTerm, 2);
  Serial.print(" + ");
  printFloat(iTerm, 2);
  Serial.print(" - ");
  printFloat(dTerm, 2);
  Serial.print(" POWER: ");
  printFloat(getHeatCycles(), 0);
  Serial.print(" ");

}

// END PID
 

Heater Control (PWM)

// HeaterControl
// Tim Hirzel
// Dec 2007
//
// This file is for controlling a heater via a solid state zero crossing relay

// since these are zero-crossing relays, it makes sense to just match my local
// AC frequency, 60hz
//
// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0

#define HEAT_RELAY_PIN 6

float heatcycles; // the number of millis out of 1000 for the current heat amount (percent * 10)

boolean heaterState = 0;

unsigned long heatCurrentTime, heatLastTime;

void setupHeater() {
  pinMode(HEAT_RELAY_PIN , OUTPUT);
}


void updateHeater() {
  boolean h;
  heatCurrentTime = millis();
  if(heatCurrentTime - heatLastTime >= 1000 or heatLastTime > heatCurrentTime) { //second statement prevents overflow errors
    // begin cycle
    _turnHeatElementOnOff(1);  //
    heatLastTime = heatCurrentTime;  
  }
  if (heatCurrentTime - heatLastTime >= heatcycles) {
    _turnHeatElementOnOff(0);
  }
}

void setHeatPowerPercentage(float power) {
  if (power <= 0.0) {
    power = 0.0;
  }    
  if (power >= 1000.0) {
    power = 1000.0;
  }
  heatcycles = power;
}

float getHeatCycles() {
  return heatcycles;
}

void _turnHeatElementOnOff(boolean on) {
  digitalWrite(HEAT_RELAY_PIN, on);     //turn pin high
  heaterState = on;
}

// End Heater Control
 

EEPROM float storage utility

// Simple extension to the EEPROM library
// Tim Hirzel
// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0

#include <avr/EEPROM.h>

float readFloat(int address) {
  float out;
  eeprom_read_block((void *) &out, (unsigned char *) address ,4 );
  return out;
}

void writeFloat(float value, int address) {
  eeprom_write_block((void *) &value, (unsigned char *) address ,4);
}

// END EEPROM Float
 

Temperature Sensor

// With the AD 595, this process is just a matter of doing some math on an
// analog input
//
// Thanks to Karl Gruenewald for the conversion formula
// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0

// This current version is based on sensing temperature with
// an AD595 and thermocouple through an A/D pin.  Any other
// sensor could be used by replacing this one function.
// feel free to use degrees C as well, it will just give a different
// PID tuning than those from F.
//


#define TEMP_SENSOR_PIN 1

float tcSum = 0.0;
float latestReading = 0.0;
int readCount = 0;
float multiplier;
void setupTempSensor() {
  multiplier = 1.0/(1023.0) * 500.0 * 9.0 / 5.0;
}  

void updateTempSensor() {
    tcSum += analogRead(TEMP_SENSOR_PIN); //output from AD595 to analog pin 1
    readCount +=1;
}

float getFreshTemp() {
      latestReading = tcSum* multiplier/readCount+32.0;
      readCount = 0;
      tcSum = 0.0;
  return latestReading;

}

float getLastTemp() {
  return latestReading;

}

// END Temperature Sensor
 

Serial Interface

//serialInterface
// Tim Hirzel February 2008
// This is a very basic serial interface for controlling the PID loop.
// thanks to the Serial exampe code  

// All code released under
// Creative Commons Attribution-Noncommercial-Share Alike 3.0

#define AUTO_PRINT_INTERVAL 200  // milliseconds
#define MAX_DELTA  100
#define MIN_DELTA  0.01
#define PRINT_PLACES_AFTER_DECIMAL 2  // set to match MIN_DELTA


int incomingByte = 0;
float delta = 1.0;
boolean autoupdate;
boolean printmode = 0;

unsigned long lastUpdateTime = 0;
void setupSerialInterface()  {
  Serial.begin(115200);
  Serial.println("\nWelcome to the BBCC, the Bare Bones Coffee Controller for Arduino");
  Serial.println("Send back one or more characters to setup the controller.");
  Serial.println("If this is your initial run, please enter 'R' to Reset the EEPROM.");
  Serial.println("Enter '?' for help.  Here's to a great cup!");
}

void printHelp() {
  Serial.println("Send these characters for control:");
  Serial.println("<space> : print status now");
  Serial.println("u : toggle periodic status update");
  Serial.println("g : toggle update style between human and graphing mode");
  Serial.println("R : reset/initialize PID gain values");
  Serial.println("b : print PID debug values");
  Serial.println("? : print help");  
  Serial.println("+/- : adjust delta by a factor of ten");
  Serial.println("P/p : up/down adjust p gain by delta");
  Serial.println("I/i : up/down adjust i gain by delta");
  Serial.println("D/d : up/down adjust d gain by delta");
  Serial.println("T/t : up/down adjust set temp by delta");


}

void updateSerialInterface() {
  while(Serial.available()){

    incomingByte = Serial.read();
    if (incomingByte == 'R') {
      setP(30.0); // make sure to keep the decimal point on these values
      setI(0.0);  // make sure to keep the decimal point on these values
      setD(0.0);  // make sure to keep the decimal point on these values
      setTargetTemp(200.0); // here too
    }
    if (incomingByte == 'P') {
      setP(getP() + delta);
    }
    if (incomingByte == 'p') {
      setP(getP() - delta);
    }
    if (incomingByte == 'I') {
      setI(getI() + delta);
    }
    if (incomingByte == 'i') {
      setI(getI() - delta);
    }
    if (incomingByte == 'D') {
      setD(getD() + delta);
    }
    if (incomingByte == 'd' ){
      setD(getD() - delta);
    }
    if (incomingByte == 'T') {
      setTargetTemp(getTargetTemp() + delta);
    }
    if (incomingByte == 't') {
      setTargetTemp(getTargetTemp() - delta);
    }
    if (incomingByte == '+') {
      delta *= 10.0;
      if (delta > MAX_DELTA)
        delta = MAX_DELTA;
    }
    if (incomingByte == '-') {
      delta /= 10.0;
      if (delta < MIN_DELTA)
        delta = MIN_DELTA;

    }
    if (incomingByte == 'u') {
      // toggle updating

      autoupdate = not autoupdate;
    }
    if (incomingByte == 'g') {
      // toggle updating

      printmode = not printmode;
    }
    if (incomingByte == ' ') {
      // toggle updating

      printStatus();
    }
    if (incomingByte == '?') {
      printHelp();
    }
    if (incomingByte == 'b') {
      printPIDDebugString();
      Serial.println();
    }
  }

  if (millis() < lastUpdateTime) {
    lastUpdateTime = 0;
  }
  if ((millis() - lastUpdateTime) > AUTO_PRINT_INTERVAL) {
    // this is triggers every slightly more than a second from the delay between these two millis() calls
    lastUpdateTime += AUTO_PRINT_INTERVAL;
    if (autoupdate) {
      if (printmode) {
        printStatusForGraph();
      }
      else {
        printStatus();
      }
    }
  }
}

void printStatus() {
  // A means for getting feedback on the current system status and controllable parameters
  Serial.print(" SET TEMP:");
  printFloat(getTargetTemp(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", CUR TEMP:");
  printFloat(getLastTemp(),PRINT_PLACES_AFTER_DECIMAL);

  Serial.print(", GAINS p:");
  printFloat(getP(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(" i:");
  printFloat(getI(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(" d:");
  printFloat(getD(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", Delta: ");
  printFloat(delta,PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", Power: ");
  printFloat((float)getHeatCycles(), 0);

  Serial.print(" \n");
}

void printStatusForGraph() {
  printFloat(getTargetTemp(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", ");
  printFloat(getLastTemp(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", ");
  printFloat(getP(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", ");
  printFloat(getI(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", ");
  printFloat(getD(),PRINT_PLACES_AFTER_DECIMAL);
  Serial.print(", ");
  printFloat((float)getHeatCycles(), 0);
  Serial.println();
}

// printFloat prints out the float 'value' rounded to 'places' places after the decimal point
void printFloat(float value, int places) {
  // this is used to cast digits
  int digit;
  float tens = 0.1;
  int tenscount = 0;
  int i;
  float tempfloat = value;

  // make sure we round properly. this could use pow from <math.h>, but doesn't seem worth the import
  // if this rounding step isn't here, the value  54.321 prints as 54.3209

  // calculate rounding term d:   0.5/pow(10,places)  
  float d = 0.5;
  if (value < 0)
    d *= -1.0;
  // divide by ten for each decimal place
  for (i = 0; i < places; i++)
    d/= 10.0;    
  // this small addition, combined with truncation will round our values properly
  tempfloat +=  d;

  // first get value tens to be the large power of ten less than value
  // tenscount isn't necessary but it would be useful if you wanted to know after this how many chars the number will take

  if (value < 0)
    tempfloat *= -1.0;
  while ((tens * 10.0) <= tempfloat) {
    tens *= 10.0;
    tenscount += 1;
  }


  // write out the negative if needed
  if (value < 0)
    Serial.print('-');

  if (tenscount == 0)
    Serial.print(0, DEC);

  for (i=0; i< tenscount; i++) {
    digit = (int) (tempfloat/tens);
    Serial.print(digit, DEC);
    tempfloat = tempfloat - ((float)digit * tens);
    tens /= 10.0;
  }

  // if no places after decimal, stop now and return
  if (places <= 0)
    return;

  // otherwise, write the point and continue on
  Serial.print('.');  

  // now write out each decimal place by shifting digits one by one into the ones place and writing the truncated value
  for (i = 0; i < places; i++) {
    tempfloat *= 10.0;
    digit = (int) tempfloat;
    Serial.print(digit,DEC);  
    // once written, subtract off that digit
    tempfloat = tempfloat - (float) digit;
  }
}

// END Serial Interface
 

Share