
Kitchen Timer (IoT)
Kitchen Timer (IoT)
Introduction
This project is the project used during the Embedded System Design class to show one way of addressing the design, building and testing of an embedded system, from proof of concept, to fist prototype and final iteration boards.
As a project timeline guideline, we will be following the schedule below [Gantt Chart from βMaking Embedded Systemsβ reference book].

The project is divide into 3 Phases:
- Phase A: Proof of Concept,
- Phase B: Prototype
- Phase C: Final Iteration
Phase A: Proof of Concept
Project Requirements
This stage is normally done in conjunction with a sponsor/company interested in the product. Since there is no company associated with this project, the requirements are going to be a bit relaxed and students can actually add more features if they want to.
Hardware requirements:
- x1 buzzer
- x2 user buttons
- x2 status LEDs
- x1 7-Segment display
Software requirements:
- Turn on/off the timer
- Increment the timer
Additional feature:
- If possible, include some IoT capability. For example, the ability to interact with the timer over internet, phone or computer
System Design
It is always good to start with a draft of a system block diagram. In this stage, some of the project requirements get polished and it is a good time to make some design decisions too. I will give some design example decision below.

If possible, you can polish and clean up your block diagram so it can be included on a report later.

Buttons
Normally buttons have jitter and some sort of debounce mechanics should be included in your design. These debounce mechanisms can be done in software or hardware [REF 1][REF 5]. The decision here depends on some factors but for this design we will be debouncing the buttons in software to save some hardware components and PCB space.
7-Seg Display
The way to interface with the user is an important step during the design stage. The decision on what type of screen or display is also correlated with the complexity of the code required to render the information to the user.
For simplicity, we are going to use a 7-seg display and we are also going to use a shift register to interface between the microprocessor and the display in order to reduce the number of pins required to operate the 7-seg display.
Components Selection
For this stage, the goal is to develop a quick prototype where it is possible to test the software and hardware solutions from the precious sections. For that, we will be developing an Arduino shield that can be used with an Arduino Uno.
There is a chance that the Arduino Uno might not be the microprocessor used in the next phases but it is sufficient for a quick rapid prototype and show the components interacting with some initial code.
Components:
- x1 Arduino Uno
- x1 7-Seg Display
- x1 Shift Register 8-Bit (SN74HC595)
- x2 Buttons
- x2 LEDs
- Resistors and Capacitors
- x1 Buzzer
Build Prototype
This stage might not be possible to do for some projects, but if possible, it is always good to test the components first before building the PCB.

However, it is a good time to design a preliminary schematic of your system with the desired pinouts connections.

Initial Code Development
The software team can also start developing some code to interface the components. This is the opportunity:
- to understand the real-time constraints of the application.
- to see if it is required additional hardware to interface the components to alleviate the code.
- to see how many inputs/outputs, and microprocessor resources (e.g.: SPI, I2C, UART, ADC, DAC, etc) will be required.
Since this is a proof of concept, it is ok if the selected microprocessor can’t do all the required code at once. But it should at least be able to interface properly with all desired components of the system, even if you need to test them individually.

PCB Design
Assemble Stage
Software Development
With the shield fabricated it is now time to try to develop the code of this proof of concept board to it’s maximum potential.
The code presented in the “Initial Code Development” section had some real-time limitations. The functions to interact with the buttons, leds, and 7-seg display were running in a pooling fashion way.
The pooling method is not the best approach for this particular case because we are facing a concurrency problem. There are different ways to address this challenge. The slides below go over different approaches to mitigate this issue.
Different Types of Program Flows
Below you can find two solutions: solution 1 using interrupt routines, and solution 2 using FreeRTOS.
Solution 1: Final Kitchen Timer Code (with interrupts)
Code block diagrams can help visualizing how the code is interacting with peripherals, communication between interrupt routines, and main loop.

As you get closer to the final version of the code, clean up those block diagrams. The amount of information that you include in the block diagrams is up to you. You can also have multiple block diagrams depicting different system interactions.

