John Bradnam
Published © GPL3+

Twist-to-Set Kitchen Timer

A hand-held twist-to-set countdown timer for your kitchen.

IntermediateFull instructions provided24 hours801
Twist-to-Set Kitchen Timer

Things used in this project

Hardware components

Microchip ATtiny3216 Microprocessor
×1
EC12E24204A8 12mm Size Insulated Shaft Rotary Encoder
×1
4-Digit 7-Segment CC 0.36in Clock Display
×1
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
6mm shaft
×1
Buzzer
Buzzer
5mm high
×1
74HC14 Schmitt Trigger Hex Inverter
SOIC package
×1
Passive components
2 x 2k2 resistors 0805, 2 x 10k resistors 0805, 1 x 10uF ceramic capacitor 1206
×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

TwistToSetKitchenTimerV2.ino

C/C++
/**************************************************************************
 TWIST-TO-SET KITCHEN TIMER

 New mechanics, electronics and software for "an Attiny85 Twist-to-Set Kitchen Timer" by bobson.h
 (https://www.instructables.com/an-Attiny85-Twist-to-Set-Kitchen-Timer/)
 
 2027-04-20 V1 John Bradnam (jbrad2089@gmail.com)
  - Replaced optical rotary encoder with a mechanical rotary encoder
  - Designed new hardware around ATtiny3216 microprocessor
  - Initial code base
  
 --------------------------------------------------------------------------
 Arduino IDE:
 --------------------------------------------------------------------------
  BOARD: 20pin tinyAVR 0/1/2 Series
  Chip: ATtiny3216
  Clock Speed: 5MHz Internal
  Programmer: jtag2updi (megaTinyCore)

  ATtiny3216 Pins mapped to Ardunio Pins
                            _____
                    VDD   1|*    |20  GND
   (nSS)  (AIN4) PA4  0~  2|     |19  16~ PA3 (AIN3)(SCK)(EXTCLK)
          (AIN5) PA5  1~  3|     |18  15  PA2 (AIN2)(MISO)
   (DAC)  (AIN6) PA6  2   4|     |17  14  PA1 (AIN1)(MOSI)
          (AIN7) PA7  3   5|     |16  17  PA0 (AIN0/nRESET/UPDI)
          (AIN8) PB5  4   6|     |15  13  PC3
          (AIN9) PB4  5   7|     |14  12  PC2
   (RXD) (TOSC1) PB3  6   8|     |13  11~ PC1 (PWM only on 1-series)
   (TXD) (TOSC2) PB2  7~  9|     |12  10~ PC0 (PWM only on 1-series)
   (SDA) (AIN10) PB1  8~ 10|_____|11   9~ PB0 (AIN11)(SCL)
 

  PA0 to PA7, PB0 to PB5, PC0 to PC3 can be analog or digital
  PWM on D0, D1, D7, D8, D9, D10, D11, D16
  
 **************************************************************************/

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

//Pins
#define SPEAKER 12 //(PC2)
#define SWITCH 16  //(PA3)
#define SEG_A 1    //(PA5)
#define SEG_B 15   //(PA2)
#define SEG_C 10   //(PC0)
#define SEG_D 8    //(PB1)
#define SEG_E 7    //(PB2)
#define SEG_F 0    //(PA4)
#define SEG_G 9    //(PB0)
#define SEG_DP 11  //(PC1)
#define DIG_1 2    //(PA6)
#define DIG_2 3    //(PA7)
#define DIG_3 14   //(PA1)
#define DIG_4 6    //(PB3)
#define ENC_A 4    //(PB5)
#define ENC_B 5    //(PB4)

#define PA0_bm PIN0_bm
#define PA1_bm PIN1_bm
#define PA2_bm PIN2_bm
#define PA3_bm PIN3_bm
#define PA4_bm PIN4_bm
#define PA5_bm PIN5_bm
#define PA6_bm PIN6_bm
#define PA7_bm PIN7_bm
#define PB0_bm PIN0_bm
#define PB1_bm PIN1_bm
#define PB2_bm PIN2_bm
#define PB3_bm PIN3_bm
#define PB4_bm PIN4_bm
#define PB5_bm PIN5_bm
#define PC0_bm PIN0_bm
#define PC1_bm PIN1_bm
#define PC2_bm PIN2_bm
#define PC3_bm PIN3_bm

