Controlling fridge and beer temperature with a predictive on/off algorithm and PID

Posted on Jan 4, 2012 in Arduino, Beer, C++, Electronics, Programming, UberFridge | 5 comments

UberFridge is capable of controlling the temperature of a fermenting beer with 0.1° C accuracy. It can also be set to keep the fridge temperature within a -0.5 to +0.5 °C range. How this is done in an energy efficient manner is explained in this article.

Controlling the fridge temperature

To control the fridge temperature, I use a predictive on/off algorithm. Why do I need a predictive algorithm? Because waiting on measurements doesn’t work.

On/Off actuators

The temperature in the fridge cbe lowered by turning on the compressor. To increase the temperature, I just turn on the light in the fridge. This is a 15W light bulb and it is perfectly capable of raising the temperature to 30 degrees, just not very fast.

Both these actuators are on/off actuators; they cannot be set to an intermediate intensity. It would be possible to use pulse width modulation, which is done by most commercial PID controllers you can buy. What you do have to take into account when using PWM is that the compressor cannot be switched on and off too quickly, because this reduces it’s lifespan.

I chose not to use PWM, but to keep it simple at first: cool when it’s too hot, heat when it’s too cold. This prove to be a bit harder than it seemed at first glance.

Overshoot

Cooling when it’s too hot and heating when it’s too cold does not work well in practice. This is because the temperature at the sensor does not respond immediately to turning on the compressor. What happens is this: the compressor is turned on and the back plate of the fridge is cooled. The back plate slowly cools the air in the fridge, which in turn cools the sensor. At the moment the sensor has reached the target temperature, the back plate temperature can be 10 degrees under target temperature. This causes the air temperature to be cooled long after the compressor is turned off, with as a result that the target temperature is overshot by a few degrees.

Estimating overshoot

I realized that I would need to turn off the compressor way before the target temperature was reached, ideally at the moment that the overshoot would exactly land on the target temperature. This would be the fasted way possible to control the fridge temperature. But to be able to do this, I needed to be able to estimate the overshoot.

My initial attempt to estimate overshoot was to use the derivative of the temperature as an estimate. Much like a moving object wanting to continue it’s path, I thought of the temperature to have momentum. My estimate for the overshoot was \alpha (\frac{dT}{dt})^2. This turned out to be a very bad estimator.

The back of the fridge cools so quickly compared to the air in the fridge, that at the moment the value at the sensor is starting to change, it has already cooled enough. Any estimate based on sensor readings therefore is flawed.

My next approach was much simpler: the overshoot is estimated based on how long the compressor has been on. Even with a simple proportional estimator, this worked reasonably well. In a formula: \text{Overshoot} = \alpha t. What’s left is to determine alpha.

A self learning estimator

The amount of overshoot depends on more than just the time the compressor or light bulb has been on: it also depends on the set point and on what is in the fridge. Alpha therefore cannot be considered a constant, but it is fairly constant when a beer is fermenting for a week at the same temperature. I therefore chose to let the fridge learn what the right value of alpha should be over time.

Heating and cooling are very different in their overshoot properties, so I have two parameters: a heatOvershootEstimator and a coolOvershootEstimator. Both are adjusted independently. I will only show the code for cooling here, because the code for heading is very similar.

When the fridge is cooling it calculates whether the current temperature plus the estimated overshoot would land on the target temperature. When it does, it stops cooling and waits for the overshoot to happen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    case COOLING:
      doNegPeakDetect=1;
      lastCoolTime = millis();    
      estimatedOvershoot = coolOvershootEstimator  * min(MAX_COOL_TIME_FOR_ESTIMATE, (float) timeSinceIdle()/(1000))/60;
      estimatedPeakTemperature = fridgeTemperatureActual - estimatedOvershoot;
      if(estimatedPeakTemperature <= fridgeTemperatureSetting + COOLING_TARGET){
        fridgeSettingForNegPeakEstimate=fridgeTemperatureSetting;
        state=IDLE;
        return;
      }
      break;
    case HEATING:
       lastHeatTime=millis();
       doPosPeakDetect=1;    
       estimatedOvershoot = heatOvershootEstimator * min(MAX_HEAT_TIME_FOR_ESTIMATE, (float) timeSinceIdle()/(1000))/60;
       estimatedPeakTemperature = fridgeTemperatureActual + estimatedOvershoot;
      if(estimatedPeakTemperature >= fridgeTemperatureSetting + HEATING_TARGET){
        fridgeSettingForPosPeakEstimate=fridgeTemperatureSetting;
        state=IDLE;
        return;
      }
      break;

