Published: January 16, 2026
This project is a digital alarm clock built with an Arduino UNO, a 16x2 LCD display, and a Real-Time Clock (RTC) module. It can keep accurate time even when the Arduino is turned off, lets you set the time and alarm using push buttons, and sounds a buzzer when the alarm goes off. You can switch between 12-hour and 24-hour display modes and stop the alarm with a button press.
The full breakdown of how to assemble this project in detail can be found in my beginners Arduino guide here →
| Component | Purpose |
|---|---|
| Arduino UNO | Main microcontroller |
| LCD1602 (16-pin) | Displays the time + alarm |
| DS1307 RTC Module | Keeps accurate time |
| Push Buttons (x3) | Set/stop/toggle alarm |
| Passive or Active Buzzer | Sounds the alarm |
| Potentiometer (10kΩ) | Controls LCD contrast |
| Resistors (10kΩ + 220Ω) | Pull-downs + backlight |
| Breadboard & wires | All connections |

Language: C++
// Arduino LCD Alarm Clock (1602) — Final Sketch with 12/24h toggle, AM/PM display,
// 12h-aware time/alarm setting, passive-buzzer alarm pattern, and improved alarm row.
//
// Hardware
// - LCD1602 (parallel): RS->7, E->8, D4..D7->9..12, RW->GND, V0 via 10k pot, LED+ (15) via 220Ω to 5V, LED- (16) to GND
// - Buttons: SET_TIME->D2, SET_ALARM->D3, STOP->D4 (wired with external 10k pulldown to GND, button to +5V)
// - Buzzer (passive): D5 -> + pin, other pin to GND
// - RTC: DS3231/DS1307 via I2C (SDA/SCL)
//
// Usage
// - NORMAL screen: double-press STOP to toggle 12HR/24HR (top-right shows mode).
// - SET TIME: press SET_TIME to enter. In hour screen:
// * Press SET_TIME to increment.
// * In 12h mode: short-press STOP toggles AM/PM.
// * Press SET_ALARM (or long-press SET_TIME/SET_ALARM) to move to minutes.
// Minutes screen: press SET_TIME to increment; press SET_ALARM (or long-press) to save.
// - SET ALARM: press SET_ALARM to enter; same controls as TIME. Saving enables the alarm.
// - When alarm rings: press STOP to silence.
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal.h>
// ---------- LCD pins (RS, EN, D4, D5, D6, D7)
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);
// ---------- RTC
RTC_DS3231 rtc; // Works with DS1307 too via RTClib
// ---------- Pins
const int BTN_SET_TIME = 2;
const int BTN_SET_ALARM = 3;
const int BTN_STOP = 4;
const int BUZZER_PIN = 5; // passive buzzer
// ---------- Alarm state
int alarmHour = 7; // 0..23 (stored internally in 24h)
int alarmMinute = 0;
bool alarmEnabled = false;
bool alarmRinging = false;
// ---------- Display mode
bool is12HourMode = false; // toggled by STOP double-press in NORMAL
// ---------- Button handling (debounce + edge + long-press)
struct BtnState {
int pin;
bool lastLevel;
bool pressedEdge;
bool longPressEdge;
unsigned long pressedAt;
BtnState(int p): pin(p), lastLevel(false), pressedEdge(false), longPressEdge(false), pressedAt(0) {}
};
BtnState bTime(BTN_SET_TIME);
BtnState bAlarm(BTN_SET_ALARM);
BtnState bStop(BTN_STOP);
const unsigned long DEBOUNCE_MS = 30;
const unsigned long LONG_MS = 800;
bool readBtn(BtnState &b) {
bool level = digitalRead(b.pin); // active HIGH (external pulldown)
bool rising = level && !b.lastLevel;
bool falling = !level && b.lastLevel;
b.pressedEdge = false;
b.longPressEdge = false;
if (rising) b.pressedAt = millis();
if (falling) {
unsigned long held = millis() - b.pressedAt;
if (held >= LONG_MS) b.longPressEdge = true;
else if (held >= DEBOUNCE_MS) b.pressedEdge = true;
}
b.lastLevel = level;
return level;
}
// ---------- Modes
enum Mode { NORMAL, SET_TIME_H, SET_TIME_M, SET_ALARM_H, SET_ALARM_M, ALARM_RING };
Mode mode = NORMAL;
// ---------- Edit buffers
int newHour = 0, newMinute = 0; // TIME editing
bool editPM = false; // for 12h time edit
int newAlarmHour = 0, newAlarmMinute = 0; // ALARM editing
bool editAlarmPM = false; // for 12h alarm edit
// ---------- STOP double-press detection
unsigned long lastStopPressAt = 0;
const unsigned long DOUBLE_MS = 450; // window for double-press
// ---------- Passive buzzer alarm pattern (non-blocking)
unsigned long lastToneMs = 0;
bool toneFlip = false; // alternate between two tones
void startAlarmTone() {
lastToneMs = millis();
toneFlip = false;
tone(BUZZER_PIN, 1200); // start high
}
void updateAlarmTone() {
// Alternate tones every 250 ms for an alarm-y sound
unsigned long now = millis();
if (now - lastToneMs >= 250) {
toneFlip = !toneFlip;
tone(BUZZER_PIN, toneFlip ? 800 : 1200);
lastToneMs = now;
}
}
void stopAlarmTone() {
noTone(BUZZER_PIN);
}
// ---------- UI helpers
void clearRow(int row) {
lcd.setCursor(0, row);
for (int i = 0; i < 16; i++) lcd.print(' ');
}
void printTimeRow(const DateTime& now) {
// Row 0: time and mode tag on the far right (cols 12..15)
lcd.setCursor(0, 0);
// Clear row to avoid artifacts when switching modes
for (int i = 0; i < 16; i++) lcd.print(' ');
int h = now.hour();
int m = now.minute();
if (is12HourMode) {
int h12 = h % 12; if (h12 == 0) h12 = 12;
const char* ampm = (h < 12) ? "AM" : "PM";
// HH:MM AM
lcd.setCursor(0, 0);
char buf[8];
snprintf(buf, sizeof(buf), "%02d:%02d", h12, m);
lcd.print(buf);
lcd.setCursor(6, 0);
lcd.print(ampm);
} else {
// HH:MM (24h)
lcd.setCursor(0, 0);
char buf[8];
snprintf(buf, sizeof(buf), "%02d:%02d", h, m);
lcd.print(buf);
}
// Mode tag at far right
lcd.setCursor(12, 0);
lcd.print(is12HourMode ? "12HR" : "24HR");
}
void printAlarmRow() {
// Row 1 shows the alarm time in either 24h or 12h with AM/PM,
// and places the ON/OFF status at the far right (cols 12..15).
clearRow(1);
lcd.setCursor(0, 1);
if (is12HourMode) {
int h24 = alarmHour;
int h12 = h24 % 12; if (h12 == 0) h12 = 12;
const char* ampm = (h24 < 12) ? "AM" : "PM";
// Print: "AL %02d:%02d" at cols 0..7
char tbuf[9];
snprintf(tbuf, sizeof(tbuf), "AL %02d:%02d", h12, alarmMinute);
lcd.print(tbuf);
// AM/PM at cols 9..10
lcd.setCursor(9, 1);
lcd.print(ampm);
// Status at cols 12..15
lcd.setCursor(12, 1);
if (alarmEnabled) lcd.print(" ON "); else lcd.print("OFF ");
} else {
// 24h: no AM/PM text
char tbuf[9];
snprintf(tbuf, sizeof(tbuf), "AL %02d:%02d", alarmHour, alarmMinute);
lcd.print(tbuf);
// Status at cols 12..15
lcd.setCursor(12, 1);
if (alarmEnabled) lcd.print(" ON "); else lcd.print("OFF ");
}
}
void showSetPrompt(const char* title, int val, bool showAMPM, bool pmFlag) {
// Row 0: title
clearRow(0);
lcd.setCursor(0, 0);
lcd.print(title);
// Row 1: value (+ optional AM/PM)
clearRow(1);
lcd.setCursor(0, 1);
char buf[5];
snprintf(buf, sizeof(buf), "%02d", val);
lcd.print(buf);
if (showAMPM) {
lcd.print(' ');
lcd.print(pmFlag ? "PM" : "AM");
}
lcd.setCursor(10, 1);
lcd.print("(next)");
}
void stopAlarm() {
stopAlarmTone();
alarmRinging = false;
mode = NORMAL;
}
void ringAlarm() {
alarmRinging = true;
mode = ALARM_RING;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" *** ALARM ***");
lcd.setCursor(0, 1);
lcd.print("Press STOP Btn");
startAlarmTone();
}
// ---------- Setup
void setup() {
pinMode(BTN_SET_TIME, INPUT);
pinMode(BTN_SET_ALARM, INPUT);
pinMode(BTN_STOP, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
lcd.begin(16, 2);
lcd.print("Digital Clock...");
delay(1200);
lcd.clear();
Wire.begin();
if (!rtc.begin()) {
lcd.print("RTC not found!");
while (1);
}
if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
}
// ---------- Loop
void loop() {
// Poll buttons
readBtn(bTime);
readBtn(bAlarm);
readBtn(bStop);
// Handle alarm ringing (non-blocking tone pattern)
if (mode == ALARM_RING) {
updateAlarmTone();
if (bStop.pressedEdge || bStop.longPressEdge) {
stopAlarm();
lcd.clear();
}
return; // stay here until stopped
}
// NORMAL clock display & alarm check
if (mode == NORMAL) {
DateTime now = rtc.now();
// Display
printTimeRow(now);
printAlarmRow();
// Alarm trigger (once at second==0)
if (alarmEnabled && !alarmRinging &&
now.hour() == alarmHour && now.minute() == alarmMinute && now.second() == 0) {
ringAlarm();
return;
}
// Enter set modes
if (bTime.pressedEdge || bTime.longPressEdge) {
// Init edit buffers from current time
DateTime n = rtc.now();
if (is12HourMode) {
editPM = (n.hour() >= 12);
int h12 = n.hour() % 12; if (h12 == 0) h12 = 12;
newHour = h12;
} else {
newHour = n.hour();
}
newMinute = n.minute();
mode = SET_TIME_H;
lcd.clear();
} else if (bAlarm.pressedEdge || bAlarm.longPressEdge) {
// Init edit buffers from current alarm
if (is12HourMode) {
editAlarmPM = (alarmHour >= 12);
int h12 = alarmHour % 12; if (h12 == 0) h12 = 12;
newAlarmHour = h12;
} else {
newAlarmHour = alarmHour;
}
newAlarmMinute = alarmMinute;
mode = SET_ALARM_H;
lcd.clear();
}
// STOP double-press to toggle 12/24h mode
if (bStop.pressedEdge) {
unsigned long nowMs = millis();
if (nowMs - lastStopPressAt <= DOUBLE_MS) {
is12HourMode = !is12HourMode;
lastStopPressAt = 0; // reset window
} else {
lastStopPressAt = nowMs;
}
}
delay(120);
return;
}
// -------- TIME SET MODE
if (mode == SET_TIME_H) {
// Show Hour prompt
showSetPrompt("Set Time: Hour", newHour, is12HourMode, editPM);
// Increment hour on TIME press
if (bTime.pressedEdge) {
if (is12HourMode) {
newHour++; if (newHour > 12) newHour = 1;
} else {
newHour = (newHour + 1) % 24;
}
showSetPrompt("Set Time: Hour", newHour, is12HourMode, editPM);
}
// In 12h mode, STOP short toggles AM/PM
if (is12HourMode && bStop.pressedEdge) {
editPM = !editPM;
showSetPrompt("Set Time: Hour", newHour, true, editPM);
}
// Next field
if (bAlarm.pressedEdge || bTime.longPressEdge || bAlarm.longPressEdge) {
mode = SET_TIME_M;
lcd.clear();
}
// Cancel
if (bStop.longPressEdge) {
mode = NORMAL;
lcd.clear();
}
return;
}
if (mode == SET_TIME_M) {
showSetPrompt("Set Time: Min", newMinute, false, false);
if (bTime.pressedEdge) {
newMinute = (newMinute + 1) % 60;
showSetPrompt("Set Time: Min", newMinute, false, false);
}
// Save
if (bAlarm.pressedEdge || bTime.longPressEdge || bAlarm.longPressEdge) {
// Build 24h value from edit fields
int saveH = newHour;
if (is12HourMode) {
saveH = newHour % 12;
if (editPM && saveH != 12) saveH += 12; // 1..11 PM => 13..23
if (!editPM && newHour == 12) saveH = 0; // 12 AM => 0
}
DateTime cur = rtc.now();
rtc.adjust(DateTime(cur.year(), cur.month(), cur.day(), saveH, newMinute, 0));
mode = NORMAL;
lcd.clear();
}
// Cancel
if (bStop.longPressEdge) {
mode = NORMAL;
lcd.clear();
}
return;
}
// -------- ALARM SET MODE
if (mode == SET_ALARM_H) {
showSetPrompt("Set Alarm: Hour", newAlarmHour, is12HourMode, editAlarmPM);
if (bTime.pressedEdge) {
if (is12HourMode) {
newAlarmHour++; if (newAlarmHour > 12) newAlarmHour = 1;
} else {
newAlarmHour = (newAlarmHour + 1) % 24;
}
showSetPrompt("Set Alarm: Hour", newAlarmHour, is12HourMode, editAlarmPM);
}
if (is12HourMode && bStop.pressedEdge) {
editAlarmPM = !editAlarmPM;
showSetPrompt("Set Alarm: Hour", newAlarmHour, true, editAlarmPM);
}
// Next field
if (bAlarm.pressedEdge || bTime.longPressEdge || bAlarm.longPressEdge) {
mode = SET_ALARM_M;
lcd.clear();
}
// Cancel
if (bStop.longPressEdge) {
mode = NORMAL;
lcd.clear();
}
return;
}
if (mode == SET_ALARM_M) {
showSetPrompt("Set Alarm: Min", newAlarmMinute, false, false);
if (bTime.pressedEdge) {
newAlarmMinute = (newAlarmMinute + 1) % 60;
showSetPrompt("Set Alarm: Min", newAlarmMinute, false, false);
}
// Save
if (bAlarm.pressedEdge || bTime.longPressEdge || bAlarm.longPressEdge) {
int saveH = newAlarmHour;
if (is12HourMode) {
saveH = newAlarmHour % 12;
if (editAlarmPM && saveH != 12) saveH += 12;
if (!editAlarmPM && newAlarmHour == 12) saveH = 0;
}
alarmHour = saveH;
alarmMinute = newAlarmMinute;
alarmEnabled = true;
mode = NORMAL;
lcd.clear();
}
// Cancel
if (bStop.longPressEdge) {
mode = NORMAL;
lcd.clear();
}
return;
}
}