#define TCB_COMPARE 150           //Refresh value for 7 segment display (1000@20MHz, 100@4MHZ)
#define BRIGHTNESS 2              //Initial brightness level (0 to 15)
#define DIGITS 4                  //Number of digits in display

uint8_t digits[DIGITS];           //Holds the current digits to display
uint8_t nextDigit = 0;            //Holds next digit to display
uint8_t brightness = BRIGHTNESS;  //Current Brightness level (0 to 15)
uint8_t bamCounter = 0;           //Bit Angle Modulation variable to keep track of things
uint8_t bamBit;                   //Used to store bit to test against brightness value

//Digit array
#define SPACE 10
uint8_t segments[11] = {
//    abcdefg   abcdefg   abcdefg   abcdefg   abcdefg   abcdefg   abcdefg   abcdefg
    B01111110,B00110000,B01101101,B01111001,B00110011,B01011011,B01011111,B01110000,
    B01111111,B01111011,B00000000
};

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
  uint32_t magic;
  int countdownTime;
} EEPROM_DATA;

EEPROM_DATA volatile EepromData;      //Current EEPROM settings


#define COLON_FLASH_RATE 500;
unsigned long colonTimeout;
bool colonOn = false;

#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;
int8_t volatile rotaryDirection = 0;
bool volatile lastRotA = false;

bool volatile timeChanged = false;
bool volatile inCoundownMode = false;
bool volatile alarmOn = false;

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

void setup() 
{
  pinMode(SWITCH, INPUT_PULLUP);
  pinMode(ENC_A, INPUT);
  pinMode(ENC_B, INPUT);
  pinMode(SPEAKER, OUTPUT);
  digitalWrite(SPEAKER, LOW);

  readEepromData();
 
  //Initialise digits
  memset(&digits[0],SPACE,4);

  //Set up 7 segment display refresh timer
  TCB1.CCMP = TCB_COMPARE;
  TCB1.INTCTRL = TCB_CAPT_bm;
  TCB1.CTRLA = TCB_ENABLE_bm;

  //Interrupt handers for rotary encoder
  attachRotaryPinChangeInterrupt();

  //Attach interrupt handler to wake up from sleep mode
  attachInterrupt(SWITCH, switchInterrupt, CHANGE);
  
  // Initialize RTC
  while (RTC.STATUS > 0);                           // Wait until registers synchronized
  RTC.PER = 1023;                                   // Set period 1 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

  //Force a display update
  inCoundownMode = false;
  timeChanged = true;
  sleepTimeout = millis() + SLEEP_TIMEOUT;

  //Enable interrupts
  sei();
}

//--------------------------------------------------------
// Attach the pin change interrupts for the rotary encoder
void attachRotaryPinChangeInterrupt()
{
  attachInterrupt(ENC_A, rotaryInterrupt, CHANGE);
}

//--------------------------------------------------------
// Detach the pin change interrupts for the rotary encoder
void detachRotaryPinChangeInterrupt()
{
  detachInterrupt(ENC_A);
}

//-------------------------------------------------------------------------
// Main loop
void loop() 
{
  if (buttonPressed())
  {
    if (inCoundownMode)
    {
      //Stop countdown
      inCoundownMode = false;
      displayTime(EepromData.countdownTime, true);
      sleepTimeout = millis() + SLEEP_TIMEOUT;
    }
    else
    {
      //Start countdown
      writeEepromData(); //Save last time
      colonTimeout = millis() + COLON_FLASH_RATE;
      colonOn = false;
      timeChanged = true;
      inCoundownMode = true;
    }
  }
  
  if (inCoundownMode)
  {
    //Handle flashing colon
    if (colonTimeout < millis())
    {
      colonTimeout = millis() + COLON_FLASH_RATE;
      colonOn = !colonOn;
      timeChanged = true;
    }
    if (timeChanged)
    {
      displayTime(EepromData.countdownTime, colonOn);
      timeChanged = false;
    }
    if (EepromData.countdownTime == 0)
    {
      displayTime(EepromData.countdownTime, true);
      soundAlaram();
      inCoundownMode = false;
      readEepromData(); //Pull last time used
      displayTime(EepromData.countdownTime, true);
    }
  }
  else if (timeChanged || rotaryDirection != 0)
  {
    if (rotaryDirection == 1 && EepromData.countdownTime < 3600)
    {
      EepromData.countdownTime += 1;
    }
    else if (rotaryDirection == -1 && EepromData.countdownTime > 0)
    {
      EepromData.countdownTime -= 1;
    }
    rotaryDirection = 0;
    displayTime(EepromData.countdownTime, true);
    timeChanged = false;
    sleepTimeout = millis() + SLEEP_TIMEOUT;
  }
  else if (!inCoundownMode && millis() >= sleepTimeout)
  {
    gotoSleep();
  }
  else
  {
    delay(100);
  }
}