As you can see, I have set a maximum on the cooling time that is accounted for.

Peak Detection

To detect whether the estimator was correct, I have to determine the actual amount of overshoot that happened. To do that I detect peaks in the fridge temperature. When I find a peak, I compare it to the estimated peak and adjust the estimator accordingly. The input for the peak detection is heavily filtered so that only big global peaks are detected.

I have defined a small range in which the overshoot can land. When it lands in this range, I leave the estimator at its current value. When the overshoot was more than expected, my estimate was too low and I increase the estimator. When the overshoot was less than expected, I decrease the estimator. The change is 20-50%, depending on the error.

I final situation I had to take into account was that the overshoot was too low, but the temperature drifts in the right direction after going back to idle. That is the last part of the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void detectPeaks(void){
  //detect peaks in fridge temperature to tune overshoot estimators
  if(doPosPeakDetect && state!=HEATING){
    if(fridgeTempFiltSlow[3] = fridgeTempFiltSlow[1]){ // maximum
      posPeak=fridgeTempFiltSlow[2];
      if(posPeak>fridgeSettingForPosPeakEstimate+HEATING_TARGET_UPPER){
        //should not happen, estimated overshoot was too low, so adjust overshoot estimator
        heatOvershootEstimator=heatOvershootEstimator*(1.2+min((posPeak-(fridgeSettingForPosPeakEstimate+HEATING_TARGET_UPPER))*.03,0.3));
        saveSettings();
      }
      if(posPeak 580000UL && timeSinceCooling() > 900000UL && fridgeTempFiltSlow[3] < fridgeSettingForPosPeakEstimate+HEATING_TARGET_LOWER){       //there was no peak, but the estimator is too low. This is the heat, then drift up situation.         posPeak=fridgeTempFiltSlow[3];         heatOvershootEstimator=heatOvershootEstimator*(0.8+max((posPeak-(fridgeSettingForPosPeakEstimate+HEATING_TARGET_LOWER))*.03,-0.3));         saveSettings();         doPosPeakDetect=0;         serialFridgeMessage(POSDRIFT);     }   }   if(doNegPeakDetect && state!=COOLING){     if(fridgeTempFiltSlow[3] >= fridgeTempFiltSlow[2] && fridgeTempFiltSlow[2] fridgeSettingForNegPeakEstimate+COOLING_TARGET_UPPER){
        //should not happen, estimated overshoot was too high, so adjust overshoot estimator
        coolOvershootEstimator=coolOvershootEstimator*(0.8+max(((fridgeSettingForNegPeakEstimate+COOLING_TARGET_UPPER)-negPeak)*.03,-0.3));
        saveSettings();
      }
      doNegPeakDetect=0;
      serialFridgeMessage(NEGPEAK);
    }
    else if(timeSinceCooling() > 1780000UL && timeSinceHeating() > 1800000UL && fridgeTempFiltSlow[3] > fridgeSettingForNegPeakEstimate+COOLING_TARGET_UPPER){
      //there was no peak, but the estimator is too low. This is the cool, then drift down situation.
        negPeak=fridgeTempFiltSlow[3];
        coolOvershootEstimator=coolOvershootEstimator*(0.8+max((negPeak-(fridgeSettingForNegPeakEstimate+COOLING_TARGET_UPPER))*.03,-0.3));
        saveSettings();
        doNegPeakDetect=0;
        serialFridgeMessage(NEGDRIFT);
    }
  }
}

