Kitchen Timer
Beginner30 minA kitchen timer for tea, eggs or pasta: you set the time by turning the rotary encoder, and the 4-digit display shows it as minutes:seconds. Press the knob and the countdown starts, with the colon in the middle blinking once a second; press again to pause, once more to carry on where it stopped, and hold it down to go back to the time you set. When the time is up the buzzer beeps three times a second and the display blinks; a press silences it and the same time is ready to run again. It teaches reading a rotary encoder's direction with an interrupt, telling a short press from a long one on a single button, counting down with millis(), and driving a TM1637 display with two wires. Install "TM1637" (Avishay Orpaz) from the Library Manager.
Build it step by step in the appWhat you need
1 × Arduino Uno R3
1 × Breadboard 830 Points (MB-102)
1 × 4-Digit 7-Segment Display Module (TM1637)
1 × Rotary Encoder (KY-040)
1 × Passive Buzzer (Piezo)
11 × Jumper Wire - Male/Male
4 × Jumper Wire - Male/Female
Wiring steps
- 1
Connect and join the power rails

Plug a red male-male jumper wire into the 5V pin on the Arduino's power header and connect its other end to the breadboard's bottom red (+) rail. Plug a black wire into the GND pin on the same header and connect it to the bottom blue (−) rail. The breadboard's top and bottom rails are not connected to each other: at the right end of the breadboard, join the bottom (+) rail to the top (+) rail with a red wire, and the bottom (−) rail to the top (−) rail with a black wire. Now all four rails carry 5V and GND.
- Arduino 5V → the bottom red (+) rail (red wire)
- Arduino GND → the bottom blue (−) rail (black wire)
- the bottom red (+) rail → the top red (+) rail (red wire)
- the bottom blue (−) rail → the top blue (−) rail (black wire)
- 2
Plug in and wire the rotary encoder

Plug the rotary encoder module into the breadboard with its pins in row F of the top half: CLK in column 9, DT in 10, SW in 11, + in 12 and GND in 13. Modules whose pins point sideways stand upright on the breadboard. Connect column 9 to the Arduino's pin 2 with a yellow wire, column 10 to pin 3 with an orange wire and column 11 to pin 4 with a white wire (ENCODER_CLK, ENCODER_DT, ENCODER_SW). Connect column 12 to the top (+) rail with a short red wire and column 13 to the top (−) rail with a short black wire. CLK goes to pin 2 because on an Uno only pins 2 and 3 have interrupts: the code catches every click with one, so no click is lost even when you turn fast.
- rotary encoder CLK → column 9, top half
- rotary encoder DT → column 10, top half
- rotary encoder SW → column 11, top half
- rotary encoder + pin → column 12, top half
- rotary encoder GND → column 13, top half
- Arduino pin 2 → column 9, top half (yellow wire)
- Arduino pin 3 → column 10, top half (orange wire)
- Arduino pin 4 → column 11, top half (white wire)
- column 12, top half → the top red (+) rail (red wire)
- column 13, top half → the top blue (−) rail (black wire)
- 3
Connect the display

Push the female ends of male-female wires onto the display module's four pins; their names are printed on the board: CLK, DIO, VCC, GND. Connect CLK to the Arduino's pin 7 and DIO to pin 6 (DISPLAY_CLK, DISPLAY_DIO). Connect VCC to the top (+) rail and GND to the top (−) rail. Some modules have their pins in a different order, so go by the names printed next to the pins, not by their order.
- Arduino pin 7 → display CLK (green wire)
- Arduino pin 6 → display DIO (blue wire)
- display VCC → the top red (+) rail (red wire)
- display GND → the top blue (−) rail (black wire)
- 4
Add and wire the buzzer

