In the beginning God created the heaven and the earth. And human beings. And human beings created digital kitchen timers, like this one.

And human beings said, Cool but a little dismal. Let's make a better one!

And God said, I heard your call, let there be Arduinos: and there was Arduinos all over the earth, and that was good.

And human beings took an Arduino and created a better kitchen timer, like this one.

Hardware components:

Arduino UNO & Genuino UNO *1

Adafruit Standard LCD - 16x2 White on Blue *1

SparkFun Pushbutton switch 12mm *4

Resistor 10k ohm *4

Resistor 221 ohm *2

Single Turn Potentiometer- 100k ohms *1

Buzzer *!

Breadboard (generic) *1

Jumper wires (generic) *1



And God saw the new kitchen timer and said: I saw your new kitchen timer and it looks awful, but it seems too much fun! And that is good. :-)

What you need to have

Now that you know where all this came from, let's go deep into it.

All components I used came from the Arduino Starter Kit, including the small breadboard you see in the pics and in the video. Feel free to accomodate the project into a larger one, if you wish.

You'll need a power source too: while playing around, the PC' USB port and cable will be enough.

What you need to do

First: please gather all needed components from the Starter Kit or your preferred component bin; if you don't have one, don't panic. There's plenty of on the Internet. You can find the component list below.

And, well, you'll need the code too. It's in its box, below again.

How it works

Basically, like any other similar device you can buy for a buck at any store near you. But this is yours. And this will show you how those little gadgets actually work.

The keyword here is: current mode. The timer itself can run in only one out of four modes at a time:

IDLE - the timer is awaiting for your input, showing the currently set time amount; this is also the initial mode after power up or reset.

SETUP - you can enter this mode by long-pressing S4 (in the code this is called also "reset button"); here, by using S3 ("start stop button"), you can choose which value to change in order to set the elapsed time to be counted down later; finally, using S2 ("down button") and S1 ("up button") respectively, you can decrease or increase the choosen value (hours, minutes or seconds).

RUNNING - You can enter this mode by pressing S3, while leaving it will require both S3 or S4 (which will lead you to IDLE mode).

RINGING - When the desider amount of time is elapsed, this mode is automatically activated; you can leave it (i.e., make the little boy stop ringing) by pressing any switch.

The code

First, we need to include the proper libraries:

#include <LiquidCrystal.h>
#include <Time.h>

If you don't have them already, you'll need to download and install them:

Next, let's initialize that nice LCD module:

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

Please feel free to scramble the pins at your will in order to obtain a nice wiring layout: don't follow me in this, as I did a terrible wiring plan! :D For instance, you can reverse the latter four pins in the above statement in order to avoid the yellow wires crossing you can see in the schematic below (obviously, you'll have to adjust the button pin constants accordingly, see below). Play, have fun! The life with Arduinos starts right after that copy/paste!

The next 51 code lines contain the static variables declaration and initialization. Please feel free to browse them, their crystal-clear names and some scattered comments will guide you understanding the whole thing.

The setup() function carries out the usual preliminary steps you've seen gazillions of times in any Arduino sketch out there and so far. The only notable statement is the first, which will set the initial LCD display cursor's position. Because, yes: this module requires you to setup a position along its rows and cols and then to "print" something, which will appear starting from that position.

Now let's move to the loop() function.

First of all, let's discover the switch statuses. In order to achieve this, the following code block is used for nearly each of them:

  /*
   * Start/Stop button management
   */
  startStopButtonPressed = false;
  startStopButtonState = digitalRead(startStopButtonPin);
  if(startStopButtonState != startStopButtonPrevState)
  {
    startStopButtonPressed = startStopButtonState == HIGH;
    startStopButtonPrevState = startStopButtonState;
  }

A digitalRead is issued against the related pin and the result is compared to a previously readed value: if something has changed, the new value is stored for future reference and the bool "xxxButtonPressed" static variable is set to true if the button is pressed.

Looking at the circuit diagram below, you'll notice that each input pin is forced to LOW by a 10k resistor unless the corresponding switch is pressed and the pin itself is directly connected to +5V. A fairly classic scenario, uh?

Previously, I said "nearly each of them" because there's one button that acts in a different way than the others: S4. Its code block is capable of detecting the aforementioned long press in order to enter SETUP mode.

Next comes the mode management block switch: each case looks at the button state triggers ("xxxButtonPressed") and redirects the flow toward the proper new state, or performs the proper action.

    case MODE_IDLE:
      if(resetButtonPressed)
      {
        Reset();
      }
      if(resetButtonLongPressed)
      {
        currentMode = MODE_SETUP;
      }
      if(startStopButtonPressed)
      {
        currentMode = currentMode == MODE_IDLE ? MODE_RUNNING : MODE_IDLE;
        if(currentMode == MODE_RUNNING)
        {
          // STARTING TIMER!
          startTime = now();
        }
      }
      break;