Energy efficiency

When the algorithm is waiting for an estimated overshoot, it should stay idle to not influence the result. When the fridge is allowed to compensate an overshoot in cooling by immediately heating again, this will make the fridge very energy inefficient. I have minimal waiting times for heating and cooling to address this. The fridge is allowed to heat or cool only when:

  • cooling is 15 minutes ago or a negative peak has already been detected
  • heating is 10 minutes ago or a positive peak has already been detected

The idle range is -0.5 to +0.5 ° C: the fridge will stay idle when the fridge temperature is in this range. When the estimators have the correct value, the overshoot lands in the middle of this range. When the temperature setting is constant, the fridge will only heat or only cool to get the temperature back at its setting, no energy is wasted by alternating between heating and cooling.

Controlling the beer temperature with PID

Now that I have a good algorithm to control the fridge temperature, I can use the fridge temperature setting to get the fermenting beer at the right value. The difference between fridge and beer temperature can be seen as an actuator for the beer temperature. This difference is set by a PID algorithm.

Why PID?

PID stands for proportional, derivative and integral: the ‘force’ of the actuator is a weighted sum of the error, the derivative of the error and the integral of the error.

Proportional

The controller wouldn’t work without a proportional part. The proportional part is just common sense: a bigger error needs a bigger correction.

Integral

The purpose of the integral term is to correct steady state errors. When the settings and temperatures have reached a state in which they are in equilibrium, but not at the correct values, there is a constant small error in beer temperature. This could happen when the fermentation is generating heat: the beer temperature is 0.2 degrees to high, the fridge temperature is set at 1 degree below the beer temperature to correct his, but this is in equilibrium.

In the integral of the error, the small error will add up over time, causing the fridge setting to be lowered gradually until the equilibrium is at the right setting.

The integral term is only needed to correct steady state errors. When the system is not in steady state, the integral term will cause additional overshoot. The integral is therefore set to zero when the error is bigger than 0.3 degrees or the slope is not close to horizontal.

Derivative

The purpose of the derivative term is to prevent overshoot. The derivative of the error is a measure of how fast the temperature is moving in the right direction. If it is moving fast, I can stop heating/cooling earlier because the overshoot will take it to the right temperature.

Finding the right settings

I have updated the PID algorithm and the information here on 23-11-2012. I placed the fridge sensor a bit lower, because it reacted too fast to heating and too slow to cooling.

I searched for the right settings experimentally. This is a terribly slow process unfortunately, because it takes an hour to cool 20 liters of water half a degree and 4 hours to heat it half a degree. These are the settings I am using at the moment:

KpHeat = 10, KpCool = 5, Ki=0.02, KdHeat = -5, KdCool = -10.

As you can see I have different parameters for heating and cooling, because their properties are different. Cooling is faster but the overshoot from cooling has more lag. The Kd parameters are negative because I use the slope of the beer temperature instead of the slope of the error (they are the same, but have different sign). The slope is the difference in beer temperature with 30 minutes ago.

The Ki parameter is the same for heating and cooling. Having different Ki’s would can cause a sudden big change in the fridge setting when the integral is large.

The fridge can only slowly transition between the heating and cooling parameters, otherwise the fridge temperature setting would jump suddenly when the beer temperature difference is around zero.

