John Bradnam
Published © GPL3+

Dynamite Kitchen Timer

An interesting twist on a kitchen timer.

IntermediateFull instructions provided12 hours357
Dynamite Kitchen Timer

Things used in this project

Hardware components

Microchip ATtiny1614 Microprocessor
×1
Pushbutton switch 12mm
SparkFun Pushbutton switch 12mm
×1
Buzzer
Buzzer
Passive type
×1
TM1650 4-Digit driver IC
×1
4 Digit 7 segment Common Cathode 0.36in Clock display
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

STL files for 3D printing

Schematics

Schematic

PCB

Eagle Files

Schematic and PCB in Eagle format

Code

Dynamite_Kitchen_Timer_V2.ino

C/C++
// Dynamite Kitchen Timer V2
// Design and Software: jbrad2089@gmail.com
//
// 2022-01-15 - V1
//    - Created code base
// 2022-01-18 - V2
//    - Modified sound to speed up clicks in the last 4 seconds
//    - Removed music and made alarm repeatly play a single note

/*-----------------------------------------------------------------------------
  ATTiny1614 Pins mapped to Ardunio Pins

             +--------+
         VCC + 1   14 + GND
 (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
       1 PA5 + 3   12 + PA2 9  (MISO)
 (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
       3 PA7 + 5   10 + PA0 11 (UPDI)
 (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
 (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
             +--------+

  PA0 to PA7 can be analog or digital
  PWM on D0, D1, D6, D7, D10

  BOARD: ATtiny1614/1604/814/804/414/404/214/204
  Chip: ATtiny1614
  Clock Speed: 20MHz
  Programmer: jtag2updi (megaTinyCore)
-----------------------------------------------------------------------------*/

#define CLK       0   //PA4
#define DATA      1   //PA5
#define SWITCH    2   //PA6
#define SPEAKER   10  //PA3

//Used to directly manipulate SPEAKER pin
#define BUZZER_PIN 3
#define BUZZER_BM PIN3_bm
#define BUZZER_PORT PORTA

#include <avr/power.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>

#include <TM1650.h>

TM1650 display1(DATA,   //byte dataPin
                CLK,    //byte clockPin
                4,      //byte number of digits
                true,   //boolean activeDisplay = true
                1       //byte intensity
);

enum SHOW {MINUTES,SECONDS};

const PROGMEM byte NUMBER_FONT[] = {
  //pgfedcba
  0b00111111, // 0
  0b00000110, // 1
  0b01011011, // 2
  0b01001111, // 3
  0b01100110, // 4
  0b01101101, // 5
  0b01111101, // 6
  0b00000111, // 7
  0b01111111, // 8
  0b01101111, // 9
  0b00000000  // SPACE
};
#define SPACE 10
#define DP_SEGMENT 0b10000000

enum BUTTON {NONE,SHORT,DOUBLE};
#define DOUBLE_PRESS_TIME 300
#define REPEAT_START_SPEED 500
#define REPEAT_INCREASE_SPEED 50
#define REPEAT_MAX_SPEED 50

enum MODE {TIMER,SET_MINUTE,SET_SECOND};
MODE currentMode = TIMER;

volatile uint8_t timerMinutes;
volatile uint8_t timerSeconds;
volatile uint8_t timerCount;
volatile uint8_t timerToneCount;
volatile uint8_t timerToneLimit;
volatile bool timerRunning = false;
volatile bool timerUpdated = false;
volatile bool alarmOn = false;

#define COLON_TIMEOUT 500
long colonTimeout = 0;
bool colon = true;

#define SLEEP_TIMEOUT 10000  //Amount of time no switch is pressed before going to sleep
uint32_t sleepTimeout = 0;   //Used to timeout mode switch function to sleep function
bool volatile ignoreNextPress = false;

#define DIGIT_TIMEOUT 150
long digitTimeout = 0;
bool digitOn = true;
bool digitFlash = false;

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0DABBAD0
typedef struct {
  uint32_t magic;
  uint8_t timerMinutes;
  uint8_t timerSeconds;
} EEPROM_DATA;

volatile EEPROM_DATA EepromData;       //Current EEPROM settings

//-------------------------------------------------------------------------
//Initialise Hardware

