Kitchen Timer (IoT)

Kitchen Timer (IoT)

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

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

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.

Block Diagram Draft

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

Updated Block Diagram

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.

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

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.

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.

Schematics (Zoom)

Layout

3D Rendering

3D PCB Shield + Arduino (Fusion360)

The boards were manufactured by PCBWay.

Assembled in house.

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.

Block Diagram Draft

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);
}

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.

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).

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.

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.

Schematics (Zoom)

Layout (Zoom)

3D Rendering

3D PCB + ESP8266 (Fusion360)

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…

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.

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();
  }
}

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);
}

Soon…

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.

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.

Enclosure and face plate printed with a 3D printer.

Kitchen Timer PCB Board Version 2.0

soon…

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.

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)

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.

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.

Credit: Adam McCombs

[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.

Figure 5. (a) Schematic of the MPU6050 model; (b) geometric model of MPU6050-Y.

We will get back to the sensor later during the Software Development section.

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.

Schematics (Zoom)

Layout (Zoom)

3D Rendering

3D PCB + Kitchen Timer Shield, STM32F4 Discovery Board, MPU6050, ESP8266 (Fusion 360)

The boards were manufactured byΒ PCBWay.

Assembled in house.

Soon…

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

Sponsor

Leave a Reply

Your email address will not be published.