The code to update the fridge temperature setting is displayed below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// update fridge temperature setting, difference with beer setting is PID actuator
void updateSettings(void){  
  if(mode == BEER_CONSTANT || mode == BEER_PROFILE){   
    float beerTemperatureDifference =  beerTemperatureSetting-beerTempFiltSlow[3];
    if(abs(beerTemperatureDifference) < 5 && ((beerSlope <= 0.7 && beerSlope >= 0) || (beerSlope >= -1.4 && beerSlope <= 0))){     //difference is smaller than .5 degree and slope is almost horizontal
      if(abs(beerTemperatureDifference)> 0.5){
        differenceIntegral = differenceIntegral + beerTemperatureDifference;
      }
    }
    else{
      differenceIntegral = differenceIntegral*0.9;
    }
 
    if(beerTemperatureDifference<0){ //linearly go to cool parameters in 3 hours
      Kp = constrain(Kp+(KpCool-KpHeat)/(360*3), KpCool, KpHeat);
      Kd = constrain(Kd+(KdCool-KdHeat)/(360*3), KdHeat, KdCool);
    }
    else{ //linearly go to heat parameters in 3 hours
      Kp = constrain(Kp+(KpHeat-KpCool)/(360*3), KpCool, KpHeat);
      Kd = constrain(Kd+(KdHeat-KdCool)/(360*3), KdHeat, KdCool);
    }
    fridgeTemperatureSetting = constrain(beerTemperatureSetting + Kp* beerTemperatureDifference + Ki* differenceIntegral + Kd*beerSlope, 40, 300);      
  }
  else{
    // FridgeTemperature is set manually
    beerTemperatureSetting = 0;
  }
}

I have one final condition for heating/cooling to prevent wasting energy: When the beer temperature is too low the fridge will not heat and vice versa, regardless of the fridge setting.

Below is a graph of a test with these settings. I am very pleased with the results.

The red line is the beer temperature setting, blue is the actual beer temperature, green is the fridge temperature setting and orange the actual fridge temperature. The peak detection and parameter adjustment is shown in the annotations.

For the full control code and the most up to date version of the algorithm, check the source code at Google Code.

The chart below if from a test run with water. As you can see, the beer temperature can be controlled very accurately. I just made a starter for the first beer I will brew with UberFridge, so a chart of a real one week fermentation can be expected after about a week.

5 Comments

  1. Hi,
    Did you get any sample data for your brew? I am wondering how well the system held temperature when the yeast was active.

    How big was the compiled code in the end?
    Have you experimented with how much free memory you have in your system?

  2. I’m using an arduino fridge controller for my fridge fermentor but haven’t seen any overshoot. I keep logs with a csv file on an sd card and noticed I don’t move more than about .3 degrees C. Perhaps this is because my fridge uses a blower while the compressor is on. Also my data may be slightly askew since I measure the fermentor wall temperature instead of the middle of the beer. I love your project and hope to implement some features in my own that uses an Android phone instead of a router. I had a question about your beer profile setup. Many trappist brewers start at a low temp but allow the temperature to rise naturally to a maximum temp. Does your system allow you to do this, say by disabling the use of the light bulb heater?

    • That is probably the cause indeed. I get the overshoot because the sensor is far from the back wall of the fridge. When the fridge starts cooling, the wall is a couple of degrees below target temperature in under a few minutes. Before the air is cooled by the back wall, the compressor has been on long enough. The blower will also help for a more even distribution.

      There is support for using a temperature profile to slowly increase the temperature, but not to do it naturally so far. It could be implemented by allowing a certain temperature range instead of an exact target temperature. Maybe something to implement in BrewPi in the future.

  3. Hi,

    I’m starting a new project involving the need to keep water temperature in an aquarium as close and steady as possible to 4 Celsius. Of course, the aquarium is in acrylic and has extra isolation to minimize heat exchange. I plan to control the chiller with an Arduino. I give myself plenty of time for testing before committing living organisms to the system.

    I’m not quite there yet but when I start writing the Arduino sketch, I will be looking at the PID library. I will look more closely at you code as well. I guess the main difference in our projects is that you have a mean to heat as well as cool where I will only use the cooling part.

    When I descided to use an Arduino to control the aquarium temperature, I suspected that overshooting could be an issue and looked on the web for similar projects and solutions. I just wanted to say that your work has been inspirational.

    Thank you for making it public!

    – Blaise

    • Nice, take a look at BrewPi as well. It’s an updated and much improved version of UberFridge.

Leave a Reply

Your email address will not be published. Required fields are marked *