void setup() 
{
  pinMode(SPEAKER, OUTPUT);
  pinMode(SWITCH, INPUT_PULLUP);
  digitalWrite(SPEAKER, LOW);
 
  timerRunning = false;
  timerMinutes = 0;
  timerSeconds = 0;
  timerCount = 0;
  timerToneCount = 0;
  timerToneLimit = 0;

  // Initialize RTC
  while (RTC.STATUS > 0);                           // Wait until registers synchronized
  RTC.PER = 1023 >> 8;                              // Set period 1/256 second
  RTC.CLKSEL = RTC_CLKSEL_INT32K_gc;                // 32.768kHz Internal Oscillator  
  RTC.INTCTRL = RTC_OVF_bm;                         // Enable overflow interrupt
  RTC.CTRLA = RTC_PRESCALER_DIV32_gc | RTC_RTCEN_bm;// Prescaler /32 and enable

  //Attach a pin change interrupt to switches to stop music
  attachInterrupt(SWITCH,SwitchInterrupt,CHANGE);
  
  //Enable interrupts
  sei();

  readEepromData();

  timerMinutes = EepromData.timerMinutes;
  timerSeconds = EepromData.timerSeconds;

  displayNumber(MINUTES, timerMinutes, true, true);
  displayNumber(SECONDS, timerSeconds, true, true);

  sleepTimeout = millis() + SLEEP_TIMEOUT;
}

//---------------------------------------------------------------
// RTC Interrupt Handler
ISR(RTC_CNT_vect)
{
  if (timerRunning)
  {
    timerCount++;
    if (timerCount == 0)
    {
      if (timerSeconds > 0)
      {
        timerSeconds--;
      }
      else if (timerMinutes > 0)
      {
        timerSeconds = 59;
        timerMinutes--;
      }
      timerUpdated = true;
      timerToneLimit = (timerMinutes != 0 || timerSeconds > 4) ? 0 : 2 << timerSeconds;
      beepShort();
    }
    else
    {
      timerToneCount--;
      if (timerToneCount == 0)
      {
        timerToneCount = timerToneLimit;
        beepShort();
      }
    }
  }
  RTC.INTFLAGS = RTC_OVF_bm;                         // Reset overflow interrupt
}

//--------------------------------------------------------------------
//Handle pin change interrupt when button is pressed
void SwitchInterrupt()
{
  alarmOn = false;    //Causes alarm to stop playing
}

//-------------------------------------------------------------------------
// Handle interactions
void loop() 
{
  BUTTON btn;
  if (currentMode == TIMER)
  {
    btn = getButtonState(NULL);
    if (timerRunning && (btn == SHORT || btn == DOUBLE))
    {
      stopButtonPressed();
    }
    else if (btn != NONE && ignoreNextPress)
    {
      ignoreNextPress = false;
    }
    else if (!timerRunning && btn == SHORT && (timerMinutes != 0 || timerSeconds != 0))
    {
      startButtonPressed();
    }
    else if (btn == DOUBLE)
    {
      currentMode = SET_MINUTE;
      displayNumber(MINUTES, timerMinutes, true, true);
    }
  }
  else if (currentMode == SET_MINUTE)
  {
    btn = getButtonState(minutesRepeat);
    switch (btn)
    {
      case SHORT: 
        minutesRepeat(); 
        break;
        
      case DOUBLE: 
        displayNumber(MINUTES, timerMinutes, true, true);
        currentMode = SET_SECOND; 
        displayNumber(SECONDS, timerSeconds, true, true);
        break;
    }
    if (currentMode == SET_MINUTE && millis() >= digitTimeout)
    {
      digitOn = !digitOn;
      digitTimeout = millis() + DIGIT_TIMEOUT;
      displayNumber(MINUTES, timerMinutes, true, digitOn);
    }
  }
  else if (currentMode == SET_SECOND)
  {
    btn = getButtonState(secondsRepeat);
    switch (btn)
    {
      case SHORT: 
        secondsRepeat(); 
        break;
        
      case DOUBLE: 
        displayNumber(SECONDS, timerSeconds, true, true);
        currentMode = TIMER; 
        sleepTimeout = millis() + SLEEP_TIMEOUT;
        colonTimeout = millis() + COLON_TIMEOUT;
        timerUpdated = true;
        EepromData.timerMinutes = timerMinutes;
        EepromData.timerSeconds = timerSeconds;
        writeEepromData();
        break;
    }
    if (currentMode == SET_SECOND && millis() >= digitTimeout)
    {
      digitOn = !digitOn;
      digitTimeout = millis() + DIGIT_TIMEOUT;
      displayNumber(SECONDS, timerSeconds, true, digitOn);
    }
  }

  if (timerRunning)
  {
    //Display updated time
    if (millis() >= colonTimeout)
    {
      colon = !colon;
      colonTimeout = millis() + COLON_TIMEOUT;
      timerUpdated = true;
    }
  }

  if (timerRunning && timerUpdated)
  {
    timerUpdated = false;
    if (timerMinutes == 0 && timerSeconds == 0)
    {
      //Stop timer - Sound alarm
      stopButtonPressed();
      //Play the melody
      alarmOn = true;
      playTone(SPEAKER,0xE000 | 659,&alarmOn);
      //Reset time back to previous setting
      timerMinutes = EepromData.timerMinutes;
      timerSeconds = EepromData.timerSeconds;
    }
    displayNumber(MINUTES, timerMinutes, true, true);
    displayNumber(SECONDS, timerSeconds, true, true);
  }
  else if (!timerRunning && currentMode == TIMER && millis() >= sleepTimeout)
  {
    gotoSleep();
  }
  delay(100);
}