//-----------------------------------------------------------------------------------
//Display a number on the 7 segment display
void displayTime(int secs, bool colon)
{
  displayNumber(secs / 60, colon, true, 2);
  displayNumber(secs % 60, colon, true, 0);
}

//-----------------------------------------------------------------------------------
//Display a number on the 7 segment display
void displayNumber(int number, bool colon, bool leading, int pos)
{
  for (uint8_t i = 0; i < 2; i++)
  {
    if (number > 0 || leading || i == 0)
    {
      digits[pos + i] = number % 10;
    }
    else
    {
      digits[pos + i] = SPACE;
    }
    if (colon && pos == 2 && i == 0)
    {
      digits[pos + i] |= 0x80;
    }
    number = number / 10;
  }
}

//-----------------------------------------------------------------------------------
//Turn off display
void displayOff()
{
  //Turn off display timer
  TCB1.CTRLA = TCB1.CTRLA & ~TCB_ENABLE_bm;
  
  //Set all digit cathodes HIGH and all segment anodes LOW
  PORTA.OUT = PORTA.IN & ~(PA2_bm | PA4_bm | PA5_bm) | PA1_bm | PA6_bm | PA7_bm; //(B, F, A), 3, 1, 2
  PORTB.OUT = PORTB_IN & ~(PB0_bm | PB1_bm | PB2_bm) | PB3_bm; //(G, D, E), 4
  PORTC.OUT = PORTC.IN & ~(PC0_bm | PC1_bm); //(C, DP)
}

//-----------------------------------------------------------------------------------
//Turn on display
void displayOn()
{
  //Turn on display timer
  TCB1.CTRLA = TCB1.CTRLA  | TCB_ENABLE_bm;
}

//---------------------------------------------------------------
// Test if button pessed
bool buttonPressed()
{
  bool down = false;
  if (digitalRead(SWITCH) == LOW)
  {
    delay(10);
    if (digitalRead(SWITCH) == LOW)
    {
      while (digitalRead(SWITCH) == LOW)
      {
        yield();
      }
      down = !ignoreNextPress;
      ignoreNextPress = false;
    }
  }
  return down;
}

//-----------------------------------------------------------------------------------
// Sound alarm
#define NOTE_A2  110
#define NOTE_B2  123
#define NOTE_G2  98
#define NOTE_G1  49
#define NOTE_D2  73

#define NUM_NOTES 5
const int closeEncounters[] PROGMEM = {                             // notes in the melody:
    NOTE_A2, NOTE_B2, NOTE_G2, NOTE_G1, NOTE_D2                     // "Close Encounters" tones
};
#define NOTE_DURATION 800
 
//Play the "Close Encounters" melody
void soundAlaram() 
{
  detachRotaryPinChangeInterrupt();
  int thisNote;
  alarmOn = true;
  while (alarmOn)
  {
    displayOff();
    thisNote = 0;
    while (alarmOn && (thisNote < NUM_NOTES))
    {
      tone(SPEAKER, pgm_read_byte(closeEncounters + thisNote) * 4); 
      delay(NOTE_DURATION);
      thisNote++;
    }
    noTone(SPEAKER);
    displayOn();
    long timeout = millis() + 2000;
    while (alarmOn && (millis() < timeout))
    {
      yield();
    }
  }
  attachRotaryPinChangeInterrupt();
  ignoreNextPress = true;
}

//--------------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData(void)
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  EEPROM.put(EEPROM_ADDRESS,EepromData);
}

//--------------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData(void)
{
#ifndef RESET_EEPROM
  EEPROM.get(EEPROM_ADDRESS,EepromData);
  if (EepromData.magic != EEPROM_MAGIC)
  {
#endif  
    EepromData.magic = EEPROM_MAGIC;
    EepromData.countdownTime = 0;
    writeEepromData();
#ifndef RESET_EEPROM
  }
#endif  
}

