Smart Alarm Clock

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.

CIRCUIT DIAGRAM:

Smart Alarm Clock
  • 16x2 I2C LCD Display:
    • 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.
  • DS1302 RTC Module:
    • 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.
  • Rotary Encoder Module:
    • 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.
  • DHT11 Temperature and Humidity Sensor:
    • 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.
  • Buzzer Module:
    • 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.

    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.