Malting barley is a critical step in the brewing of a nice beer. Brewers rely on maltsters to craft this ingredient. It involves soaking the grain in water and allowing the germination process to begin. When the germinating grain has reached a predetermined stage, it is kilned dry to halt the process and develop flavour. The grain will continue to germinate if the kiln temperature is too low. If the temperature is too hot the enzymes which for fermentable sugars from the starch in the grain will be destroyed making it impossible to make beer from the malt. This is a problem I found when using a cheap food dehydrator to kiln the malt so I’ve built a malting kiln which uses a heating element from a hot-air gun and an Arduino to control to air temperature while drying the grain. This provides tight control of the kilning process, aiding in the production of a quality ingredient.
So what made me want to figure out how to malt barley? Simple curiosity mostly but I have been brewing beer from grain for about 5 years now and was interested in what was for me the last piece of the puzzle of grain to beer. It started reading a book by Andrew Jefford, Peat Smoke and Spirit: a Portrait of Islay and its Whiskies. I was fascinated by the detail of whiskey making that this author had uncovered and thought I’d give it a go. Since historically, most distilleries had their own malting floors where they produced their own malt, I decided to start there. It took a quite a few attempts to figure out how to steep and germinate the barley so that I could get reasonable Hot Water Extract values (Read more about malt analysis here). I started kilning using a food dehydrator which had no temperature control. I found that the on air temperature was about 90 °C which was likely to destroy the enzymes. I had read that the early drying stage needs to be kept below 50 °C. So what a great excuse to build one using an Arduino to control the process!
If your interested in the software tool used in this project download our Arduino graphing program.
Malting
Raw barley grains are soaked in water to hydrate them followed by a period of germination. Once the little shoot that will grow above the ground has developed to about half to three quarters of the length of the grain, kilning can start. This involves blowing air, at no more than 50 °C, through a bed of grain until the air temperature leaving the grain raises to a similar temperature. The on temperature can then be raised until the final kiln temperature is reached. For whisky the end temperature might be 55 °C, for lager malt perhaps 70 °C and for pale ale malt about 105 °C.
For a steep tank, I modified a 7 L storage container with two fittings for aquarium air hoses in the bottom and one in the lid. This enabled me to bubble air through the steeping grain to keep it aerobic (anaerobic steep liquors can kill the germinating grains) as well as pumping humidified air through the bed during air rests and germination. This is based on a commercial technique known as pneumatic malting. This arrangement gives usable malt but it still germinates unevenly. I suspect I do not need to bubble air through the steeping grain continuously which starts germination before all the grains are hydrated, which I think produces uneven malting. Humidified air for steeping, air rests and germination was provided from a small aquarium air pump bubbling through a jar of water that was also kept in the fridge.
Malting time schedule that I used was: 7 hrs steep, 17 hr air rest, 7 hrs steep, 17 hr air rest, and 4 days germination.
This steep/germination procedure is sadly not yet automated but is something I’d like to have a go at. It was all placed in a fridge running at 12 °C. Warmer temperatures can cause the grain the germinate unevenly.
Kiln design
I built a simple flat bed kiln. The structure of the kiln is a wooden box 25 cm wide, 30 cm long and 24 cm high. Inside the box is a perforated stainless steel floor mounted at about one third of the height of the box from the bottom. The perforations are 2 mm in diameter which support the green malt and allow air to flow through it. Under this is a 300 W hot air gun element mounted in a tube about 20 mm larger in diameter. A small blower directs air from the room through a stainless steel tube surrounding the hot-air gun element. The heated air drys the grain as it passes through. To avoid damaging the grain with air that is too hot, or prolonging the drying process with cool air, an Arduino Uno monitors the air temperature going through the grain using a thermistor mounted just under the bed of malt. The Uno adjusts the air temperature using Proportional-Integral control by switching the element on and off, through a relay to maintain the air temperature entering the grain bed of 50 °C. An LCD screen displays the current temperature, set-point and element duty cycle. Buttons beneath the LCD are used to adjust the set-point and the grain temperature data from a second thermistor buried in the bed of grain is sent out the serial port. The drying process can be monitored by graphing the grain temperature with MegunoLink. It takes around 10 hrs to dry 3 kg of dry-weight barley.
I used the Arduino PID library created by Brett Beauregard to control the hot-air gun element, adjusting the control system parameters by using his autotune code which I modified for relay control. Temperature was measured with a 100 kΩ thermistor; Alan Wendt provides a simple guide to calculating temperature from the themistor resistance in the Arduino forums.
And as a disclaimer, all mains wiring is dangerous. Make sure someone competent oversees your work if you are unfamiliar with mains wiring. This is also a serious fire hazard, air blowing into a heated chamber above which is several kilos of fuel. I don’t recommend running this unattended.
Kilning
Once the germinated malt grains have been loaded into the kiln. The kiln is started. The screenshot shows MegunoLink plotting the control values and actual temperature readings being sent from the Arduino via the serial port. This gives great visual feedback that the control system is behaving well and only took a few lines of code to add. The onTemp is the controlled temperature being blown into the bottom of the bed of grains. The offTemp is the temperature of the air leaving the bed of grain. The setpoint is the target value for the onTemp and control[%] is the fraction of a 5 second window that the heater relay will be turned on.
If you are interested in plotting data Grab MegunoLink Pro here.
Code
Here’s the code that controls the kiln:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
/* This code runs a small malt kiln with a squirrel cage blower and a 300W * ceramic heater element. * Richard Oliver */ #include <pid_v1.h> // digital pin connections #define heaterPin 3 #define fanPin 2 // analog pin connections #define onTempPin 1 // sensor is just below grain bed, this is the control temp #define offTempPin 2 // sensor is on top of grain bed // pushbuttons #define buttonRIGHT 0 #define buttonUP 1 #define buttonDOWN 2 #define buttonLEFT 3 #define buttonSELECT 4 #define buttonNONE 5 //declare functions float getTemp(int analogChannel); void setRelayState(int Pin); int getButtonId (int analogValue); void updateLcd (float onTemp, float offTemp); void sendPlotData(String seriesName, float data); //Initialize PID double pidSetpoint, pidInput, pidOutput, pidWindowOutput; PID heaterPID(&pidInput, &pidOutput, &pidSetpoint, 1520, 4.3, 0, DIRECT); int pidWindowSize = 5000; unsigned long pidWindowStartTime; float onTemp, offTemp; // initialize the lcd library LiquidCrystal lcd(8, 9, 4, 5, 6, 7); unsigned long lcdUpdateTimer; int lcdUpdateInterval = 1000; //initialize ramp boolean breakPointReached = false; boolean rampEnded = false; float startTemp = 50; float rampEndTemp = 105; float rampRate = 1; // degrees per minute unsigned long rampTimerStartTime; double rampStartTemp = pidSetpoint; void setup() { Serial.begin(9600); pinMode(fanPin, OUTPUT); pinMode(heaterPin,OUTPUT); lcd.begin(16,2); lcd.print("Malt Kiln v1.3"); delay(2000); heaterPID.SetMode(AUTOMATIC); heaterPID.SetOutputLimits(0, pidWindowSize); pidWindowStartTime = millis(); pidSetpoint = startTemp; // turn on fan for duration of kilning digitalWrite(fanPin, HIGH); // this is start point for lcd refresh rate lcdUpdateTimer = millis(); onTemp = getTemp(onTempPin); offTemp = getTemp(offTempPin); } void loop() { onTemp = 0.9 * onTemp + 0.1 * getTemp(onTempPin); offTemp = 0.9 * offTemp + 0.1 * getTemp(offTempPin); if ( (offTemp >= startTemp) && !breakPointReached) { breakPointReached = true; rampTimerStartTime = millis(); rampStartTemp = pidSetpoint; } if (breakPointReached && !rampEnded) { float rampMinutesElapsed = float(millis() - rampTimerStartTime) / 60000; pidSetpoint = rampRate * rampMinutesElapsed + rampStartTemp; } if (onTemp >= rampEndTemp && !rampEnded) { rampEnded = true; pidSetpoint = rampEndTemp; } if (rampEnded == true && offTemp >= rampEndTemp) { pidSetpoint = 0; digitalWrite(fanPin, LOW); } pidInput = onTemp; heaterPID.Compute(); setRelayState(heaterPin); int buttonState = analogRead(0); if (buttonState < 1000) { int buttonId = getButtonId(buttonState); switch(buttonId) { case buttonUP: ++pidSetpoint; updateLcd(onTemp, offTemp); break; case buttonDOWN: --pidSetpoint; updateLcd(onTemp, offTemp); break; //default: } } if (lcdUpdateTimer + lcdUpdateInterval < millis()) { lcdUpdateTimer = millis(); updateLcd(onTemp, offTemp); sendPlotData("onTemp", onTemp)); sendPlotData("offTemp", offTemp); sendPlotData("setPoint", pidSetpoint); sendPlotData("Control[%]", pidWindowOutput/pidWindowSize*100); } } float getTemp(int analogChannel) { float a = analogRead(analogChannel); float voltage = a / 1024 * 5.0; float resistance = (10000 * (5.0-voltage)) / voltage; float logcubed = log(resistance); logcubed = logcubed * logcubed * logcubed; float kelvin = 1.0 / (8.19e-4 + 2.07e-4 * log(resistance) + 9.74e-8 * (logcubed)); float celcius = (kelvin - 273.15); return celcius; } void setRelayState(int Pin) { if(millis() - pidWindowStartTime > pidWindowSize) { //time to shift the Relay Window pidWindowStartTime += pidWindowSize; pidWindowOutput = pidOutput; } if(pidWindowOutput > millis() - pidWindowStartTime) { digitalWrite(Pin,HIGH); } else { digitalWrite(Pin,LOW); } } int getButtonId (int analogValue) { int buttonPressed; delay(60); //debounce int difference = (analogRead(0) - analogValue); if (abs(difference) > 5) { buttonPressed = buttonNONE; //two readings either side of debounce were different } else { if (analogValue < 50) buttonPressed = buttonRIGHT; if (analogValue > 50 && analogValue < 195) buttonPressed = buttonUP; if (analogValue > 195 && analogValue < 380) buttonPressed = buttonDOWN; if (analogValue > 380 && analogValue < 555) buttonPressed = buttonLEFT; if (analogValue > 555 && analogValue < 790) buttonPressed = buttonSELECT; if (analogValue > 1000) buttonPressed = buttonNONE; } return buttonPressed; } void updateLcd (float onTemp, float offTemp) { // line 1 lcd.setCursor(0,0); lcd.print("SP:"); lcd.print(pidSetpoint, 0); lcd.print(" On:"); lcd.print(onTemp, 1); // line 2 lcd.setCursor(0,1); lcd.print("Off:"); lcd.print(offTemp, 1); lcd.print(" "); lcd.print(pidWindowOutput/pidWindowSize*100, 1); lcd.print("%"); } void sendPlotData(String seriesName, float data) { Serial.print("{"); Serial.print(seriesName); Serial.print(",T,"); Serial.print(data); Serial.println("}"); } |
Auto-tune code
The Malt Kiln uses proportional-integral control to maintain the air temperature being blown through the grain. The coefficients for proportional and integral control needed depend on many factors such as the geometry of the kiln and the air flow rate through the system. I used Brett Beauregard’s Autotune library to select these parameters automatically. It probes the kilns behaviour to step changes in the duty cycle of the hot-air gun element. The autotune is run once when the kiln is commissioned and the parameters it chooses are entered into the program that drives the kiln.
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
#include <pid_v1.h> #include <pid_autotune_v0.h> // digital pin connections #define heaterPin 3 #define fanPin 2 // analog pin connections #define onTempPin 1 #define offTempPin 2 byte ATuneModeRemember=2; double input=20, output=2500, setpoint=50; double kp=5000,ki=2,kd=0; double aTuneStep=500, aTuneNoise=1, aTuneStartValue=2500; unsigned int aTuneLookBack=20; boolean tuning = false; unsigned long modelTime, serialTime; PID myPID(&input, &output, &setpoint,kp,ki,kd, DIRECT); PID_ATune aTune(&input, &output); int pidWindowSize = 5000; unsigned long pidWindowStartTime; double pidWindowOutput; void setup() { pinMode(fanPin, OUTPUT); pinMode(heaterPin,OUTPUT); digitalWrite(fanPin, HIGH); //Setup the pid myPID.SetMode(AUTOMATIC); myPID.SetOutputLimits(0, pidWindowSize); pidWindowStartTime = millis(); if(tuning) { tuning=false; changeAutoTune(); tuning=true; } serialTime = 0; Serial.begin(9600); } void loop() { unsigned long now = millis(); input = getTemp(onTempPin); if(tuning) { byte val = (aTune.Runtime()); if (val!=0) { tuning = false; } if(!tuning) { //we're done, set the tuning parameters kp = aTune.GetKp(); ki = aTune.GetKi(); kd = aTune.GetKd(); myPID.SetTunings(kp,ki,kd); AutoTuneHelper(false); } } else myPID.Compute(); setRelayState(heaterPin); //send-receive with processing if it's time if(millis() > serialTime) { SerialReceive(); SerialSend(); serialTime+=500; } } void changeAutoTune() { if(!tuning) { //Set the output to the desired starting frequency. output=aTuneStartValue; aTune.SetNoiseBand(aTuneNoise); aTune.SetOutputStep(aTuneStep); aTune.SetLookbackSec((int)aTuneLookBack); AutoTuneHelper(true); tuning = true; } else { //cancel autotune aTune.Cancel(); tuning = false; AutoTuneHelper(false); } } void AutoTuneHelper(boolean start) { if(start) ATuneModeRemember = myPID.GetMode(); else myPID.SetMode(ATuneModeRemember); } void SerialSend() { Serial.print("setpoint: ");Serial.print(setpoint); Serial.print(" "); Serial.print("input: ");Serial.print(input); Serial.print(" "); Serial.print("output: ");Serial.print(output); Serial.print(" "); if(tuning){ Serial.println("tuning mode"); } else { Serial.print("kp: ");Serial.print(myPID.GetKp());Serial.print(" "); Serial.print("ki: ");Serial.print(myPID.GetKi());Serial.print(" "); Serial.print("kd: ");Serial.print(myPID.GetKd());Serial.println(); } } void SerialReceive() { if(Serial.available()) { char b = Serial.read(); Serial.flush(); if((b=='1' && !tuning) || (b!='1' && tuning))changeAutoTune(); } } float getTemp(int analogChannel) { float a = analogRead(analogChannel); float voltage = a / 1024 * 5.0; float resistance = (10000 * (5.0-voltage)) / voltage; float logcubed = log(resistance); logcubed = logcubed * logcubed * logcubed; float kelvin = 1.0 / (8.19e-4 + 2.07e-4 * log(resistance) + 9.74e-8 * (logcubed)); float celcius = (kelvin - 273.15); return celcius; } void setRelayState(int Pin) { if(millis() - pidWindowStartTime > pidWindowSize) { //time to shift the Relay Window pidWindowStartTime += pidWindowSize; pidWindowOutput = output; } if(pidWindowOutput > millis() - pidWindowStartTime) { digitalWrite(Pin,HIGH); } else { digitalWrite(Pin,LOW); } } |
Downloads
MegunoLink was used to plot temperature data for this project. Grab MegunoLink Pro here.