The previous code snippet shows how the IDLE mode is managed, and it's pretty self-explanatory. Another example shows how any button press while ringing will stop it:
    case MODE_RINGING:
      if(resetButtonPressed || startStopButtonPressed || downButtonPressed || upButtonPressed)
      {
        currentMode = MODE_IDLE;
      }
      break;

Looks easy, isn't it? :-) It is.

The next block - "Time management" - performs the actual time difference calculation, triggers the RINGING mode and actually rings the buzz when it's time to do so.

The last block - "LCD management" - manages the LCD display for each mode by printing the proper strings at their proper locations.

That's it.

Wrap up and action!

Now that this little puppy has no more secrets to you, let's see it in action. Thanks for watching, and have fun!

Fritzing Schematics

You can use any general purpose breadboard, including the Starter Kit one which is smaller than the one depicted here (the buzzer can be placed under the yellow wires, see the video and the photos).

Fritzing Circuit Diagram

CODE

/**************************************************
 * Arduino Kitchen Timer v1.0 - 2016/01/27
 * By Angelo Fiorillo (Rome, IT)
 * This work is distributed under the GNU General 
 * Public License version 3 or later (GPL3+)
 * Please include this credit note if you want to 
 * re-use any part of this sketch. Respect my work 
 * as I'll do with yours.
 * Feel free to contact me: afiorillo@gmail.com
 * ************************************************/
#include <LiquidCrystal.h>
#include <Time.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const int buzzerPin = 10;
const int resetButtonPin = 6;
const int startStopButtonPin = 7;
const int downButtonPin = 8;
const int upButtonPin = 9;

int setupHours = 0;     // How many hours will count down when started
int setupMinutes = 0;   // How many minutes will count down when started
int setupSeconds = 0;   // How many seconds will count down when started
time_t setupTime = 0;

int currentHours = 0;
int currentMinutes = 0;
int currentSeconds = 0;
time_t currentTime = 0;

time_t startTime = 0;
time_t elapsedTime = 0;

int resetButtonState = LOW;
long resetButtonLongPressCounter = 0;
int startStopButtonState = LOW;
int upButtonState = LOW;
int downButtonState = LOW;
int resetButtonPrevState = LOW;
int startStopButtonPrevState = LOW;
int upButtonPrevState = LOW;
int downButtonPrevState = LOW;
bool resetButtonPressed = false;
bool resetButtonLongPressed = false;
bool startStopButtonPressed = false;
bool upButtonPressed = false;
bool downButtonPressed = false;

const int MODE_IDLE = 0;
const int MODE_SETUP = 1;
const int MODE_RUNNING = 2;
const int MODE_RINGING = 3;

int currentMode = MODE_IDLE;    // 0=idle 1=setup 2=running 3=ringing
                                // Power up --> idle
                                // Reset --> idle
                                //  Start/Stop --> start or stop counter
                                //  Up / Down --> NOP
                                // Reset (long press) --> enter setup
                                //   Start/Stop --> data select
                                //   Up --> increase current data value
                                //   Down --> decrease current data value
                                //   Reset --> exit setup (idle)

int dataSelection = 0;  // Currently selected data for edit (setup mode, changes with Start/Stop)
                        // 0=hours (00-99) 1=minutes (00-59) 2=seconds (00-59)