Quick note, the code below is still missing a debounce code for the buttons.
#define BUTTON_2 2 // Inc Timer
#define BUTTON_1 3 // Start/Stop Timer and Stop Buzzer
#define GREEN_LED 4
#define RED_LED 5
#define BUZZER 6
#define DATA 9 // DS
#define LATCH 8 // ST_CP
#define CLOCK 7 // SH_CP
#define DIGIT_4 10
#define DIGIT_3 11
#define DIGIT_2 12
#define DIGIT_1 13
// 7-Seg Display Variables
unsigned char gtable[]=
{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c
,0x39,0x5e,0x79,0x71,0x00};
byte gCurrentDigit;
// Volatile Variables
volatile unsigned char gISRFlag1 = 0;
volatile unsigned char gBuzzerFlag = 0;
// Timer Variables
#define DEFAULT_COUNT 30 // default value is 30secs
volatile int gCount = DEFAULT_COUNT;
unsigned char gTimerRunning = 0;
unsigned int gReloadTimer1 = 62500; // corresponds to 1 second
byte gReloadTimer2 = 10; // display refresh time
/**
* @brief Setup peripherals and timers
* @param
* @return
*/
void setup() {
// LEDs Pins
pinMode(RED_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
// Button Pins
pinMode(BUTTON_1, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_1), buttonISR1, RISING);
pinMode(BUTTON_2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_2), buttonISR2, RISING);
// Buzer Pins
pinMode(BUZZER, OUTPUT);
// Buzzer -> Off
digitalWrite(BUZZER,LOW);
// 7-Seg Display
pinMode(DIGIT_1, OUTPUT);
pinMode(DIGIT_2, OUTPUT);
pinMode(DIGIT_3, OUTPUT);
pinMode(DIGIT_4, OUTPUT);
// Shift Register Pins
pinMode(LATCH, OUTPUT);
pinMode(CLOCK, OUTPUT);
pinMode(DATA, OUTPUT);
dispOff(); // turn off the display
// Initialize Timer1 (16bit) -> Used for clock
// Speed of Timer1 = 16MHz/256 = 62.5 KHz
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
OCR1A = gReloadTimer1; // compare match register 16MHz/256
TCCR1B |= (1<<WGM12); // CTC mode
// Start Timer by setting the prescaler -> done using the start button
//TCCR1B |= (1<<CS12); // 256 prescaler
TIMSK1 |= (1<<OCIE1A); // enable timer compare interrupt
interrupts();
// Initialize Timer2 (8bit) -> Used to refresh display
// Speed of Timer2 = 16MHz/1024 = 15.625 KHz
TCCR2A = 0;
TCCR2B = 0;
OCR2A = gReloadTimer2; // max value 2^8 - 1 = 255
TCCR2A |= (1<<WGM21);
TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20); // 1204 prescaler
TIMSK2 |= (1<<OCIE2A);
interrupts(); // enable all interrupts
}
/**
* @brief Shifts the bits through the shift register
* @param num
* @param dp
* @return
*/
void display(unsigned char num, unsigned char dp)
{
digitalWrite(LATCH, LOW);
shiftOut(DATA, CLOCK, MSBFIRST, gtable[num] | (dp<<7));
digitalWrite(LATCH, HIGH);
}
/**
* @brief Turns the 7-seg display off
* @param
* @return
*/
void dispOff()
{
digitalWrite(DIGIT_1, HIGH);
digitalWrite(DIGIT_2, HIGH);
digitalWrite(DIGIT_3, HIGH);
digitalWrite(DIGIT_4, HIGH);
}
/**
* @brief Button 2 ISR
* @param
* @return
*/
void buttonISR2()
{
// Increment Clock
gCount++;
}
/**
* @brief Button 1 ISR
* @param
* @return
*/
void buttonISR1()
{
// Set ISR Flag
gISRFlag1 = 1;
}
/**
* @brief Timer 2 ISR
* @param TIMER2_COMPA_vect
* @return
*/
ISR(TIMER2_COMPA_vect) // Timer2 interrupt service routine (ISR)
{
dispOff(); // turn off the display
switch (gCurrentDigit)
{
case 1: //0x:xx
display( int((gCount/60) / 10) % 6, 0 ); // prepare to display digit 1 (most left)
digitalWrite(DIGIT_1, LOW); // turn on digit 1
break;
case 2: //x0:xx
display( int(gCount / 60) % 10, 1 ); // prepare to display digit 2
digitalWrite(DIGIT_2, LOW); // turn on digit 2
break;
case 3: //xx:0x
display( (gCount / 10) % 6, 0 ); // prepare to display digit 3
digitalWrite(DIGIT_3, LOW); // turn on digit 3
break;
case 4: //xx:x0
display(gCount % 10, 0); // prepare to display digit 4 (most right)
digitalWrite(DIGIT_4, LOW); // turn on digit 4
break;
default:
break;
}
gCurrentDigit = (gCurrentDigit % 4) + 1;
}
/**
* @brief Timer 1 ISR
* @param TIMER1_COMPA_vect
* @return
*/
ISR(TIMER1_COMPA_vect) // Timer1 interrupt service routine (ISR)
{
gCount--;
if(gCount == 0)
{
// Stop Timer
stopTimer1();
// Raise Alarm
gBuzzerFlag = 1;
gTimerRunning = 0;
}
}
/**
* @brief Stop Timer 1
* @param
* @return
*/
void stopTimer1()
{
// Stop Timer
TCCR1B &= 0b11111000; // stop clock
TIMSK1 = 0; // cancel clock timer interrupt
}
/**
* @brief Start Timer 1
* @param
* @return
*/
void startTimer1()
{
// Start Timer
TCCR1B |= (1<<CS12); // 256 prescaler
TIMSK1 |= (1<<OCIE1A); // enable timer compare interrupt
}
/**
* @brief Turn On Buzzer
* @param
* @return
*/
void activeBuzzer()
{
unsigned char i;
unsigned char sleepTime = 1; // ms
for(i=0;i<100;i++)
{
digitalWrite(BUZZER,HIGH);
delay(sleepTime);//wait for 1ms
digitalWrite(BUZZER,LOW);
delay(sleepTime);//wait for 1ms
}
}
/**
* @brief Main Loop
* @param
* @return
*/
void loop()
{
// Attend Button 2 ISR
if(gISRFlag1 == 1)
{
// Reset ISR Flag
gISRFlag1 = 0;
if(gTimerRunning == 0)
{
// Start Timer
gTimerRunning = 1;
if(gCount == 0)
gCount = DEFAULT_COUNT;
if(gBuzzerFlag == 1)
{
gBuzzerFlag = 0;
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
else
{
startTimer1();
// LEDs -> Timer Running
digitalWrite(RED_LED, LOW);
digitalWrite(GREEN_LED, HIGH);
}
}
else
{
// Stop Timer
stopTimer1();
gTimerRunning = 0;
// LEDs -> Timer Running
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
}
// Attend gBuzzerFlag
if(gBuzzerFlag == 1)
{
// Make Noise...
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, LOW);
activeBuzzer();
}
}
Solution 2: Final Kitchen Timer Code (with FreeRTOS)
Luckily in this case, the code is not much different than solution 1. The code that was previously running inside the interrupt routines is now done inside tasks that are managed by the FreeRTOS scheduler.

Quick note, the version of the code below, includes a simple debounce mechanism for the buttons.
// Include Arduino FreeRTOS library
#include <Arduino_FreeRTOS.h>
#define BUTTON_2 2 // Inc Timer
#define BUTTON_1 3 // Start/Stop Timer and Stop Buzzer
#define GREEN_LED 4
#define RED_LED 5
#define BUZZER 6
#define DATA 9 // DS
#define LATCH 8 // ST_CP
#define CLOCK 7 // SH_CP
#define DIGIT_4 10
#define DIGIT_3 11
#define DIGIT_2 12
#define DIGIT_1 13
// 7-Seg Display Variables
unsigned char gTable[]=
{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c
,0x39,0x5e,0x79,0x71,0x00};
// Timer Variables
#define DEFAULT_COUNT 30 // default value is 30secs
int gCount = DEFAULT_COUNT;
char gBuzzerFlag = 0;
unsigned char gTimerRunning = 0;
//define task handles
TaskHandle_t TaskClockTimer_Handler;
void setup() {
/**
* Set up Pins
*/
// LEDs Pins
pinMode(RED_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
// Button Pins
pinMode(BUTTON_1, INPUT);
pinMode(BUTTON_2, INPUT);
// Buzer Pins
pinMode(BUZZER, OUTPUT);
// Buzzer -> Off
digitalWrite(BUZZER,LOW);
// 7-Seg Display
pinMode(DIGIT_1, OUTPUT);
pinMode(DIGIT_2, OUTPUT);
pinMode(DIGIT_3, OUTPUT);
pinMode(DIGIT_4, OUTPUT);
// Shift Register Pins
pinMode(LATCH, OUTPUT);
pinMode(CLOCK, OUTPUT);
pinMode(DATA, OUTPUT);
dispOff(); // turn off the display
// 7-Seg Display Task -> Running time 5 ms
xTaskCreate(TaskDisplay, // Task function
"Display", // A name just for humans
128, // This stack size can be checked & adjusted by reading the Stack Highwater
NULL,
0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
NULL);
// Clock timer Task -> Running time 1 second
xTaskCreate(TaskClockTimer, // Task function
"ClockTimer", // Task name
128, // Stack size
NULL,
0, // Priority
&TaskClockTimer_Handler);
stopTaskClockTimer();
// Buzzer Task -> Running at 250 ms
xTaskCreate(TaskBuzzer, // Task function
"Buzzer", // Task name
128, // Stack size
NULL,
0, // Priority
NULL );
// Read Button 2 Task -> Running at 10 ms
xTaskCreate(TaskReadButton2, // Task function
"ReadButton2", // Task name
128, // Stack size
NULL,
0, // Priority
NULL);
// Read Button 1 Task -> Running at 10 ms
xTaskCreate(TaskReadButton1, // Task function
"ReadButton1", // Task name
128, // Stack size
NULL,
0, // Priority
NULL);
}
void loop() {}
// ------
// TASKS
// ------
/**
* 7-Seg Display Task
* Refresh display -> Running at 5 ms
*/
void TaskDisplay(void * pvParameters) {
(void) pvParameters;
byte current_digit;
for (;;)
{
dispOff(); // turn off the display
switch (current_digit)
{
case 1: //0x:xx
display( int((gCount/60) / 10) % 6, 0 ); // prepare to display digit 1 (most left)
digitalWrite(DIGIT_1, LOW); // turn on digit 1
break;
case 2: //x0:xx
display( int(gCount / 60) % 10, 1 ); // prepare to display digit 2
digitalWrite(DIGIT_2, LOW); // turn on digit 2
break;
case 3: //xx:0x
display( (gCount / 10) % 6, 0 ); // prepare to display digit 3
digitalWrite(DIGIT_3, LOW); // turn on digit 3
break;
case 4: //xx:x0
display(gCount % 10, 0); // prepare to display digit 4 (most right)
digitalWrite(DIGIT_4, LOW); // turn on digit 4
}
current_digit = (current_digit % 4) + 1;
// 5 ms
vTaskDelay( 5 / portTICK_PERIOD_MS );
}
}
/**
* Clock Timer Task
* If timer is on, decrements timer by one second.
*/
void TaskClockTimer(void *pvParameters)
{
(void) pvParameters;
for (;;)
{
gCount--;
if(gCount == 0)
{
//raise alarm
gBuzzerFlag = 1;
gTimerRunning = 0;
//stop timer
stopTaskClockTimer();
}
// 1 second
vTaskDelay( 1000 / portTICK_PERIOD_MS );
}
}
/*
* Buzzer Task
*/
void TaskBuzzer(void *pvParameters)
{
(void) pvParameters;
unsigned char state = 0;
for (;;)
{
// Attend gBuzzerFlag
if(gBuzzerFlag == 1)
{
// Make Noise...
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, LOW);
digitalWrite(BUZZER, HIGH);
vTaskDelay( 250 / portTICK_PERIOD_MS );
digitalWrite(BUZZER, LOW);
vTaskDelay( 250 / portTICK_PERIOD_MS );
}
else
digitalWrite(BUZZER, LOW);
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
/**
* Read Button 2 Task
* Reads button 2, Inc timer
*/
void TaskReadButton2(void *pvParameters)
{
(void) pvParameters;
unsigned int buttonState = 0; // variable for reading the pushbutton status
for (;;)
{
// Read Button 2 -> pooling method is fine in this case
buttonState = digitalRead(BUTTON_2);
// TODO -> implement the code to debounce the button
if(buttonState == 1)
{
vTaskDelay( 250 / portTICK_PERIOD_MS ); // Very simple debounce
gCount++;
}
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
/**
* Read Button 1 Task
* Reads button 1, Start/Stop timer
*/
void TaskReadButton1(void *pvParameters)
{
(void) pvParameters;
unsigned int buttonState = 0; // variable for reading the pushbutton status
for (;;)
{
// Read Button 1 -> pooling method is fine in this case
buttonState = digitalRead(BUTTON_1);
if(buttonState == 1)
{
vTaskDelay( 250 / portTICK_PERIOD_MS ); // Very simple debounce
if(gTimerRunning == 0)
{
// Start Clock Timer
gTimerRunning = 1;
if(gCount == 0)
{
gCount = DEFAULT_COUNT;
}
if(gBuzzerFlag == 1)
{
gBuzzerFlag = 0;
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
else
{
startTaskClockTimer();
// LEDs -> Timer Running
digitalWrite(RED_LED, LOW);
digitalWrite(GREEN_LED, HIGH);
}
}
else
{
// Stop Timer
stopTaskClockTimer();
gTimerRunning = 0;
// LEDs -> Timer Running
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
}
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
// --------------------------------------------------
void stopTaskClockTimer()
{
vTaskSuspend(TaskClockTimer_Handler);
}
void startTaskClockTimer()
{
vTaskResume(TaskClockTimer_Handler);
}
void dispOff()
{
digitalWrite(DIGIT_1, HIGH);
digitalWrite(DIGIT_2, HIGH);
digitalWrite(DIGIT_3, HIGH);
digitalWrite(DIGIT_4, HIGH);
}
void display(unsigned char num, unsigned char dp)
{
digitalWrite(LATCH, LOW);
shiftOut(DATA, CLOCK, MSBFIRST, gTable[num] | (dp<<7));
digitalWrite(LATCH, HIGH);
}
Phase B: Prototype
The goal with this phase is to move the proof of concept prototype to a prototype that shapes the board closer to the final idea of the product.
Project Requirements
In this stage, we eliminate components from the development board that are not needed (e.g.: Arduino Uno board), and add additional components that can potential enhance the product (e.g.: communication module added -> ESP8266).
Note: You should only add components/modules if from the software side there is room to add the code required to enable these features.
The project requirements should also settle at this stage, or at least they shouldn’t change that much from this point on.
The only thing that I will be adding to this project is the ability to communicate externally with the clock.
Hardware Requirements:
- Substitute the Arduino Uno board with a microprocessor that has similar CPU specs but more available pinouts.
Communication Requirements:
- The ability to control the device remotely (IoT).
System Design
With the new requirements in place, we redesign the block diagram to include the new microprocessor, communication module and additional support modules.

Microprocessor
The microprocessor used in this stage is the ATMEGA32U4. It is the same microprocessor used in the Arduino Leonardo boards. The ISP Programmer, Crystal, and USB modules are added to support the uP module.
A comparison between the Leonardo and Uno board can be found here. The ATMEGA32U4 has additional pins which allows us to include the communication module without affecting the rest of the design.
Communications
For the communications modules, I will be using the ESP8266. I will be using this module in a format that it is easier to integrate (Link) with the PCB board. Since this module works at 3.3V, we need a voltage regulator to drop the voltage down to the required voltage.
Build Prototype
This stage might not be possible to do for some projects, but if possible, it is always good to test the components first before building the PCB.

In this case, I was able to test out the ATMEGA32U4 that will be included on the PCB board. The rest of the modules are the same as the ones used during Phase A.


Assemble Stage
Version 2.0
This version had some design mistakes that were fixed in version 3.0.
Design Mistakes:
- The pull down resistors for the buttons were connected to the wrong pins.
- The ESP8266 module had the wrong pinout direction.
The boards were manufactured by PCBWay.
Note: the PCB’s were manufactured properly. The mistakes were done at PCB layout stage before sending the boards to PCBWay.


Assembled in house.

The boards were manufactured by PCBWay.
Version 3.0


Assemble in progress…
Software Development
The first two solutions below have the same code from Phase A adapted to the new microprocessor (ATMEGA32U4). I did this as an intermediate step to make sure that the code is running properly before integrating the ESP8266 module with the system.
Solution 1: with interrupts (Phase A code adpatation)
Code block diagrams can help visualizing how the code is interacting with peripherals, communication between interrupt routines, and main loop.

The main difference from Phase A code is the use of timer 3 instead of timer 2. The Arduino Uno board (which has an ATmega328P) has Timer0, Timer1 and Timer2 (check page 49 of the datasheet). The ATMEGA32U4 chipset has Timer0, Timer1, Timer3 and Timer4 (check page 63 of the datasheet).
Quick note, the code below is still missing a debounce code for the buttons.
#include <Arduino.h>
#include <avr/power.h>
#define PF0 A5
#define PF1 A4
#define PF4 A3
#define PF5 A2
#define PF6 A1
#define PF7 A0
#define DIGIT_1 13 // PC7
#define RED_LED 5 // PC6
#define DIGIT_4 10 // PB6
#define DATA 9 // PB5 -> DS
#define LATCH 8 // PB4 -> ST_CP
#define BUZZER 6 // PD7
#define DIGIT_2 12 // PD6
#define GREEN_LED 4 // PD4
#define PD5 NotMappedPort_PD5 // Not Mapped on Leonardo Board
#define PD3 1 // TX
#define PD2 0 // RX
#define BUTTON_1 2 // PD1 -> Start/Stop Timer and Stop Buzzer
#define BUTTON_2 3 // PD0 -> Inc Timer
#define DIGIT_3 11 // PB7
#define PB3 NotMappedPort_PB3 // MISO -> Not Mapped on Leonardo Board
#define PB2 NotMappedPort_PB2 // MOSI -> Not Mapped on Leonardo Board
#define PB1 NotMappedPort_PB1 // SCK -> Not Mapped on Leonardo Board
#define PB0 NotMappedPort_PB0 // SS -> Not Mapped on Leonardo Board
#define CLOCK 7 // PE6 -> SH_CP
// 7-Seg Display Variables
unsigned char gtable[]=
{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c
,0x39,0x5e,0x79,0x71,0x00};
byte gCurrentDigit;
// Volatile Variables
volatile unsigned char gISRFlag1 = 0;
volatile unsigned char gBuzzerFlag = 0;
// Timer Variables
#define DEFAULT_COUNT 30 // default value is 30secs
volatile int gCount = DEFAULT_COUNT;
unsigned char gTimerRunning = 0;
unsigned int gReloadTimer1 = 62499; // corresponds to 1 second
byte gReloadTimer2 = 200; // display refresh time
/**
* @brief Setup peripherals and timers
* @param
* @return
*/
void setup() {
if(F_CPU == 16000000) clock_prescale_set(clock_div_1);
// LEDs Pins
pinMode(RED_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
// Button Pins
pinMode(BUTTON_1, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_1), buttonISR1, RISING);
pinMode(BUTTON_2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_2), buttonISR2, RISING);
// Buzer Pins
pinMode(BUZZER, OUTPUT);
// Buzzer -> Off
digitalWrite(BUZZER,LOW);
// 7-Seg Display
pinMode(DIGIT_1, OUTPUT);
pinMode(DIGIT_2, OUTPUT);
pinMode(DIGIT_3, OUTPUT);
pinMode(DIGIT_4, OUTPUT);
// Shift Register Pins
pinMode(LATCH, OUTPUT);
pinMode(CLOCK, OUTPUT);
pinMode(DATA, OUTPUT);
dispOff(); // turn off the display
// Initialize Timer1 (16bit) -> Used for clock
// OCR1A = (F_CPU / (N * f_target)) - 1
// Speed of Timer1 = (16e6 / (256 * 1)) - 1 = 62499
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
OCR1A = gReloadTimer1; // compare match register 16MHz/256
TCCR1B |= (1<<WGM12); // CTC mode
// Start Timer by setting the prescaler -> done using the start button
//TCCR1B |= (1<<CS12); // 256 prescaler
TIMSK1 |= (1<<OCIE1A); // enable timer compare interrupt
interrupts();
// Initialize Timer3 (16bit) -> Used to refresh display
// Speed of Timer3 = 16MHz/64 = 250 KHz
TCCR3A = 0;
TCCR3B = 0;
OCR3A = gReloadTimer2; // max value 2^16 - 1 = 65535
TCCR3A |= (1<<WGM31);
TCCR3B = (1<<CS31) | (1<<CS30); // 64 prescaler
TIMSK3 |= (1<<OCIE3A);
interrupts(); // enable all interrupts
}
/**
* @brief Shifts the bits through the shift register
* @param num
* @param dp
* @return
*/
void display(unsigned char num, unsigned char dp)
{
digitalWrite(LATCH, LOW);
shiftOut(DATA, CLOCK, MSBFIRST, gtable[num] | (dp<<7));
digitalWrite(LATCH, HIGH);
}
/**
* @brief Turns the 7-seg display off
* @param
* @return
*/
void dispOff()
{
digitalWrite(DIGIT_1, HIGH);
digitalWrite(DIGIT_2, HIGH);
digitalWrite(DIGIT_3, HIGH);
digitalWrite(DIGIT_4, HIGH);
}
/**
* @brief Button 2 ISR
* @param
* @return
*/
void buttonISR2()
{
// Increment Clock
gCount++;
}
/**
* @brief Button 1 ISR
* @param
* @return
*/
void buttonISR1()
{
// Set ISR Flag
gISRFlag1 = 1;
}
/**
* @brief Timer 2 ISR
* @param TIMER2_COMPA_vect
* @return
*/
ISR(TIMER3_COMPA_vect) // Timer2 interrupt service routine (ISR)
{
dispOff(); // turn off the display
switch (gCurrentDigit)
{
case 1: //0x:xx
display( int((gCount/60) / 10) % 6, 0 ); // prepare to display digit 1 (most left)
digitalWrite(DIGIT_1, LOW); // turn on digit 1
break;
case 2: //x0:xx
display( int(gCount / 60) % 10, 1 ); // prepare to display digit 2
digitalWrite(DIGIT_2, LOW); // turn on digit 2
break;
case 3: //xx:0x
display( (gCount / 10) % 6, 0 ); // prepare to display digit 3
digitalWrite(DIGIT_3, LOW); // turn on digit 3
break;
case 4: //xx:x0
display(gCount % 10, 0); // prepare to display digit 4 (most right)
digitalWrite(DIGIT_4, LOW); // turn on digit 4
break;
default:
break;
}
gCurrentDigit = (gCurrentDigit % 4) + 1;
}
/**
* @brief Timer 1 ISR
* @param TIMER1_COMPA_vect
* @return
*/
ISR(TIMER1_COMPA_vect) // Timer1 interrupt service routine (ISR)
{
gCount--;
if(gCount == 0)
{
// Stop Timer
stopTimer1();
// Raise Alarm
gBuzzerFlag = 1;
gTimerRunning = 0;
}
}
/**
* @brief Stop Timer 1
* @param
* @return
*/
void stopTimer1()
{
// Stop Timer
TCCR1B &= 0b11111000; // stop clock
TIMSK1 = 0; // cancel clock timer interrupt
}
/**
* @brief Start Timer 1
* @param
* @return
*/
void startTimer1()
{
// Start Timer
TCCR1B |= (1<<CS12); // 256 prescaler
TIMSK1 |= (1<<OCIE1A); // enable timer compare interrupt
}
/**
* @brief Turn On Buzzer
* @param
* @return
*/
void activeBuzzer()
{
unsigned char i;
unsigned char sleepTime = 1; // ms
for(i=0;i<100;i++)
{
digitalWrite(BUZZER,HIGH);
delay(sleepTime);//wait for 1ms
digitalWrite(BUZZER,LOW);
delay(sleepTime);//wait for 1ms
}
}
/**
* @brief Main Loop
* @param
* @return
*/
void loop()
{
// Attend Button 2 ISR
if(gISRFlag1 == 1)
{
// Reset ISR Flag
gISRFlag1 = 0;
if(gTimerRunning == 0)
{
// Start Timer
gTimerRunning = 1;
if(gCount == 0)
gCount = DEFAULT_COUNT;
if(gBuzzerFlag == 1)
{
gBuzzerFlag = 0;
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
else
{
startTimer1();
// LEDs -> Timer Running
digitalWrite(RED_LED, LOW);
digitalWrite(GREEN_LED, HIGH);
}
}
else
{
// Stop Timer
stopTimer1();
gTimerRunning = 0;
// LEDs -> Timer Running
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
}
// Attend gBuzzerFlag
if(gBuzzerFlag == 1)
{
// Make Noise...
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, LOW);
activeBuzzer();
}
}
Solution 2: with FreeRTOS (Phase A code adpatation)
The beautiful part of using FreeRTOS, is that the code is exactly the same as in Phase A. We don’t need to change anything because the HAL (Hardware Abstraction Layer) takes care of everything for us π

#include <Arduino.h>
#include <avr/power.h>
// Include Arduino FreeRTOS library
#include <Arduino_FreeRTOS.h>
#define PF0 A5
#define PF1 A4
#define PF4 A3
#define PF5 A2
#define PF6 A1
#define PF7 A0
#define DIGIT_1 13 // PC7
#define RED_LED 5 // PC6
#define DIGIT_4 10 // PB6
#define DATA 9 // PB5 -> DS
#define LATCH 8 // PB4 -> ST_CP
#define BUZZER 6 // PD7
#define DIGIT_2 12 // PD6
#define GREEN_LED 4 // PD4
#define PD5 NotMappedPort_PD5 // Not Mapped on Leonardo Board
#define PD3 1 // TX
#define PD2 0 // RX
#define BUTTON_1 2 // PD1 -> Start/Stop Timer and Stop Buzzer
#define BUTTON_2 3 // PD0 -> Inc Timer
#define DIGIT_3 11 // PB7
#define PB3 NotMappedPort_PB3 // MISO -> Not Mapped on Leonardo Board
#define PB2 NotMappedPort_PB2 // MOSI -> Not Mapped on Leonardo Board
#define PB1 NotMappedPort_PB1 // SCK -> Not Mapped on Leonardo Board
#define PB0 NotMappedPort_PB0 // SS -> Not Mapped on Leonardo Board
#define CLOCK 7 // PE6 -> SH_CP
// 7-Seg Display Variables
unsigned char gTable[]=
{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c
,0x39,0x5e,0x79,0x71,0x00};
// Timer Variables
#define DEFAULT_COUNT 30 // default value is 30secs
int gCount = DEFAULT_COUNT;
char gBuzzerFlag = 0;
unsigned char gTimerRunning = 0;
//define task handles
TaskHandle_t TaskClockTimer_Handler;
void setup() {
if(F_CPU == 16000000) clock_prescale_set(clock_div_1);
/**
* Set up Pins
*/
// LEDs Pins
pinMode(RED_LED, OUTPUT);
pinMode(GREEN_LED, OUTPUT);
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
// Button Pins
pinMode(BUTTON_1, INPUT);
pinMode(BUTTON_2, INPUT);
// Buzer Pins
pinMode(BUZZER, OUTPUT);
// Buzzer -> Off
digitalWrite(BUZZER,LOW);
// 7-Seg Display
pinMode(DIGIT_1, OUTPUT);
pinMode(DIGIT_2, OUTPUT);
pinMode(DIGIT_3, OUTPUT);
pinMode(DIGIT_4, OUTPUT);
// Shift Register Pins
pinMode(LATCH, OUTPUT);
pinMode(CLOCK, OUTPUT);
pinMode(DATA, OUTPUT);
dispOff(); // turn off the display
// 7-Seg Display Task -> Running time 5 ms
xTaskCreate(TaskDisplay, // Task function
"Display", // A name just for humans
128, // This stack size can be checked & adjusted by reading the Stack Highwater
NULL,
0, // Priority, with 3 (configMAX_PRIORITIES - 1) being the highest, and 0 being the lowest.
NULL);
// Clock timer Task -> Running time 1 second
xTaskCreate(TaskClockTimer, // Task function
"ClockTimer", // Task name
128, // Stack size
NULL,
0, // Priority
&TaskClockTimer_Handler);
stopTaskClockTimer();
// Buzzer Task -> Running at 250 ms
xTaskCreate(TaskBuzzer, // Task function
"Buzzer", // Task name
128, // Stack size
NULL,
0, // Priority
NULL );
// Read Button 2 Task -> Running at 10 ms
xTaskCreate(TaskReadButton2, // Task function
"ReadButton2", // Task name
128, // Stack size
NULL,
0, // Priority
NULL);
// Read Button 1 Task -> Running at 10 ms
xTaskCreate(TaskReadButton1, // Task function
"ReadButton1", // Task name
128, // Stack size
NULL,
0, // Priority
NULL);
}
void loop() {}
// ------
// TASKS
// ------
/**
* 7-Seg Display Task
* Refresh display -> Running at 5 ms
*/
void TaskDisplay(void * pvParameters) {
(void) pvParameters;
byte current_digit;
for (;;)
{
dispOff(); // turn off the display
switch (current_digit)
{
case 1: //0x:xx
display( int((gCount/60) / 10) % 6, 0 ); // prepare to display digit 1 (most left)
digitalWrite(DIGIT_1, LOW); // turn on digit 1
break;
case 2: //x0:xx
display( int(gCount / 60) % 10, 1 ); // prepare to display digit 2
digitalWrite(DIGIT_2, LOW); // turn on digit 2
break;
case 3: //xx:0x
display( (gCount / 10) % 6, 0 ); // prepare to display digit 3
digitalWrite(DIGIT_3, LOW); // turn on digit 3
break;
case 4: //xx:x0
display(gCount % 10, 0); // prepare to display digit 4 (most right)
digitalWrite(DIGIT_4, LOW); // turn on digit 4
}
current_digit = (current_digit % 4) + 1;
// 5 ms
vTaskDelay( 5 / portTICK_PERIOD_MS );
}
}
/**
* Clock Timer Task
* If timer is on, decrements timer by one second.
*/
void TaskClockTimer(void *pvParameters)
{
(void) pvParameters;
for (;;)
{
gCount--;
if(gCount == 0)
{
//raise alarm
gBuzzerFlag = 1;
gTimerRunning = 0;
//stop timer
stopTaskClockTimer();
}
// 1 second
vTaskDelay( 1000 / portTICK_PERIOD_MS );
}
}
/*
* Buzzer Task
*/
void TaskBuzzer(void *pvParameters)
{
(void) pvParameters;
unsigned char state = 0;
for (;;)
{
// Attend gBuzzerFlag
if(gBuzzerFlag == 1)
{
// Make Noise...
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, LOW);
digitalWrite(BUZZER, HIGH);
vTaskDelay( 250 / portTICK_PERIOD_MS );
digitalWrite(BUZZER, LOW);
vTaskDelay( 250 / portTICK_PERIOD_MS );
}
else
digitalWrite(BUZZER, LOW);
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
/**
* Read Button 2 Task
* Reads button 2, Inc timer
*/
void TaskReadButton2(void *pvParameters)
{
(void) pvParameters;
unsigned int buttonState = 0; // variable for reading the pushbutton status
for (;;)
{
// Read Button 2 -> pooling method is fine in this case
buttonState = digitalRead(BUTTON_2);
// TODO -> implement the code to debounce the button
if(buttonState == 1)
{
vTaskDelay( 250 / portTICK_PERIOD_MS ); // Very simple debounce
gCount++;
}
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
/**
* Read Button 1 Task
* Reads button 1, Start/Stop timer
*/
void TaskReadButton1(void *pvParameters)
{
(void) pvParameters;
unsigned int buttonState = 0; // variable for reading the pushbutton status
for (;;)
{
// Read Button 1 -> pooling method is fine in this case
buttonState = digitalRead(BUTTON_1);
if(buttonState == 1)
{
vTaskDelay( 250 / portTICK_PERIOD_MS ); // Very simple debounce
if(gTimerRunning == 0)
{
// Start Clock Timer
gTimerRunning = 1;
if(gCount == 0)
{
gCount = DEFAULT_COUNT;
}
if(gBuzzerFlag == 1)
{
gBuzzerFlag = 0;
// LEDs -> Timer Stopped
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
else
{
startTaskClockTimer();
// LEDs -> Timer Running
digitalWrite(RED_LED, LOW);
digitalWrite(GREEN_LED, HIGH);
}
}
else
{
// Stop Timer
stopTaskClockTimer();
gTimerRunning = 0;
// LEDs -> Timer Running
digitalWrite(RED_LED, HIGH);
digitalWrite(GREEN_LED, HIGH);
}
}
// One tick delay (15ms) in between reads for stability
vTaskDelay(1);
}
}
// --------------------------------------------------
void stopTaskClockTimer()
{
vTaskSuspend(TaskClockTimer_Handler);
}
void startTaskClockTimer()
{
vTaskResume(TaskClockTimer_Handler);
}
void dispOff()
{
digitalWrite(DIGIT_1, HIGH);
digitalWrite(DIGIT_2, HIGH);
digitalWrite(DIGIT_3, HIGH);
digitalWrite(DIGIT_4, HIGH);
}
void display(unsigned char num, unsigned char dp)
{
digitalWrite(LATCH, LOW);
shiftOut(DATA, CLOCK, MSBFIRST, gTable[num] | (dp<<7));
digitalWrite(LATCH, HIGH);
}
Solution 3: Final Kitchen Timer with ESP8266
Soon…
Enclosure Design
This section focus on the design and fabrication of an enclosure for the Kitchen Timer.
Note: normally this phase is done in conjunction with a marketing or product design team and it should be done before any PCB components routing. This is to make sure that output, input, and display connectors at the desired place.
Enclosure 3D Render
The material for this enclosure can be any material from plastic to wood. The render below is using wood texture for the enclosure and acrylic for the face plate.
Dimensions
3D Printed Model
Enclosure and face plate printed with a 3D printer.

Students Phase B Boards
soon…
Phase C: Final Iteration
If we want to add additional sensors/actuators (e.g.: IMU, vibration motor, etc) or add other code features to the Kitchen Timer, we probably need to move to a microprocessor that has more GPIOs available and potentially runs faster.
For this last Phase, we will be using an Arm Cortex-M4 32-bit core. We will be using the STM32F4 Discovery Board for the prototype stage and make sure that the code is ported accordingly from Phase B. After that we will add an additional sensor, the IMU MPU6050 and respective code.
STM32F4 Discovery Board Introduction
The STM32F4 Discovery Kit has a good portion of peripherals already available for the user to explore, namely, 3 axis accelerometer, 4 user LEDs, 1 user button, a 3.5mm audio jack, chip microphone and an USB OTG.

For this project, we will be using only the user LEDs and the rest of the peripherals will be disabled in software accordingly. However, the components are still connected physically to the pinouts.
While mapping the Kitchen Timer Shield components to this board, we will avoid using the pins that have external components attached, in order to avoid conflict while coding the STM32F4 later.
Pinout
The images below, are from the Espruino Website [REF 2]. The documentation on this website is stellar and very well organized. We can select any pin besides the ones in purple since they have a functionality on the board already. For this project, we should also avoid selecting the pins that are not 5v tolerant.

(Zoom)

System Design
As always, let’s start with a draft of the system block diagram. The blocks are pretty much the same as the blocks for Phase B. The pinouts were mapped to the STM32F4 Discovery Board peripherals (Link).
Note: I want to clarify that this design was not done in one go. In fact it took several iterations and it was adjusted at the same time as the code was being developed. What you see below, is the final iteration of the design.

If possible, we can polish and clean up the block diagram.
Note: you can select other pins, as long as you change the code accordingly later. I selected these ones because they made my life easier while routing the wires on the PCB board shield for this phase.

Component Selection
The new sensor for this phase is the MPU6050. I picked this sensor because it is one of the most common IMU modules used in Arduino projects and it is relatively affordable (~$7). The MPU6050 integrates 3-axis gyroscope 3-axis accelerometer, and a Digital Motion Processor (DMP). It can measure angular momentum or rotation along all 3 axis, static acceleration caused by gravity, and dynamic acceleration caused by motion, shock or vibration.
[REF 3] does a fantastic job introducing this device, going into detail on the micro-machined structure built on top of a silicon wafer to sense accelerations and angular rotations.

[REF 4] shows an acoustic injection study on the MPU6050 that is also interesting to read and learn more about the internals of this module.

We will get back to the sensor later during the Software Development section.
Build Prototype
This stage might not be possible to do for some projects, but if possible, it is always good to test the components first before building the PCB.

PCB Design
Assemble Stage
Software Development
Soon…
GitHub Repository
Phase A Code (Arduino Uno Compatible)
Phase B Code (Arduino Leonardo Compatible)
- BlinkLED – soon
- Clock code with Interrupts (no Comms) – soon
- Clock code with FreeRTOS (no Comms) – soon
Phase C Code (STM32F4 Discovery Board Compatible)
- Clock code (no IMU) – soon
- Clock code with IMU – soon
References
- [Ref 1] βSwitch Debouncing for Electronic Product Designsβ, Nuvation Engineering Website [Article]
- [Ref 2] βSTM32 F4 Discovery – Interactive Pinoutβ, Espruino Website [Article]
- [Ref 3] βInterface MPU6050 Accelerometer and Gyroscope Sensor with Arduinoβ, Last Minute Engineers Website [Article]
- [Ref 4] βInvestigation of Acoustic Injection on the MPU6050 Accelerometerβ, Sensors 2019 [Article]
- [Ref 5] βHow to Implement Hardware Debounce for Switches and Relays When Software Debounce Isnβt Appropriateβ, Digi-Key Website [Article]
Sponsor