//--------------------------------------------------------
// Shut down display and put ATtiny to sleep
// Will wake up when SWITCH is pressed
void gotoSleep() 
{
  displayOff();
  detachRotaryPinChangeInterrupt();
  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
  attachRotaryPinChangeInterrupt();
  displayOn();
  timeChanged = true;                   // Force a screen update
  sleepTimeout = millis() + SLEEP_TIMEOUT;
  ignoreNextPress = true;
}

//=========================================== ISR Routines =======================================

//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each mS - output segments
ISR(TCB1_INT_vect)
{

  //This is 4 bit 'Bit Angle Modulation' or BAM, 
  if (bamCounter == (DIGITS * 1) || bamCounter == (DIGITS * 3) || bamCounter == (DIGITS * 7))
  {
    bamBit = bamBit << 1;
  }
  bamCounter++;

  //Display digit based on brightness
  uint8_t i = digits[3 - nextDigit];
  uint8_t d = segments[i & 0x7F];
  if (i & 0x80)
  {
    d |= 0x80;
  }

  //Make all segment and digits pins outputs
  PORTA.DIRSET = PA1_bm | PA2_bm | PA4_bm | PA5_bm | PA6_bm | PA7_bm; //3, B, F, A, 1, 2
  PORTB.DIRSET = PB0_bm | PB1_bm | PB2_bm | PB3_bm; //G, D, E, 4
  PORTC.DIRSET = PC0_bm | PC1_bm;          //C, DP

  //Set all digit cathodes HIGH and all segment anodes LOW
  uint8_t a = PORTA.IN & ~(PA2_bm | PA4_bm | PA5_bm) | PA1_bm | PA6_bm | PA7_bm; //(B, F, A), 3, 1, 2
  uint8_t b = PORTB_IN & ~(PB0_bm | PB1_bm | PB2_bm) | PB3_bm; //(G, D, E), 4
  uint8_t c = PORTC.IN & ~(PC0_bm | PC1_bm); //(C, DP)

  //Only show if bamBit matches brightness level
  if (brightness & bamBit)
  {
    //Set the cathode of the current digit LOW
    switch (nextDigit)
    {
      case 0: a &= ~PA6_bm; break;
      case 1: a &= ~PA7_bm; break;
      case 2: a &= ~PA1_bm; break;
      case 3: b &= ~PB3_bm; break;
    }
  
    //Set the segment anodes HIGH
    //Segment order for d is pabcdefg
    if (d & B01000000) a |= PA5_bm;  //A
    if (d & B00100000) a |= PA2_bm;  //B
    if (d & B00010000) c |= PC0_bm;  //C
    if (d & B00001000) b |= PB1_bm;  //D
    if (d & B00000100) b |= PB2_bm;  //E
    if (d & B00000010) a |= PA4_bm;  //F
    if (d & B00000001) b |= PB0_bm;  //G
    if (d & B10000000) c |= PC1_bm;  //DP
  }
  
  //Set the output pins
  PORTA.OUT = a;  //Set the outputs
  PORTB.OUT = b;
  PORTC.OUT = c;

  //Setup for next digit
  nextDigit = (nextDigit + 1) & 0x03;

  //Check if bamCount overflowed, reset if necessary
  if (bamCounter == (DIGITS * 15)) 
  {
    bamCounter = 0;
    bamBit = 0x01;
  }

  //Clear interrupt flag
  TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}

//---------------------------------------------------------------------
// Interrupt Handler: Rotary encoder has moved
void rotaryInterrupt()
{
  if (!digitalRead(ENC_A) && lastRotA)
  {
    //Record direction
    rotaryDirection = (digitalRead(ENC_B)) ? 1 : -1;
  }
  lastRotA = digitalRead(ENC_A);
}

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

//---------------------------------------------------------------
// RTC Interrupt Handler
ISR(RTC_CNT_vect)
{
  if (inCoundownMode)
  {
    if (EepromData.countdownTime > 0)
    {
      EepromData.countdownTime--;
    }
    timeChanged = true;
  }
  RTC.INTFLAGS = RTC_OVF_bm;                         // Reset overflow interrupt
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to bobson.h.

Comments