void setup() {
  // put your setup code here, to run once:
  lcd.begin(16, 2);
  pinMode(resetButtonPin, INPUT);
  pinMode(startStopButtonPin, INPUT);
  pinMode(upButtonPin, INPUT);
  pinMode(downButtonPin, INPUT);
  pinMode(buzzerPin, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  startStopButtonPressed = false;
  upButtonPressed = false;
  downButtonPressed = false;

  /*
   * Reset button management
   */
  resetButtonPressed = false;
  resetButtonLongPressed = false;
  resetButtonState = digitalRead(resetButtonPin);
  if(resetButtonState != resetButtonPrevState)
  {
    resetButtonPressed = resetButtonState == HIGH;
    resetButtonPrevState = resetButtonState;
  }
  else  // Long press management...
  {
    if(resetButtonState == HIGH)
    {
      resetButtonLongPressCounter++;
      if(resetButtonLongPressCounter == 100)
      {
        resetButtonPressed = false;
        resetButtonLongPressed = true;
        resetButtonLongPressCounter = 0;
      }
    }
    else
    {
      resetButtonLongPressCounter = 0;
      resetButtonPressed = false;
      resetButtonLongPressed = false;
    }
  }

  /*
   * Start/Stop button management
   */
  startStopButtonPressed = false;
  startStopButtonState = digitalRead(startStopButtonPin);
  if(startStopButtonState != startStopButtonPrevState)
  {
    startStopButtonPressed = startStopButtonState == HIGH;
    startStopButtonPrevState = startStopButtonState;
  }

  /*
   * Down button management
   */
  downButtonPressed = false;
  downButtonState = digitalRead(downButtonPin);
  if(downButtonState != downButtonPrevState)
  {
    downButtonPressed = downButtonState == HIGH;
    downButtonPrevState = downButtonState;
  }

  /*
   * Up button management
   */
  upButtonPressed = false;
  upButtonState = digitalRead(upButtonPin);
  if(upButtonState != upButtonPrevState)
  {
    upButtonPressed = upButtonState == HIGH;
    upButtonPrevState = upButtonState;
  }

  /*
   * Mode management
   */
  switch(currentMode)
  {
    case MODE_IDLE:
      if(resetButtonPressed)
      {
        Reset();
      }
      if(resetButtonLongPressed)
      {
        currentMode = MODE_SETUP;
      }
      if(startStopButtonPressed)
      {
        currentMode = currentMode == MODE_IDLE ? MODE_RUNNING : MODE_IDLE;
        if(currentMode == MODE_RUNNING)
        {
          // STARTING TIMER!
          startTime = now();
        }
      }
      break;

    case MODE_SETUP:
      if(resetButtonPressed)
      {
        // Exit setup mode
        setupTime = setupSeconds + (60 * setupMinutes) + (3600 * setupHours);
        currentHours = setupHours;
        currentMinutes = setupMinutes;
        currentSeconds = setupSeconds;
        dataSelection = 0;
        currentMode = MODE_IDLE;
      }
      if(startStopButtonPressed)
      {
        // Select next data to adjust
        dataSelection++;
        if(dataSelection == 3)
        {
          dataSelection = 0;
        }
      }
      if(downButtonPressed)
      {
        switch(dataSelection)
        {
          case 0: // hours
            setupHours--;
            if(setupHours == -1)
            {
              setupHours = 99;
            }
            break;
          case 1: // minutes
            setupMinutes--;
            if(setupMinutes == -1)
            {
              setupMinutes = 59;
            }
            break;
          case 2: // seconds
            setupSeconds--;
            if(setupSeconds == -1)
            {
              setupSeconds = 59;
            }
            break;
        }
      }
      if(upButtonPressed)
      {
        switch(dataSelection)
        {
          case 0: // hours
            setupHours++;
            if(setupHours == 100)
            {
              setupHours = 0;
            }
            break;
          case 1: // minutes
            setupMinutes++;
            if(setupMinutes == 60)
            {
              setupMinutes = 0;
            }
            break;
          case 2: // seconds
            setupSeconds++;
            if(setupSeconds == 60)
            {
              setupSeconds = 0;
            }
            break;
        }
      }
      break;
    
    case MODE_RUNNING:
      if(startStopButtonPressed)
      {
        currentMode = MODE_IDLE;
      }
      if(resetButtonPressed)
      {
        Reset();
        currentMode = MODE_IDLE;
      }
      break;

    case MODE_RINGING:
      if(resetButtonPressed || startStopButtonPressed || downButtonPressed || upButtonPressed)
      {
        currentMode = MODE_IDLE;
      }
      break;
  }

  /*
   * Time management
   */
  switch(currentMode)
  {
    case MODE_IDLE:
    case MODE_SETUP:
      // NOP
      break;
    case MODE_RUNNING:
      currentTime = setupTime - (now() - startTime);
      if(currentTime <= 0)
      {
        currentMode = MODE_RINGING;
      }
      break;
    case MODE_RINGING:
      analogWrite(buzzerPin, 20);
      delay(20);
      analogWrite(buzzerPin, 0);
      delay(40);
      break;
  }

  /*
   * LCD management
   */
  //lcd.clear();
  lcd.setCursor(0, 0);
  switch(currentMode)
  {
    case MODE_IDLE:
      lcd.print("Timer ready     ");
      lcd.setCursor(0, 1);
      lcd.print(currentHours);
      lcd.print(" ");
      lcd.print(currentMinutes);
      lcd.print(" ");
      lcd.print(currentSeconds);
      lcd.print("    ");
      break;
    case MODE_SETUP:
      lcd.print("Setup mode: ");
      switch(dataSelection)
      {
        case 0:
          lcd.print("HRS ");
          break;
        case 1:
          lcd.print("MINS");
          break;
        case 2:
          lcd.print("SECS");
          break;
      }
      lcd.setCursor(0, 1);
      lcd.print(setupHours);
      lcd.print(" ");
      lcd.print(setupMinutes);
      lcd.print(" ");
      lcd.print(setupSeconds);
      lcd.print("    ");
      break;
    case MODE_RUNNING:
      lcd.print("Counting down...");
      lcd.setCursor(0, 1);
      if(hour(currentTime) < 10) lcd.print("0");
      lcd.print(hour(currentTime));
      lcd.print(":");
      if(minute(currentTime) < 10) lcd.print("0");
      lcd.print(minute(currentTime));
      lcd.print(":");
      if(second(currentTime) < 10) lcd.print("0");
      lcd.print(second(currentTime));
      break;
    case MODE_RINGING:
      lcd.print("   RING-RING!   ");
      lcd.setCursor(0, 1);
      lcd.print("                ");
      break;
  }
  delay(10);
}

void Reset()
{
  currentMode = MODE_IDLE;
  currentHours = setupHours;
  currentMinutes = setupMinutes;
  currentSeconds = setupSeconds;
}