Although a significant amount of hardware / electronics has been created, it is useless without any software controlling it. When I started with the design of my system I wanted to have full flexibility. I could go for a microcontroller based design, with embedded software in it, so you would not need a PC. But I have various PCs hanging around in my house, so why not use one of them (in a network configuration) for controlling your home brewery? In this way, I have lots of flexibility. If I want some new behaviour built into my brewing program, I make an update, test it and distribute it to the PC in the brewery.
This particular setup has worked for years without any problem. The hardware was connected to the PC by means of a parallel port (or printer port). With the transition to a new hardware platform, this was upgraded to a (nowadays) more common USB connection. And then the set-up with the microcontroller becomes interesting again. Lots of old functionality, like the generation of the PWM signal, is easy to realize for any microcontroller. So the amount of hardware components can be reduced, as a result of this. On one side you have the PC-program that communicates through the USB connection with an Arduino Nano / a ATmega328P microcontroller. This microcontroller also needs software, which we call firmware, to distinguish it from the PC-software.
What shall we use as programming language for both systems. Well, for the firmware this is quite clear: the Arduino Nano is used, for which I use Atmel Studio and the C programming language. For embedded systems you almost never have a decent alternative, so this is the preferred set-up. For the PC-program it is less clear because of the large variety of programming languages available. I do not really like the new programming languages like Java and C#/.Net (I never ever used a MS programming language). I normally write my programs in the C programming language, but I definitely needed a nice front-end GUI (windows, menus, all the stuff). C in itself is not really suitable for that. The solution however was to use the C++ programming language, you can use your existing C libraries and also build a nice looking program. And if you use Borland's very very excellent Borland C++ Builder, programming becomes a lot of fun!
So the configuration is chosen, the programming language is set. What to do next? Well, I can tell a bit about the internal structure of my brewing program (which I called e-brew! by the way, which stands for electronic brewing). This page consists of the following paragraphs:
- 1. Overview / Software-Architecture
- 2. The Scheduler
- 3. The State Transition Diagram (STD)
- 4. The Graphical User Interface (GUI)
- 5. Firmware for the Arduino Nano / ATmega328
1. Overview / Software-Architecture
The PC-program contains a number of important components which together make up the entire system. Because the C++ language is object oriënted, it is normal to identify a number of objects. Next to this, there are also certain components written in the C programming language. But these C components can easily be called from within any C++ object. The main components for the PC-program are:
- A Scheduler with a number of tasks that need to be executed within fixed timing-intervals.
- State Transition Diagram (STD)"A state transition diagram is responsible for the overall control of the entire program.
- Graphical objects containing the various screens of the PC-program. Every screen has its own graphical object.
- Routines for the read- and write-operations from/to the COM-port. The COM port is not physically present, but emulated through the USB-port. This is called a 'virtual COM port'.
- A background process (ebrew_idle_handler()) that updates all of the graphical objects on the screen.
- A PID-controller function. For the implementation details, have a look at Implementation of a PID controller
The PC-program's main tasks are to allow the brewer to do all kinds of settings and adjustments needed for the brewing process (times, temperatures, parameters) and to send commands to the brewing-hardware on a periodic bas, using a standard communication protocol. The responses from the brewing-hardware are made visible to the various screens. The following screen-shot gives an overview of all project-files and screens used:
The total PC-application contains approximately 4400 lines of C++ code, of which the main-object (in Unit1.cpp) already contains 1600 Lines of Code (LOC).
Next to the PC-program there's a second important software system: the firmware for the ATmega328 on the Arduino Nano. This software-system also contains quite a lot of code: for revision R1.28 this was approximately 3300 lines of C-code. All-in-all: both systems together contain more than 7700 lines of code!
You can download a fully functional version of the PC-program here (.zip file, v1.83). In order to have this working correctly, you also need the hardware with the proper firmware.Back to the Top of the Page
The e-brew! application demands strict timing-requirements. A lot of things need to be done at fixed time-intervals. Commands have to be send to the brewing-hardware, temperatures and volumes need to be read in, the screens need to be updated, etcetera. It would be nice to have a real-time embedded operating system (called an RTOS), but Windows is far from being real-time or embedded. To execute something every 100 milliseconds is already quite demanding. Fortunately, temperatures are not rapidly changing, so controlling this does not have to be very fast. But something is needed to make sure that all tasks are executed in time. For this I have written a separate scheduler component. A scheduler is a piece of software that makes sure that all tasks in a program are executed at the right time. The scheduler used here is non pre-emptive, which means that after a task has been started, it is not interrupted any more by another task. The next task can only start when this task has finished executing. This is sufficient for our brewing system. You just need to have a bit of attention for the duration of every tasks.
Tasks are easy to add with this scheduler component. To illustrate this, an excerpt of a bit of C code from the PC-application is given here:
//----------------------------------------- // Now add all the tasks for the scheduler //----------------------------------------- add_task(task_alive_led , "alive_led" , 0, 500); /* 0, 500, 1000, 1500 */ add_task(task_read_temps , "read_temps" , 100, 2000); add_task(task_pid_ctrl , "pid_control" , 300, (uint16_t)(pid_pars_hlt.ts * 1000)); add_task(task_update_std , "update_std" , 400, 1000); /* 400, 1400 */ add_task(task_hw_debug , "hw_debug" , 600, 2000); add_task(task_read_flows , "read_flows" ,1100, 2000); add_task(task_log_file , "wr_log_file" ,1600, 5000); add_task(task_write_pars , "write_pars" ,1700, 5000);
Adding tasks is simple and convenient. The task task_update_std() for example executes the state transition diagram function every second (1000 msec.). You can also add an offset: task_update_std() is executed at time stamp 0.40 (400 msec.) and then at timestamps 1.40, 2.40, 3.40 etcetera. The PID controller, task_pid_ctrl(), is controlled in another way: the time-interval between two calls to this function is dependent of the parameter Ts, which is given in seconds. If this parameter is set in the PC-program to 20 seconds, this call here makes sure that the PID controller is executed every 20 seconds. As mentioned before, it is important to make sure that all of these tasks finish in time and do not take too much executing time, since this will delay other tasks. The scheduler is able to measure the time-duration and the maximum time-duration of every task. This information can be used for debugging purposes and looks like (result is from a log-file):
Task-Name T(ms) Stat T(ms) M(ms)
alive_led 500 0x02 0.4 0.5 read_temps 2000 0x02 51.3 51.6 pid_control 20000 0x02 0.0 0.0 update_std 1000 0x02 1.2 1.3 hw_debug 2000 0x03 0.0 0.0 read_flows 2000 0x02 49.9 49.9 wr_log_file 5000 0x02 0.9 1.0 write_pars 5000 0x02 0.0 0.0
From this, it appears that task_update_std() had a maximum time-duration of 1.3 milliseconds. Together with the code excerpt before, it gives a good insight in the behaviour of the various tasks. Debugging timing-issues is relatively easy to do. The scheduler itself is called from within an OnTimer() function of a Timer object. This is done every 100 milliseconds. The main function of this function is to update all task-timers and to administrate if a task needs to be enabled of disabled. The execution of a task is not done here, only the administration!Back to the Top of the Page
3. State Transition Diagram (STD)
According to the free on-line dictionary of Computing, a state transition diagram (STD) is a "A diagram consisting of circles to represent states and directed line segments to represent transitions between the states. One or more actions (outputs) may be associated with each transition.". In terms of my brewing program, a STD is a central control function, that controls the various phases of the brewing process. A state can be a 'mash rest' phase or a 'mash preheat' phase. A transition from one state to another state can be made if a certain condition is TRUE (e.g. the 'temperature is OK'). Conditions in the STD below are given in RED. When a transition is made to another state, a particular action can be programmed (e.g. set pump on). Actions are given in GREEN. Summarized: there are states, transitions between states, conditions and actions. These need to be defined for the brewing program and is give here:
The total STD of the PC-program has become quite complex. A short explanation for every state is given here:
- 00. Initialization: this state is entered after the brewing program is started. It remains in this state until the brewer enables the PID controller. In this state, all valves are closed and the pump is off.
- 01. Wait for HLT temp.: the brewer has enabled the PID controller and water in the HLT is heated. The program remains in this state as long as the required temperature has not been reached. If the option "Add Malt First" is checked, you can add the malt to the MLT in this state.
- 14. Prime Pump: The magnetic pump is a non self-priming pump and needs to be filled with water in order to be able to operate. Filling the pump is done in this state: water runs from the HLT into the pump for 1 minute.
- 02. Fill MLT: The HLT temperature is OK and the pump is switched on. The MLT is now filled with the pre-set amount of water.
- 03. Wait for MLT Temp.: Remain in this state as long as the MLT temperature has not reached the required dough-in temperature.
- 15. Add Malt to MLT: This is a manual action for the brewer: the milled grains are added to the water in the MLT, which has now reached the proper temperature. When adding the grains is finished, the brewer can indicate this to the PC-program. This state is executed only once and only when the option "Add Malt First" is unchecked.
- 18. Mash Rest 5 Min.: Give the milled grains a bit of time to soak water up. Starting the pump directly might clock up the filter bed.
- 04. Mash timer running: the mash rest timer is started, but has not reached a time-out yet. The program remains in this state as long as the timer is running. If all temperature-time pairs have been executed, a transition is made to the sparging phase (05. Sparge Timer Running). This state controls, together with the states 03. Wait for MLT Temp. and 13. Mash preheat HLT the entire temperature-time trajectory of the mashing process. These states are activated multiple times (dependent of the number of temperatures set) in sequential order.
- 13. Mash preheat HLT: the entire mash rest is not finished yet, but the HLT is already preheated to the next temperature (the pump is switched off now to prevent that the MLT is heated as well). When a time-out occurs from the mash rest timer, a transition is made to 03. Mash in progress and the mash-index is increased by 1.
- 05. Sparge Timer Running: this state is activated when all temperature-time pairs have been handled. The mashing phase is now finished and sparging is started. Upon entering this state for the first time (coming from state 04. Mash timer running), a transition is made directly to state 06. Pump from MLT to Boil-Kettle. This pumps a part of the wort to the boil kettle. In all other cases this state remains active for a pre-set time (typically 15-20 minutes). If time-out occurs, new fresh water is added from the HLT. A transition to state 09. Empty MLT is made when the number of sparging sessions is equal to the pre-set number of sparging sessions (typically 4-5 times). This means that we are done with the sparging process.
- 06. Pump from MLT to Boil. A part of the wort is pumped from the MLT to the boil kettle. When the proper amount of litres has been transferred, a transition is made to state 07. Delay_xSec.
- 07. Delay_xSec., this state remains active for only 10 seconds. Its purpose is to let the system rest a little.
- 08. Pump from HLT to MLT. In this state, fresh water of 80 °C from the HLT is pumped to the MLT. When the proper amount of litres has been transferred, a transition is made to state 05. Sparging Rest and the sparging index (sp_idx) is increased by 1. Cycling through the states 05 -> 06 -> 07 -> 08 is repeated a couple of times and this is made adjustable in the PC-program.
- 09. Empty MLT. If sparging is completed, this state is activated. All wort from the MLT is now pumped into the boil kettle.
- 10. Wait for Boil. All wort now sits inside the boil kettle, but it is not boiling yet. Only when it is actually boiling, a transition is made to the state 11. Now Boiling. Since there are no temperature sensors inside the boil kettle, the brewer needs to indicate manually to the PC-program that boiling has indeed started. After the brewer has done this, the transition to the state 11. Now Boiling is made.
- 11. Now Boiling. This state is activated when boiling has started, as indicated by the brewer. A boiling timer is started and as long as this timer has not timed-out, the PC-program remains in this state.
- 12. Boiling finished, prepare Chiller The boiling-timer has timed out and cooling the wort and pumping it into the fermentation bin can commence. When the counterflow chiller (CFC) is ready and cooling water is running through it, the brew can indicate this in the PC-program with another manual action. After this, a transition is made to the state 16. Chill & Pump to Fermentor.
- 16. Chill & Pump to Fermentor. The boiling-hot wort is pumped through the counterflow chiller and directly into the fermentation bin. After all wort has been transferred to this bin, the brewer can indicate this with a manual action in the PC-program. A transition is then made to state (17. Finished!).
- 17. Finished!. End of the brew day. This state is a dead-lock and does not contain any transitions to other states. If you want to start a new brewing session, you need to restart the entire PC-program.
The table (upper right corner) shows, for every state given, which valves are open (1) or closed (0) and whether or not the pump is on (1) or off (1). By designing such a table, the behaviour of the various valves becomes very defined. You always know exactly when a valve (or pump) is open or closed, given the state the STD is in. There are many more details to tell about this state transition diagram, but this will be sufficient (I hope) to give you an idea about the functionality. The STD is called once every second from the scheduler.Back to the Top of the Page
4. Graphical User Interface (GUI)
The scheduler with all the tasks and the State Transition Diagram are the heart and soul of the brewing program. But to make this flexible and easy to use when brewing, a nice front-end GUI is needed. Well, here is a screen-shot of the ebrew program:
There is a lot of information displayed on this screen, which is ideal when you are brewing and you need some specific information:
- Set-point and actual values of temperatures from HLT, MLT, Boil Kettle and output of Counter Flow Chiller. The picture shows an actual MLT temperature of 24.59 °C. The desired (setpoint) temperature for the MLT is 63 °C and for the HLT it is 36 °C (these are values taken during a debugging session, so the values are not real values used in a brewing session)
- PID controller switch: The PID controller can be enabled (light is GREEN) and disabled (light is RED) with this switch. When the PID controller is switched off, the heating output is always equal to 0 %.
- Current state: this label gives the actual state of the State Transition Diagram. Here it is "00. Initialization". By clicking with the right-mouse on this label, a popup-menu appears with which the brewer can enter a couple of manual commands, like 'malt is added'.
- Temperature of the electronics: this is the actual temperature from the cooling-plate of the power-electronics (the triac). The thermometer becomes red-coloured when the temperature is too high and the PID controller output then becomes 0 %. The current temperature here is 0.0 °C (sensor was not attached).
- Power Bar: this are the red bars with the text "HLT Power" and "Boil Power". It shows the actual output of the PID controllers. Here, they are both at 0 %.
- The actual volumes of the kettles: the screen shot indicates that the HLT volume is 197.1 L, 2.9 L for the MLT and 0.0 L for the boil kettle. Volumes are not read in directly here, but are calculated from the flow-sensor values. Everything flowing from MLT to boil-kettle increases the boil kettle volume and decreases the MLT volume.
- The flow-sensor values: there are three flow-sensors present in the system. The first sensor is mounted between the HLT and the MLT, the second sensor is mounted between the MLT and the boil-kettle and the third one is mounted at the output of the counter flow chiller. These values show the total amount of litres flown through them. In this case, the first sensor reads 2.9 L, the other two sensors read 0.0 L.
- The status of the pump: three options are possible: "Auto", "Pump Off (M)" and "Pump On (M)". These three options can be chosen by clicking on the pump with the right-mouse button. When the option "Auto" is selected, the brewing program (and the STD in particular) takes care of switching the pump on and off. The brewer can override the brewing program by selecting one of the two manual options (indicated with (M)).
- The status of every individual valve: again, three options are possible here: "Auto", "Off (M)" and "On (M)". The screen shot shows that all valves have in Auto mode (switched by the PC-program) and all are off.
- Red label in the lower-left corner (not shown): this shows debugging information for the PID controller. With these numbers, you can debug what the PID controller is doing.
- The status bar: the status bar shows some general information, like the actual revision number of the PC-program (R1.83) and from the connected brewing hardware (R1.28).
If one of the values, as read in from the brewing hardware, is not correct or when communication is not possible, the colour of these numbers change from GREEN to RED. The screen shot above shows that the MLT temperature and the three flow-sensor values are read in correctly.
4.1 The menu-bar of the ebrew program
The menu-bar of the ebrew program contains the following menu-items:
- File -> Read Log File...: in case there was a crash in either the hardware or the software, this function reads the log-file and restores the system to the last know status in the log-file.
- File -> Exit: exits the PC-program, all electronics are switched off and USB communication is closed.
- Edit -> Mash Scheme...: : you can enter up to 10 pairs of temperature versus time pairs.
- Edit -> Fix Parameters...: every relevant parameter can be given a value. Example: normally, the HLT temperature is read from the digital temperature sensor, but suppose that you want to set it (e.g. for testing purposes) to some other value, thereby overruling the value from the sensor. You can use this menu option to fix a parameter to a certain value (and release it again, so that the ebrew program will take over again).
- View -> Mash/Sparge Progress: this option shows a screen with the status of all mash and sparging timers, so you can see directly how long a particular rest will last.
- Help -> Contents...
- Help -> How to Use Help
- Help -> About...: shows version number of the ebrew program.
- Options -> I2C Hardware Settings...: various parameters that deal with the hardware can be adjusted here.
- Options -> PID Controller Settings...: various parameters that deal with the PID controller can be adjusted here.
- Options -> Sparge & STD Settings...: various parameters that deal with mashing / sparging / STD can be adjusted here.
- Options -> Measurements...: various parameters that deal with measurements of temperatures and volumes can be adjusted here.
5. Firmware voor de Arduino Nano / ATmega328
the firmware contains the C source-code for the ATmega328 microcontroller and has not much to do with the design of the PC-program described above. Both software systems do communicate with each other, using the virtual COM port function of the USB connection. A big advantage of the use of a microcontroller is that all hardware related and often time-critical tasks no longer have to be handled by the PC-program. A microcontroller is much better suited for this. At the PC side, we use the Arduino hardware boards. Prior to use, the Arduino Software Environment needs to be installed. This already installs all the drivers needed for the virtual COM port and the USB connection. At the microcontroller side, the story is a bit different. Fortunately we do not have to dig into USB communication (complex!), but we can use existing routines for serial communication. The microcontroller used (ATmega328P) contains a component called USART, which stands for Universal Synchronous/Asynchronous Receiver/Transmitter). As long as you have the proper USART library, it is relatively easy to start with serial communication. Settings used here are 38400,N,8,1 which means: 38400 Baud, no parity bit, 8 data-bits and 1 stop-bit. This is of course identical to the settings in the PC-program.
Back to the overall design of the microcontroller software. I do not use Arduino sketches and the Arduino Programming Environment, since it is too limited for what I need. From some sketches I made proper C libraries, others I wrote myself. As usual for a C project, for every component a C source-file with a header-file (.h) is made. Together this contains all the necessary routines for such a component. For example: the I2C module (with files i2c.c and i2c.h) contain all functions for I2C communication, including specific routines for the ICs used here, such as the PCA9544, the MCP23017 and the DS2482. The overall architectural design looks like the following:
Low-level routines: As you can see in the picture, there are libraries for I2C, SPI, ADC, PWM, One-Wire and USART. Basically for every low-level hardware component of the microcontroller. In order to really understand these libraries, you need to read the ATmega328P microcontroller data sheet. Especially if you would like to make adjustments. But, since the ATmega328P is a very popular microcontroller, there are many good working examples available.
Interrupt routines: The interrupt routines are time-critical pieces of code that cannot wait until the scheduler has time for them. It needs to be executed as soon as something happens. For the embedded design engineer it is important to keep these routines as small as possible: you just cannot start to wait until something happens in such a routine. A nice example of the use of such an interrupt routine are the flow-sensors. These are connected to two port pins of the microcontroller. As soon as a pulse comes in from a flow-sensor, an interrupt is generated by the microcontroller. The interrupt source routine does nothing more than to increase a software counter. Every flow-sensor has it's own unique interrupt routine. Besides these routines there is also a timer interrupt defined for the scheduler. This one is executed every millisecond. Finally there are two more interrupts defined for the serial communication. If the USART hardware from the microcontroller has received a byte, an interrupt is generated. The interrupt routine merely stores the byte read into a ring-buffer. Something similar happens when there is data in the ring-buffer that needs to be send to the PC-program.
Next to these low-level libraries, there is also a miscellaneous library. This library contain functions that deal with signal processing, such as filtering. It contains a moving-average filter (which is a low pass filter) and a slope-limiter function. Furthermore a ring-buffer for the serial communication and a RS232 command handler. Every command from the serial interface is processed by this function.
Ethernet/Wiz550io: the brewing-hardware has already been prepared for the addition of a WIZ550io Ethernet module. The C-libraries are derived from several Arduino sketches (which were a mess to read). With this addition it becomes possible for the brewing hardware to send/receive commands using the UDP protocol, instead of using the USB connection. This however, is not a standard part of the brewing system, but merely work-in-progress. Adding such a library impacts the resources of the microcontroller (Flash memory and RAM memory), so this should be done with care.
The scheduler from the PC-program is reused here as well. So the software is used on various platforms.Back to the Top of the Page