Smart Alarm Clock
HARDWARE REQUIRED:
- PICUNO Microcontroller board
- 1 × 16x2 I2C LCD Display
- 1 × DS1302 Real Time Clock Module
- 1 × DHT11 Temperature and humidity sensor
- 1 × Rotary Encoder Module
- 1 × Buzzer
- USB cable
- 1 × 4xAA Battery Pack (for external supply)
DESCRIPTION:
This project creates a multi-function smart alarm clock. By default, it displays the live time and date from a Real-Time Clock (RTC) module. The user can press the rotary encoder's button to cycle through different modes: a "weather station" that displays live temperature and humidity from a DHT11 sensor, and an "alarm set" mode. In alarm set mode, the rotary encoder knob is used to adjust the hour and minute for the alarm. When the real time matches the alarm time, a buzzer sounds continuously and a "TIME'S UP" message is displayed until the user presses the button to dismiss it.
LIBRARIES REQUIRED:
For C / Arduino IDE:
dht.h: Manages dht11 sensor.
Wire.h: Manages I2C communication (usually included by default).
LiquidCrystal_I2C.h: The driver library for the I2C LCD module.
For Micropython / Thonny IDE:
dht11.py: The custom library file for the dht11 sensor, saved to the PICUNO board.
i2c_lcd.py: The custom library file saved to the PICUNO board.
The code also uses the built-in machine and time modules.
LIBRARIES REQUIRED:
For C / Arduino IDE:
dht.h: Manages dht11 sensor.
Wire.h: Manages I2C communication (usually included by default).
LiquidCrystal_I2C.h: The driver library for the I2C LCD module.
For Micropython / Thonny IDE:
dht11.py: The custom library file for the dht11 sensor, saved to the PICUNO board.
i2c_lcd.py: The custom library file saved to the PICUNO board.
The code also uses the built-in machine and time modules.
CIRCUIT DIAGRAM:
- Connect the LCD Module's GND pin to GND.
- Connect the LCD Module's VCC pin to the Positive (+) terminal of the 4xAA Battery Pack.
- Connect the LCD Module's SDA pin GPIO 4 (SDA Pin on PICUNO).
- Connect the LCD Module's SCL pin GPIO 5 (SCL Pin on PICUNO).
- Connect the negative terminal (-) of the 4xAA Battery Pack to Common GND on breadboard.
- Connect the VCC pin to 3.3V pin on the board.
- Connect the GND pin to GND pin on board.
- Connect the CLK pin to GPIO 8.
- Connect the DAT pin to GPIO 9.
- Connect the RST pin to GPIO 10.
- Connect the VCC (+) pin to 3.3V pin on the board.
- Connect the GND pin to GND pin on board.
- Connect the CLK pin to GPIO 11.
- Connect the DT pin to GPIO 12.
- Connect the SW pin to GPIO 13.
- Connect the VCC (+) pin to 3.3V pin on the board.
- Connect the GND (-) pin to GND pin on board.
- Connect the Signal (S) pin to GPIO 6.
- Connect the GND (-) pin to GND pin.
- Connect the VCC (+) pin to 5V.
- Connect the Signal (S) pin to GPIO 15.
SCHEMATIC:
16x2 I2C LCD Display:
LCD VCC → 5V
LCD GND → GND
LCD SDA → GPIO 4 (Board SDA Pin)
LCD SCL → GPIO 5 (Board SCL Pin)
DS1302 RTC Module:
VCC → 3.3V
GND → GND
CLK → GPIO 8
DAT → GPIO 9
RST → GPIO 10
Rotary Encoder Module:
VCC → 3.3V
GND → GND
CLK → GPIO 11
DT → GPIO 12
RST → GPIO 13
DHT11 Temperature and Humidity Sensor:
VCC / + → 3.3V
GND / - → GND
Signal / S → GPIO 6
Buzzer Module:
VCC / (+) → 5V
GND / (-) → GND
Signal (S) → GPIO 15
Common Ground Connection:
4xAA Battery Pack (-) → PICUNO Board GND
CODE -- C:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 #include #include #include // --- State Machine & Global Variables --- enum Mode { MODE_CLOCK, MODE_WEATHER, MODE_SET_ALARM_HOUR, MODE_SET_ALARM_MINUTE }; Mode current_mode = MODE_CLOCK; int alarm_hour = 7, alarm_minute = 30; bool alarm_enabled = true; bool is_alarming = false; // --- Pin & Object Setup --- LiquidCrystal_I2C lcd(0x27, 16, 2); DHT dht(6, DHT11); const int BUZZER_PIN = 15; const int ENCODER_CLK_PIN = 11, ENCODER_DT_PIN = 12, ENCODER_SW_PIN = 13; int last_clk_state, last_switch_state; // --- Manual DS1302 RTC Control --- const int RTC_CLK_PIN = 8, RTC_DIO_PIN = 9, RTC_CS_PIN = 10; struct Time { int year, month, day, hour, minute, second; }; byte bcdToDec(byte val) { return ((val / 16 * 10) + (val % 16)); } void write_byte(byte data) { pinMode(RTC_DIO_PIN, OUTPUT); for (int i = 0; i < 8; i++) { digitalWrite(RTC_DIO_PIN, (data >> i) & 1); digitalWrite(RTC_CLK_PIN, HIGH); digitalWrite(RTC_CLK_PIN, LOW); } } byte read_byte() { byte val = 0; pinMode(RTC_DIO_PIN, INPUT); for (int i = 0; i < 8; i++) { val |= (digitalRead(RTC_DIO_PIN) << i); digitalWrite(RTC_CLK_PIN, HIGH); digitalWrite(RTC_CLK_PIN, LOW); } return val; } byte get_reg(byte reg) { digitalWrite(RTC_CS_PIN, HIGH); write_byte(reg); byte val = read_byte(); digitalWrite(RTC_CS_PIN, LOW); return val; } Time read_time() { Time t; t.second = bcdToDec(get_reg(0x81) & 0x7F); t.minute = bcdToDec(get_reg(0x83)); t.hour = bcdToDec(get_reg(0x85)); t.day = bcdToDec(get_reg(0x87)); t.month = bcdToDec(get_reg(0x89)); t.year = bcdToDec(get_reg(0x8D)) + 2000; return t; } void sound_alarm() { tone(BUZZER_PIN, 2000); } void stop_alarm() { noTone(BUZZER_PIN); } void update_display() { if (current_mode == MODE_CLOCK) { Time now = read_time(); lcd.clear(); lcd.setCursor(0, 0); lcd.print(now.day); lcd.print("/"); lcd.print(now.month); lcd.print("/"); lcd.print(now.year); lcd.setCursor(0, 1); lcd.print(now.hour); lcd.print(":"); lcd.print(now.minute); lcd.print(":"); lcd.print(now.second); } else if (current_mode == MODE_WEATHER) { float h = dht.readHumidity(); float t = dht.readTemperature(); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Temp: "); lcd.print(t); lcd.print("C"); lcd.setCursor(0, 1); lcd.print("Humi: "); lcd.print(h); lcd.print("%"); } else if (current_mode == MODE_SET_ALARM_HOUR || current_mode == MODE_SET_ALARM_MINUTE) { lcd.clear(); lcd.print("Set Alarm Time"); lcd.setCursor(0, 1); char buffer[17]; sprintf(buffer, " %02d:%02d", alarm_hour, alarm_minute); lcd.print(buffer); } } unsigned long last_display_update = 0, last_alarm_check = 0; void setup() { lcd.init(); lcd.backlight(); dht.begin(); pinMode(BUZZER_PIN, OUTPUT); pinMode(ENCODER_CLK_PIN, INPUT_PULLUP); pinMode(ENCODER_DT_PIN, INPUT_PULLUP); pinMode(ENCODER_SW_PIN, INPUT_PULLUP); pinMode(RTC_CLK_PIN, OUTPUT); pinMode(RTC_CS_PIN, OUTPUT); last_clk_state = digitalRead(ENCODER_CLK_PIN); last_switch_state = digitalRead(ENCODER_SW_PIN); update_display(); } void loop() { bool display_needs_update = false; if (is_alarming) { sound_alarm(); int current_switch_state = digitalRead(ENCODER_SW_PIN); if (current_switch_state == LOW && last_switch_state == HIGH) { is_alarming = false; alarm_enabled = false; stop_alarm(); current_mode = MODE_CLOCK; display_needs_update = true; } last_switch_state = current_switch_state; if (display_needs_update) update_display(); delay(100); return; } int current_switch_state = digitalRead(ENCODER_SW_PIN); if (current_switch_state == LOW && last_switch_state == HIGH) { delay(50); if (current_mode == MODE_SET_ALARM_HOUR) current_mode = MODE_SET_ALARM_MINUTE; else if (current_mode == MODE_SET_ALARM_MINUTE) { current_mode = MODE_CLOCK; alarm_enabled = true; } else current_mode = (Mode)((int)current_mode + 1); display_needs_update = true; } last_switch_state = current_switch_state; int current_clk_state = digitalRead(ENCODER_CLK_PIN); if (current_clk_state != last_clk_state && current_clk_state == LOW) { int change = (digitalRead(ENCODER_DT_PIN) != current_clk_state) ? 1 : -1; if (current_mode == MODE_SET_ALARM_HOUR) alarm_hour = (alarm_hour + change + 24) % 24; else if (current_mode == MODE_SET_ALARM_MINUTE) alarm_minute = (alarm_minute + change + 60) % 60; display_needs_update = true; } last_clk_state = current_clk_state; if (display_needs_update) update_display(); if (current_mode == MODE_SET_ALARM_HOUR || current_mode == MODE_SET_ALARM_MINUTE) { if (millis() % 1000 > 500) { lcd.setCursor((current_mode == MODE_SET_ALARM_HOUR ? 3 : 6), 1); lcd.print("__"); } else { char buffer[6]; sprintf(buffer, "%02d:%02d", alarm_hour, alarm_minute); lcd.setCursor(3, 1); lcd.print(buffer); } } if (millis() - last_display_update > 1000) { if (current_mode == MODE_CLOCK || current_mode == MODE_WEATHER) update_display(); last_display_update = millis(); } if (alarm_enabled && (millis() - last_alarm_check > 1000)) { Time now = read_time(); if (now.hour == alarm_hour && now.minute == alarm_minute && now.second == 0) { is_alarming = true; lcd.clear(); lcd.print("!! TIME'S UP !!"); lcd.setCursor(0, 1); lcd.print("Press to Stop"); } last_alarm_check = millis(); } delay(10); }
enum Mode - This creates a set of named integer constants (e.g., MODE_CLOCK, MODE_WEATHER). This is used to build a state machine, which makes the code for managing the different menus much cleaner and more readable than using simple numbers.
Manual RTC Functions (read_time, get_reg, etc.) - This group of functions replaces an external library. They handle the low-level, bit-by-bit communication required to send commands and read data directly from the DS1302 RTC chip.
Manual Encoder Logic - The code in the main loop() that checks digitalRead(ENCODER_CLK_PIN) and digitalRead(ENCODER_DT_PIN) is the manual "polling" method for the rotary encoder. It determines the direction of rotation by comparing the states of the two pins when a change is detected.
Helper Functions (update_display, sound_alarm, etc.) - The code is organized into logical blocks for different tasks. This makes the main loop() easier to read and separates the logic for displaying information from the logic for handling inputs.
Manual RTC Functions (read_time, get_reg, etc.) - This group of functions replaces an external library. They handle the low-level, bit-by-bit communication required to send commands and read data directly from the DS1302 RTC chip.
Manual Encoder Logic - The code in the main loop() that checks digitalRead(ENCODER_CLK_PIN) and digitalRead(ENCODER_DT_PIN) is the manual "polling" method for the rotary encoder. It determines the direction of rotation by comparing the states of the two pins when a change is detected.
Helper Functions (update_display, sound_alarm, etc.) - The code is organized into logical blocks for different tasks. This makes the main loop() easier to read and separates the logic for displaying information from the logic for handling inputs.
CODE -- PYTHON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 from machine import Pin, I2C, PWM, ADC from time import sleep_ms, ticks_ms, ticks_diff from i2c_lcd import I2cLcd import dht MODE_CLOCK = 0; MODE_WEATHER = 1; MODE_SET_ALARM_HOUR = 2; MODE_SET_ALARM_MINUTE = 3 current_mode = MODE_CLOCK alarm_hour = 7; alarm_minute = 30; alarm_enabled = True is_alarming = False i2c = I2C(0, scl=Pin(5), sda=Pin(4)) lcd = I2cLcd(i2c, 0x27, 2, 16) dht_sensor = dht.DHT11(Pin(6)) buzzer = PWM(Pin(15)) # --- Manual Rotary Encoder Setup --- encoder_clk = Pin(11, Pin.IN, Pin.PULL_UP) encoder_dt = Pin(12, Pin.IN, Pin.PULL_UP) encoder_sw = Pin(13, Pin.IN, Pin.PULL_UP) last_clk_state = encoder_clk.value() last_switch_state = encoder_sw.value() # --- Manual DS1302 RTC Control --- rtc_clk = Pin(8, Pin.OUT) rtc_cs = Pin(10, Pin.OUT) rtc_dio = Pin(9) def _write_byte(data): rtc_dio.init(Pin.OUT) for _ in range(8): rtc_dio.value((data & 1)); data >>= 1 rtc_clk.high(); rtc_clk.low() def _read_byte(): val = 0; rtc_dio.init(Pin.IN) for i in range(8): val |= (rtc_dio.value() << i) rtc_clk.high(); rtc_clk.low() return val def _get_reg(reg): rtc_cs.high(); _write_byte(reg); val = _read_byte(); rtc_cs.low() return val def _bcd_to_dec(bcd): return (bcd // 16) * 10 + (bcd % 16) def read_time(): t = [_get_reg(i) for i in [0x8d, 0x89, 0x87, 0x85, 0x83, 0x81]] return (_bcd_to_dec(t[0]) + 2000, _bcd_to_dec(t[1]), _bcd_to_dec(t[2]), _bcd_to_dec(t[3]), _bcd_to_dec(t[4]), _bcd_to_dec(t[5] & 0x7f)) # --- Helper Functions --- def sound_alarm(): buzzer.freq(3000) buzzer.duty_u16(32768) def stop_alarm(): buzzer.duty_u16(0) def update_display(): if current_mode == MODE_CLOCK: now = read_time() lcd.clear(); lcd.putstr(f"{now[2]:02d}/{now[1]:02d}/{now[0]}"); lcd.move_to(0, 1); lcd.putstr(f"{now[3]:02d}:{now[4]:02d}:{now[5]:02d}") elif current_mode == MODE_WEATHER: try: dht_sensor.measure(); t = dht_sensor.temperature(); h = dht_sensor.humidity() lcd.clear(); lcd.putstr(f"Temp: {t:.1f}C"); lcd.move_to(0,1); lcd.putstr(f"Humi: {h:.1f}%") except: lcd.clear(); lcd.putstr("DHT Error") elif current_mode == MODE_SET_ALARM_HOUR or current_mode == MODE_SET_ALARM_MINUTE: lcd.clear(); lcd.putstr("Set Alarm Time"); lcd.move_to(0,1); lcd.putstr(f" {alarm_hour:02d}:{alarm_minute:02d}") # --- Main Program --- last_display_update = 0 last_alarm_check = 0 print("Smart Alarm Clock Initialized.") update_display() while True: display_needs_update = False # --- Dedicated Alarm State --- if is_alarming: sound_alarm() current_switch_state = encoder_sw.value() if current_switch_state == 0 and last_switch_state == 1: is_alarming = False alarm_enabled = False stop_alarm() current_mode = MODE_CLOCK display_needs_update = True last_switch_state = current_switch_state if display_needs_update: update_display() sleep_ms(100) continue # --- 1. Handle Button Press --- current_switch_state = encoder_sw.value() if current_switch_state == 0 and last_switch_state == 1: sleep_ms(50) if current_mode == MODE_SET_ALARM_HOUR: current_mode = MODE_SET_ALARM_MINUTE elif current_mode == MODE_SET_ALARM_MINUTE: current_mode = MODE_CLOCK alarm_enabled = True # Re-arm the alarm for the next day print(f"Alarm set for {alarm_hour:02d}:{alarm_minute:02d}") else: current_mode = (current_mode + 1) % 3 display_needs_update = True last_switch_state = current_switch_state # --- 2. Handle Encoder Turning --- current_clk_state = encoder_clk.value() if current_clk_state != last_clk_state and current_clk_state == 0: if encoder_dt.value() != current_clk_state: change = 1 else: change = -1 if current_mode == MODE_SET_ALARM_HOUR: alarm_hour = (alarm_hour + change) % 24 elif current_mode == MODE_SET_ALARM_MINUTE: alarm_minute = (alarm_minute + change) % 60 display_needs_update = True last_clk_state = current_clk_state # --- 3. Update Display IF an input happened --- if display_needs_update: update_display() # Blinking cursor logic if current_mode == MODE_SET_ALARM_HOUR or current_mode == MODE_SET_ALARM_MINUTE: if ticks_ms() % 1000 > 500: if current_mode == MODE_SET_ALARM_HOUR: lcd.move_to(3,1); lcd.putstr("__") else: lcd.move_to(6,1); lcd.putstr("__") else: lcd.move_to(3,1); lcd.putstr(f"{alarm_hour:02d}") lcd.move_to(6,1); lcd.putstr(f"{alarm_minute:02d}") # --- 4. Timed Tasks --- if ticks_diff(ticks_ms(), last_display_update) > 1000: if current_mode == MODE_CLOCK or current_mode == MODE_WEATHER: update_display() last_display_update = ticks_ms() if alarm_enabled and ticks_diff(ticks_ms(), last_alarm_check) > 1000: now = read_time() if now[3] == alarm_hour and now[4] == alarm_minute and now[5] == 0: is_alarming = True # Trigger the new alarm state lcd.clear(); lcd.putstr("!! TIME'S UP !!"); lcd.move_to(0,1); lcd.putstr("Press to Stop") last_alarm_check = ticks_ms() sleep_ms(10)
Manual Control Functions - The code does not use external libraries for the RTC or Rotary Encoder. Instead, it includes low-level functions like _read_byte() and read_time() to communicate directly with the RTC chip, and it "polls" the encoder pins in the main loop to detect rotation.
State Machine (current_mode) - A global variable that tracks which mode the clock is in (displaying time, weather, or setting the alarm). The rotary encoder button is used to cycle through these states.
is_alarming flag - This boolean variable creates a special "alarm" state. When triggered, the main loop is trapped in a section that sounds the buzzer and waits for a button press, ignoring all other functions until the alarm is dismissed.
Non-Blocking Logic (ticks_ms) - Timers based on ticks_ms() are used to refresh the screen and check for the alarm condition once per second, without using long sleep() commands that would make the program unresponsive.
State Machine (current_mode) - A global variable that tracks which mode the clock is in (displaying time, weather, or setting the alarm). The rotary encoder button is used to cycle through these states.
is_alarming flag - This boolean variable creates a special "alarm" state. When triggered, the main loop is trapped in a section that sounds the buzzer and waits for a button press, ignoring all other functions until the alarm is dismissed.
Non-Blocking Logic (ticks_ms) - Timers based on ticks_ms() are used to refresh the screen and check for the alarm condition once per second, without using long sleep() commands that would make the program unresponsive.