Plug the passive buzzer's + leg into column 21 in the top half and its other leg into column 22; the + leg is usually the longer one and the body has a + mark. Connect the Arduino's pin 5 to column 21 with a purple wire (BUZZER_PIN 5). Connect column 22 to the top (−) rail with a short black wire.
- buzzer + leg → column 21, top half
- buzzer − leg → column 22, top half
- Arduino pin 5 → column 21, top half (purple wire)
- column 22, top half → the top blue (−) rail (black wire)
Arduino sketch
// Install from the Library Manager:
// "TM1637" (Avishay Orpaz)
#include <TM1637Display.h>
// Rotary encoder (KY-040): two signals
// and a button that closes when pressed
const int ENCODER_CLK = 2;
const int ENCODER_DT = 3;
const int ENCODER_SW = 4;
// The display module's CLK and DIO pins
const int DISPLAY_CLK = 7;
const int DISPLAY_DIO = 6;
const int BUZZER_PIN = 5;
// Set true if turning right lowers it
const bool REVERSE = false;
// Longest time: 99 minutes
const long MAX_SECONDS = 99L * 60;
// Holding this long resets (ms)
const unsigned long LONG_PRESS = 800;
// The alarm rings this long at most (ms)
const unsigned long ALARM_TIME = 60000;
// The colon in the middle of the display
const uint8_t COLON = 0b01000000;
TM1637Display display(DISPLAY_CLK, DISPLAY_DIO);
// Setting, counting, paused and alarm
enum State { SETTING, RUNNING, PAUSED, ALARM };
State state = SETTING;
// Time set (s) and time left (ms)
long setSeconds = 0;
long remainingMs = 0;
unsigned long lastTick = 0;
unsigned long alarmStart = 0;
bool beeping = false;
// Clicks the interrupt counted (+ right)
volatile int turns = 0;
volatile unsigned long lastEdge = 0;
unsigned long lastTurn = 0;
bool buttonDown = false;
unsigned long buttonChange = 0;
unsigned long pressStart = 0;
bool longDone = false;
// What was last written on the display
long shown = -1;
// Runs on every click: when CLK falls, DT
// tells which way the knob turned
void onTurn() {
// Bounces within 3 ms are not counted
unsigned long now = millis();
if (now - lastEdge < 3) return;
lastEdge = now;
turns += digitalRead(ENCODER_DT) == HIGH ? 1 : -1;
}
long secondsLeft() {
return (remainingMs + 999) / 1000;
}
// Prints a time to Serial Monitor (MM:SS)
void printTime(const char *label, long seconds) {
Serial.print(label);
if (seconds / 60 < 10) Serial.print("0");
Serial.print(seconds / 60);
Serial.print(":");
if (seconds % 60 < 10) Serial.print("0");
Serial.println(seconds % 60);
}
// One click's step: 10 s up to 2 minutes,
// 30 s up to 10 minutes, then 1 minute
long stepFor(long seconds, int dir) {
if (dir > 0) seconds++;
if (seconds > 600) return 60;
if (seconds > 120) return 30;
return 10;
}
void stopAlarm() {
noTone(BUZZER_PIN);
beeping = false;
state = SETTING;
// One press runs the same time again
remainingMs = setSeconds * 1000;
Serial.println("Alarm stopped.");
}
void turn(int dir) {
if (state == ALARM) {
stopAlarm();
return;
}
long seconds = state == SETTING ? setSeconds : secondsLeft();
long step = stepFor(seconds, dir);
// Lands on a step: right from 47 is 50
long next = dir > 0 ? (seconds / step + 1) * step
: ((seconds + step - 1) / step - 1) * step;
next = constrain(next, 0, MAX_SECONDS);
if (next == seconds) return;
// While counting it never turns to zero
if (next == 0 && state != SETTING) return;
if (state == SETTING) setSeconds = next;
remainingMs = next * 1000;
printTime("Time: ", next);
tone(BUZZER_PIN, 4000, 2);
}
void shortPress() {
if (state == SETTING) {
if (setSeconds == 0) return;
remainingMs = setSeconds * 1000;
state = RUNNING;
lastTick = millis();
tone(BUZZER_PIN, 1500, 80);
printTime("Started: ", setSeconds);
} else if (state == RUNNING) {
state = PAUSED;
tone(BUZZER_PIN, 800, 80);
printTime("Paused: ", secondsLeft());
} else if (state == PAUSED) {
state = RUNNING;
lastTick = millis();
tone(BUZZER_PIN, 1500, 80);
printTime("Resumed: ", secondsLeft());
}
}
// Held down: when counting, back to the
// time set; when setting, back to zero
void longPress() {
if (state == SETTING) setSeconds = 0;
state = SETTING;
remainingMs = setSeconds * 1000;
tone(BUZZER_PIN, 600, 150);
printTime("Reset: ", setSeconds);
}
// A short press starts or pauses, a long
// press resets
void readButton() {
bool down = digitalRead(ENCODER_SW) == LOW;
if (down != buttonDown) {
// Contact bounce: wait 30 ms
if (millis() - buttonChange < 30) return;
buttonChange = millis();
buttonDown = down;
if (down) {
pressStart = millis();
longDone = false;
// A press stops the alarm at once
if (state == ALARM) {
stopAlarm();
longDone = true;
}
} else if (!longDone) {
shortPress();
}
} else if (down && !longDone && millis() - pressStart >= LONG_PRESS) {
longDone = true;
longPress();
}
}
void update() {
unsigned long now = millis();
if (state == RUNNING) {
remainingMs -= (long)(now - lastTick);
lastTick = now;
if (remainingMs <= 0) {
remainingMs = 0;
state = ALARM;
alarmStart = now;
Serial.println("Time's up!");
}
}
if (state == ALARM) {
unsigned long t = now - alarmStart;
// Goes quiet by itself after a minute
if (t >= ALARM_TIME) {
stopAlarm();
return;
}
// Three short beeps every second
bool on = t % 1000 < 600 && t % 200 < 100;
if (on != beeping) {
beeping = on;
if (on) tone(BUZZER_PIN, 2000);
else noTone(BUZZER_PIN);
}
}
}
// Writes to the display only on a change;
// writing takes a few ms
void updateDisplay() {
unsigned long now = millis();
long seconds = state == SETTING ? setSeconds : secondsLeft();
bool colon = true;
bool visible = true;
if (state == RUNNING) {
// The colon blinks once a second
colon = remainingMs % 1000 >= 500;
} else if (state == PAUSED) {
// Paused, the digits blink; after a
// turn the new time stays in view
visible = now % 1000 < 600 || now - lastTurn < 1500;
} else if (state == ALARM) {
visible = (now - alarmStart) % 1000 < 500;
}
long key = seconds * 4 + colon * 2 + visible;
if (key == shown) return;
shown = key;
if (!visible) {
display.clear();
return;
}
int value = seconds / 60 * 100 + seconds % 60;
display.showNumberDecEx(value, colon ? COLON : 0, true);
}
void setup() {
Serial.begin(9600);
pinMode(ENCODER_CLK, INPUT_PULLUP);
pinMode(ENCODER_DT, INPUT_PULLUP);
pinMode(ENCODER_SW, INPUT_PULLUP);
// CLK on an interrupt: no lost clicks
attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), onTurn, FALLING);
display.setBrightness(5);
Serial.println("Kitchen timer ready.");
}
void loop() {
// Take the clicks the interrupt counted
noInterrupts();
int n = turns;
turns = 0;
interrupts();
if (REVERSE) n = -n;
if (n != 0) lastTurn = millis();
while (n > 0) {
turn(1);
n--;
}
while (n < 0) {
turn(-1);
n++;
}
readButton();
update();
updateDisplay();
}
What the Serial Monitor shows
Kitchen timer ready. Time: 00:10 Time: 00:20 Time: 00:30 Started: 00:30 Paused: 00:21 Resumed: 00:21 Time's up! Alarm stopped.
What you will see
Once the code is uploaded the display shows 00:00. Turn the encoder right: each click gives a small tick from the buzzer and adds 10 seconds up to 2 minutes, 30 seconds up to 10 minutes, then a minute at a time (99:00 at most); turning left takes time off. Press the knob once: a short beep plays, the countdown starts and the colon in the middle blinks once a second. While it counts you can turn the knob to add or take off time. Press again to pause, and the digits blink; press once more to carry on. Hold the knob down to go back to the time you set; hold it while setting and the time goes back to 00:00. When the time is up the buzzer beeps three times a second and 00:00 blinks; a press or a turn silences it, and it also goes quiet by itself after a minute. The display shows the time you set again, so one press runs it once more. If turning right lowers the time, set REVERSE in the code to true and upload again. If the display stays dark, check that CLK and DIO are on pins 7 and 6 and that VCC and GND are not swapped. If turning does not change the time, check that CLK is on pin 2 and DT on pin 3.
In the app, tick off every wire, match the parts to your inventory and pick up where you left off.
Build it step by step in the app