//---------------------------------------------------------------
//Test whether the button is down and handle short, long and repeat presses
BUTTON getButtonState(void (*pRepeatFunction)())
{
  long pressDelay;
  
  BUTTON pressed = NONE;
  if (digitalRead(SWITCH) == LOW)
  {
    delay(10);
    if (digitalRead(SWITCH) == LOW)
    {
      long pressStart = millis();
      if (pRepeatFunction != NULL)
      {
        //pRepeatFunction();
        unsigned long speed = REPEAT_START_SPEED;
        unsigned long time = millis() + speed;
        while (digitalRead(SWITCH) == LOW)
        {
          if (millis() >= time)
          {
            pRepeatFunction();
            unsigned long faster = speed - REPEAT_INCREASE_SPEED;
            if (faster >= REPEAT_MAX_SPEED)
            {
              speed = faster;
            }
            time = millis() + speed;
          }
        }
        pressDelay = millis() - pressStart;
        if (pressDelay < REPEAT_START_SPEED)
        {
          pressed = SHORT;
          pressDelay = pressStart + DOUBLE_PRESS_TIME;
          while (millis() < pressDelay)
          {
            if (digitalRead(SWITCH) == LOW)
            {
              delay(10);
              if (digitalRead(SWITCH) == LOW)
              {
                while (digitalRead(SWITCH) == LOW);
                pressed = DOUBLE;
              }
            }
          }
        }
      }
      else
      {
        while (digitalRead(SWITCH) == LOW);
        pressed = SHORT;
        pressDelay = pressStart + DOUBLE_PRESS_TIME;
        while (millis() < pressDelay)
        {
          if (digitalRead(SWITCH) == LOW)
          {
            delay(10);
            if (digitalRead(SWITCH) == LOW)
            {
              while (digitalRead(SWITCH) == LOW);
              pressed = DOUBLE;
            }
          }
        }
      }
    }
  }
  return pressed;
}
  
//---------------------------------------------------------------
//Handle Stop btton
void stopButtonPressed()
{
  timerRunning = false;
  colon = true;
  displayNumber(MINUTES, timerMinutes, true, true);
  displayNumber(SECONDS, timerSeconds, true, true);
  sleepTimeout = millis() + SLEEP_TIMEOUT;
}

//---------------------------------------------------------------
//Handle Menu btton
void startButtonPressed()
{
  timerToneCount = 0;
  timerToneLimit = 0;
  timerRunning = true;
  colonTimeout = millis() + COLON_TIMEOUT;
}

//---------------------------------------------------------------
//Handle repeat of minutes
void minutesRepeat()
{
  timerMinutes = (timerMinutes == 59) ? 0 : timerMinutes + 1;
  if (millis() >= digitTimeout)
  {
    digitOn = !digitOn;
    digitTimeout = millis() + DIGIT_TIMEOUT;
  }
  displayNumber(MINUTES, timerMinutes, true, digitOn);
}

//---------------------------------------------------------------
//Handle repeat of seconds
void secondsRepeat()
{
  timerSeconds = (timerSeconds == 59) ? 0 : timerSeconds + 1;
  if (millis() >= digitTimeout)
  {
    digitOn = !digitOn;
    digitTimeout = millis() + DIGIT_TIMEOUT;
  }
  displayNumber(SECONDS, timerSeconds, true, digitOn);
}

//---------------------------------------------------------------------
//Write number to display
//  s - SHOW constant
//  num - (0 to 99) 
//  leadingZeros - true to have leading zeros
//  on - true to show digit, false for hide digit
void displayNumber(SHOW s, int num, bool leadingZeros, bool on)
{
  num = max(min(num, 99), 0);
  int b = (s == MINUTES) ? 2 : 0;
  for (int i = 0; i < 2; i++)
  {
    if (on && (num > 0 || i == 0 || leadingZeros))
    {
      displayDigit(b + i, num % 10);
    }
    else
    {
      displayDigit(b + i, SPACE);
    }
    num = num / 10;
  }
}

//---------------------------------------------------------------------
//Write digit to display
//  digit - digit to write to (0 - left most to 3 - right most)
//  num - (0 to 9) or SPACE
void displayDigit(int digit, int number)
{
  byte segments = pgm_read_byte(&NUMBER_FONT[number & 0x0F]);
  if (digit == 2 && colon)
  {
    segments = segments | DP_SEGMENT;
  }
  display1.setSegments(segments, 3 - digit);
}

//---------------------------------------------------------------------
//Clear the display
void displayClear()
{
  for(int i = 0; i < 4; i++)
  {
    display1.setSegments(0,i);
  }
}

//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  #ifdef __AVR_ATtiny1614__
    eeprom_update_block (( void *) &EepromData , ( const void *) EEPROM_ADDRESS, sizeof(EepromData));  
  #else
    EEPROM.put(EEPROM_ADDRESS,EepromData);
  #endif
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
  //Eprom
  eeprom_read_block (( void *) &EepromData , ( const void *) EEPROM_ADDRESS, sizeof(EepromData));  
  #ifndef RESET_EEPROM
  if (EepromData.magic != EEPROM_MAGIC)
  #endif
  {
    EepromData.magic = EEPROM_MAGIC;
    EepromData.timerMinutes = 3;
    EepromData.timerSeconds = 0;
    writeEepromData();
  }
}

//-----------------------------------------------------------------------------------

//Turn on and off buzzer quickly
void beepShort() 
{
  BUZZER_PORT.OUTSET |= BUZZER_BM;    // turn on buzzer
  for(int i=0;i < 30;i++)
  {
    asm volatile("nop":::);
  }
  BUZZER_PORT.OUTCLR |= BUZZER_BM;   // turn off the buzzer
}

//-----------------------------------------------------------------------------------
int playTone(int pin, uint16_t noteRaw, volatile bool* alarmOn)
{
  //Play each note until button is pressed
  while (*alarmOn)
  {
    playNote(pin, noteRaw);
  } //while
}

//-----------------------------------------------------------------------------------
uint16_t playNote(int pin, uint16_t noteRaw)
{
  // to calculate the note duration, take one second divided by the note type.
  // e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
  uint16_t frequency = noteRaw & 0x1FFF;
  uint8_t duration = (noteRaw & 0xE000) >> 13;
  if (duration == 7)
    duration = 8;
  uint16_t noteDuration = 1800 / duration;

  tone(pin, frequency, noteDuration);
    
  // to distinguish the notes, set a minimum time between them.
  // the note's duration + 30% seems to work well:
  uint16_t pauseBetweenNotes = (noteDuration * 13) / 10;
  delay(pauseBetweenNotes);

  // stop the tone playing:
  noTone(pin);

  return frequency;
}

//--------------------------------------------------------
// Shut down display and put ATtiny to sleep
// Will wake up when SWITCH is pressed
void gotoSleep() 
{
  displayClear();
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);  // sleep mode is set here
  sleep_enable();
  sleep_mode();                         // System actually sleeps here
  sleep_disable();                      // System continues execution here when watchdog timed out
  displayNumber(MINUTES, timerMinutes, true, true);
  displayNumber(SECONDS, timerSeconds, true, true);
  sleepTimeout = millis() + SLEEP_TIMEOUT;
  ignoreNextPress = true;
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers

Comments