DS3231-based alarm clock

About this Post

In this article, I would like to introduce you to concepts for a DS3231-based alarm clock. I have already reported on the DS3231 real-time clock module in detail here, including the alarm function. I have now dedicated a separate article to the alarm clock, as it is a certain challenge to implement settings such as the time, the alarm time or the snooze function with just a few buttons. I will also show you how you can reduce the power requirement to such an extent that you can operate the alarm clock with batteries.

You will also learn about the Nokia 5110 display and the EA DOG display series. And finally, you will learn how to send an Arduino Pro Mini into deep sleep mode and wake it up again. All in one post!

Expectation management: This article is not a ready-made blueprint for an alarm clock with circuit board layout, 3D print template, etc. I have also refrained from defining the acoustic alarm. For me, it was about addressing the biggest stumbling blocks from my point of view. I’ll leave the rest to your creativity.

Preparation: the “basic clock” (version 1)

I will not go into detail about the DS3231 module in this article. Take a look here if you want to find out more. Nevertheless, to start with, we will begin by ‘warming up’ with the bare clock without any setting functions.

I am initially using a classic Arduino Nano to control the project. An I2C-controlled LCD is used as the display. You can replace the board and display relatively easily with your favorite devices – as long as we don’t go into battery operation.

I use Adafruit’s RTCLib library to control the DS3231 module. You can install it via the library manager of the Arduino IDE. The same applies to the LiquidCrystal_I2C library from John Rickman, which I use to control the LCD.

All components run on 5 volts and are controlled via I2C, which makes wiring simple:

Basic circuit for the DS3231 alarm clock
Alarm clock, version 1 – Basic circuit

I use the DS3231 module without a battery. Applying battery and external power supply is not a good idea (see my DS3231 article).

Here is the corresponding sketch:

#include <Wire.h>
#include <RTClib.h> // Adafruit library for the DS3231
#include <LiquidCrystal_I2C.h> // library for the LCD display

RTC_DS3231 rtc;  // create RTC_DS3231 object
LiquidCrystal_I2C lcd(0x27,20,2);  // create display object (address, columns, rows)

void setup () {
    Serial.begin(115200);

    lcd.init(); 
    lcd.backlight();
    lcd.clear();

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));  // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
}

void loop() {
    if(millis()%1000 == 0){ // update date/time every second
        printDateTime();
        delay(1);
    }
}

void printDateTime(){
    DateTime now = rtc.now();

    char datBuf[] = "DDD, DD.MM.YYYY"; // define the date format
    char timeBuf[] = "hh:mm:ss"; // define the time format
    Serial.println(now.toString(datBuf)); // print date
    Serial.println(now.toString(timeBuf));  // print time
    lcd.setCursor(0,0);
    lcd.print(datBuf);  // display date 
    lcd.setCursor(0,1);
    lcd.print(timeBuf);  // display time
}

 


The current time is set with rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));. This is the system time of your computer when the sketch is compiled. Since the upload takes some time, the clock is somewhat delayed.

Here is the result on the display:

Alarm clock: Display of date and time.
Output on the LCD

Alarm clock – Version 2 with two buttons

The operating concept

In version 2, we turn our clock into an alarm clock. The clock time and the alarm time are set using two buttons. Here is the circuit:

Circuit for the DS3231 alarm clock with two buttons.
Alarm clock, version 2 – circuit with two buttons

The buttons have dual functions:

  • Button 1 – “Item Key”:
    • Press for 1 second to enter the time setting mode.
    • In time setting mode, the time units year, month, day, hour and minute are set one after the other. Press briefly to accept the setting and move to the next time unit (‘Item’).
    • In the alarm setting mode, you can set the hour and minute and activate the alarm.
    • When the minutes are confirmed, the seconds are set to zero.
  • Button 2 – “Value Key”:
    • Press for 1 second to enter the alarm setting mode.
    • In the mode for setting the time and alarm clock, briefly pressing the button increases the setting value by 1 up to the maximum value. If the maximum value is exceeded, the setting value returns to the minimum value.

Time setting

Once again, in detail: If you press the “Item Key”, the display will ask you to do so for one second. When the following message appears, release it:

Time setting mode is started
Time setting mode is started

At first, if necessary, you can set the year. Pressing the “Value Key” increases the year by 1. Pressing it continuously increases the value in a continuous loop. After 2050, the value returns to 2025. Pressing the “Item Key” accepts the value. Then it’s the turn of the months, then the day and so on down to the minutes:

Time setting - minutes
Time setting mode: Example minutes

As the seconds are “zeroed” when the minutes are confirmed, you can set the time to the exact second.

Alarm setting

The DS3231 is very flexible when it comes to setting alarms (see my article on this). I have decided to only implement a daily alarm at a specific time. If you press the “Value Key” for one second, you enter the alarm setting mode:

Alarm setting mode is started
Alarm setting mode is started.

As in time setting mode, you increment the value with the “Value Key” and accept it with the “Item Key”. You do this down to the minutes:

Alarm setting mode: Example minutes
Alarm setting mode: Example minutes

The alarm time and the activation status then appear on the display:

Alarm setting mode: Completion
Alarm setting mode: Completion

If you now press the “Value Key”, the display changes from “Activate: no” to “Activate: yes”. Press the “Item Key” to complete the setting. The time appears on the display again, and the activated alarm is indicated by an “A” at the bottom right:

Date/time with activated alarm
Date/time with activated alarm

Alarm event

When the alarm goes off, the following display flashes:

Alarm was triggered
Alarm was triggered

Of course, just a flashing display would not wake you up. This is where you need to expand the concept accordingly, for example to wake you up with your favorite song. You could use an MP3 player module such as the DFPlayer Mini or the YX5300.

I have programmed the alarm to stop by itself after 60 seconds. If you are not at home and your alarm clock makes a lot of noise, your neighbors will be grateful for this function. You can extend the maximum alarm period as you wish (#define MAX_ALARM_DURATION).

If the maximum alarm time has not yet been exceeded, you can switch to snooze mode by briefly pressing one of the two buttons. I have preset the snooze time to one minute. It starts at the time you press the button. After the snooze time has elapsed, a new alarm goes off.

Snooze mode is indicated by an “S” on the display:

Display in snooze mode
Date/time in snooze mode

To switch off the alarm, go to the alarm settings mode and switch to the “Activate: no” setting.

The alarm clock sketch (version 2)

Here is the sketch for version 2:

#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <LiquidCrystal_I2C.h>  // library for the LCD display
#define SNOOZE_TIME 60  // set snooze time in seconds 
#define SET_INCREMENT_DELAY 260 // incrementation time for settings
#define MAX_ALARM_DURATION 60000  // maximum duration of an alarm in milliseconds
volatile bool setItemKeyPressed = false; // flag for item key
volatile bool setValueKeyPressed = false; // flag for value key
volatile unsigned long lastLow; // needed to handle bouncing
bool alarmOn = false;  // alarm activated?
const int setItemPin = 2; // choose item to set / time setting mode
const int setValuePin = 3; // set (increment) value / alarm setting mode

typedef enum TIME_UNIT { // needed for struct "timeElement"
    YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
} timeUnit;

typedef enum SET_MODE { // setting modes
    DATE_TIME, ALARM
} setMode;

struct timeElement{  // for date/time/alarm setting
    timeUnit tUnit;  
    uint16_t tMin; // minimum value
    uint16_t tMax; // maximum value
    char tname[7]; // name to display
};

timeElement tElementYear = {YEAR, 2025, 2050, "Year"};
timeElement tElementMonth = {MONTH, 1, 12, "Month"};
timeElement tElementDay = {DAY, 1, 31, "Day"};
timeElement tElementHour = {HOUR, 0, 23, "Hour"};
timeElement tElementMinute = {MINUTE, 0, 59, "Minute"};

RTC_DS3231 rtc;  //  // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);
LiquidCrystal_I2C lcd(0x27,20,2); // create display object (address, columns, rows)

void setup () {
    Serial.begin(115200);

    lcd.init(); 
    lcd.backlight();
    lcd.clear();

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K(); // stop signal at the 32K pin
    rtc.clearAlarm(1); // clear alarm
    rtc.clearAlarm(2);
    rtc.disableAlarm(1);  // disable alarm function
    rtc.disableAlarm(2);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    
    /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    pinMode(setItemPin, INPUT_PULLUP); 
    pinMode(setValuePin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING);
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void loop() {
    if(millis()%1000 == 0){ // update date/time every second
        printDateTime();
        delay(1); // avoids multiple updates per second
    }
    
    if(setItemKeyPressed){ // item key = date/time setting key
        goIntoSetMode(DATE_TIME);
    }

    if(setValueKeyPressed){  // value key = alarm key
        goIntoSetMode(ALARM); 
    }
    
    if(rtc.alarmFired(1) && alarmOn){
        alarmAction();   // act on alarm
    }
}

void goIntoSetMode(setMode settingMode){
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print(F("Press 1 sec"));
    delay(1000); // press 1 second to go into setting mode
    if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){
        lcd.setCursor(0,0);
        lcd.print(F("Setting Mode"));
        lcd.setCursor(0,1);
        if(settingMode == DATE_TIME){
            lcd.print(F("Date/Time"));
        }
        else{
            lcd.print(F("Alarm"));
        }
        /* wait until button is released */
        while(digitalRead(setItemPin) == LOW){}
        while(digitalRead(setValuePin) == LOW){}
        delay(50); // debouncing
        if(settingMode == DATE_TIME){
            setDateTime();
        }
        else{
            setAlarmTime();
        }
    }
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void setDateTime(){ // date/time setting procedure
    DateTime now = rtc.now();  // get current time
    uint16_t tItem = 0;
    tItem = setDateTimeElement(&tElementYear, now.year());
    adjustRTC(YEAR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMonth, now.month());
    adjustRTC(MONTH, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementDay, now.day());
    adjustRTC(DAY, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementHour, now.hour());
    adjustRTC(HOUR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMinute, now.minute());
    adjustRTC(MINUTE, tItem);
    adjustRTC(SECOND, 0);
    lcd.clear();
    if(alarmOn){
        lcd.setCursor(15,1);
        lcd.print("A"); // display "alarm is on" flag
    }
    printDateTime();
}

void setAlarmTime(){  // procedure to set an alarm
    uint16_t value = 0;
    value = setDateTimeElement(&tElementHour, alarmTime.hour());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0);
    value = setDateTimeElement(&tElementMinute, alarmTime.minute());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0);
    lcd.clear();
    char alarmTimeBuf[] = "hh:mm";
    alarmTime.toString(alarmTimeBuf);
    lcd.setCursor(0,0);
    lcd.print("Alarm: ");
    lcd.print(alarmTimeBuf);
    lcd.setCursor(0,1);
    alarmOn = false;
    lcd.print(F("Activate: no "));
    setItemKeyPressed = false;
    setValueKeyPressed = false;
    while(!setItemKeyPressed && !setValueKeyPressed){}
    if(setValueKeyPressed){
        setValueKeyPressed = false;
        while(!setItemKeyPressed){
            if(digitalRead(setValuePin) == LOW){
                if(alarmOn){
                    lcd.setCursor(10,1);
                    lcd.print("no ");
                    alarmOn = false;
                    rtc.disableAlarm(1);
                    delay(500); 
                }
                else{
                    lcd.setCursor(10,1);
                    lcd.print(F("yes "));
                    alarmOn = true;
                    rtc.setAlarm1(alarmTime, DS3231_A1_Hour);
                    delay(500);
                }
            }           
        }
    }
    lcd.clear();
    if(alarmOn){
        lcd.setCursor(15,1);
        lcd.print("A");
    }
    printDateTime();
}

void alarmAction(){
    lcd.clear();
    unsigned long alarmStart = millis();
    /* alarm was fired, not key pressed, and alarm did not exceed max. duration */
    while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){
        lcd.setCursor(5,0);
        lcd.print(F("Alarm!"));
        delay(500); 
        lcd.clear();
        delay(500);
        if(millis() < alarmStart) {   // if millis() overflow occured
            alarmStart = 0;
        }
    }
    /* wait till buttons are released: */
    while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){} 
    rtc.clearAlarm(1);
    delay(50); // debouncing
    DateTime now = rtc.now();
    if(millis() - alarmStart < MAX_ALARM_DURATION){
        alarmTime = now + TimeSpan(SNOOZE_TIME); // set new alarm (snooze)
        rtc.setAlarm1(alarmTime, DS3231_A1_Hour);
        lcd.setCursor(15,1);
        lcd.print("S"); // display snooze flag
    }
    else{
        alarmOn = false;
    }
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

/* This functions sets a time element. Pressing the value key increments the value until
   the maximum value is reached. Pressing the item key will return. */ 
uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){
    char buf[13];
    clearLineLCD(0);
    lcd.setCursor(0,0);
    sprintf(buf, "Set %s: ", tE->tname);
    lcd.print(buf);
    lcd.setCursor(12,0);
    sprintf(buf, "%02d", currentSet);
    lcd.print(buf);
    setItemKeyPressed = false;
    while(!setItemKeyPressed){    
        if(setValueKeyPressed){
            while(digitalRead(setValuePin) == LOW){
                currentSet++;
                if(currentSet > tE->tMax){
                    currentSet = tE->tMin;
                }
                lcd.setCursor(12,0);
                sprintf(buf, "%02d", currentSet);
                lcd.print(buf);
                delay(SET_INCREMENT_DELAY);                
            }
        }
    }
    setItemKeyPressed = false;
    return currentSet;
}

void printDateTime(){ // display date/time 
    DateTime now = rtc.now();

    char datBuf[] = "DDD, DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm:ss"; //  // define the time format
    Serial.println(now.toString(datBuf)); // print date
    Serial.println(now.toString(timeBuf)); // print time
    lcd.setCursor(0,0);
    lcd.print(datBuf); // display date 
    lcd.setCursor(0,1);
    lcd.print(timeBuf); // display time
}

void adjustRTC(timeUnit tU, uint16_t value){ // adjust year, month, day etc.
    DateTime now = rtc.now();
    switch(tU){
        case(YEAR):
            rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(MONTH):
            rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(DAY):
            rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second()));
            break;
        case(HOUR):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second()));
            break;
        case(MINUTE):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second()));
            break;
        case(SECOND):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value));
            break;
    }
    delay(10);
}

void clearLineLCD(int line){ // clears on line on the display
    char buf[17];
    sprintf(buf, "%16s", " ");
    lcd.setCursor(0, line);
    lcd.print(buf);
}

void setItemKeyISR(){ // ISR for pressed item key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setItemKeyPressed = true;
    }
    lastLow = millis();
}

void setValueKeyISR(){  // ISR for pressed value key
    if(millis()-lastLow >= 50){ // ignore bouncing when button is released
        setValueKeyPressed = true;
    }
    lastLow = millis();
}

 


Explanations of the code

I don’t want to go through the sketch line by line and hope that it is reasonably understandable with the comments in the sketch and the following notes.

  • The push-buttons are located on interrupt pins 2 and 3. Pressing the push-buttons triggers an interrupt. In the ISR, the query if(millis()-lastLow >= 300) ensures that bouncing is not evaluated as a button press when the button is released.
  • If a valid interrupt is triggered, the corresponding flags are set (setItemKeyPressed / setValueKeyPressed), which causes the setting function goIntoSetMode() to be called (provided the sketch is in loop() ).
  • from goIntoSetMode() it then goes to setDateTime() or setAlarmTime(). As the setting of years, months, days, hours and minutes always follows the same pattern, I have written the function setDateTimeElement() for this purpose. This function is passed a structure of the type timeElement, which specifies what the value is, its minimum and maximum and the designation on the display. Furthermore, the current value of the time element is passed and the value to be set is returned.
  • The value to be set is sent to the DS3231 using the function adjustRTC().

Alarm clock – Version 3 with two buttons and a slide switch

In version 2, I was annoyed by the process of switching off the alarm. I don’t like the idea of having to work my way through a setting procedure in the morning. That’s why, in version 3, I added a slide switch to Arduino pin 4 that turns the alarm on or off:

Alarm clock, version 3 - extended by a slide switch
Alarm clock, version 3 – extended by a slide switch

Nothing else changes in the operation. In the sketch, the corresponding item at the end of the alarm time setting has been removed. However, it is now necessary to check in loop() whether the alarm has been switched on or off. To do this, the status of pin 4 is compared with the alarmOn flag.


#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <LiquidCrystal_I2C.h> // library for the LCD display
#define SNOOZE_TIME 60  // set snooze time
#define SET_INCREMENT_DELAY 260 // incrementation time for settings
#define MAX_ALARM_DURATION 20000 // maximum duration of an alarm
volatile bool setItemKeyPressed = false; // flag for item key
volatile bool setValueKeyPressed = false; // flag for value key
volatile unsigned long lastLow; // needed to handle bouncing
bool alarmOn = false; // alarm activated
const int setItemPin = 2; // choose item to set / time setting mode
const int setValuePin = 3; // set (increment) value / alarm setting mode
const int alarmPin = 4;  // Pin for the alarm switch

typedef enum TIME_UNIT { // needed for struct "timeElement"
    YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
} timeUnit;

typedef enum SET_MODE { // setting modes
    DATE_TIME, ALARM
} setMode;

struct timeElement{ // for date/time/alarm setting
    timeUnit tUnit;
    uint16_t tMin; // minimum value
    uint16_t tMax; // maximum value
    char tname[7]; // name to display
};

timeElement tElementYear = {YEAR, 2025, 2050, "Year"};
timeElement tElementMonth = {MONTH, 1, 12, "Month"};
timeElement tElementDay = {DAY, 1, 31, "Day"};
timeElement tElementHour = {HOUR, 0, 23, "Hour"};
timeElement tElementMinute = {MINUTE, 0, 59, "Minute"};

RTC_DS3231 rtc;   // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);
LiquidCrystal_I2C lcd(0x27,20,2);  // create display object (address, columns, rows)

void setup () {
    Serial.begin(115200);

    lcd.init(); 
    lcd.backlight();
    lcd.clear();

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.clearAlarm(1);
    rtc.clearAlarm(2);
    rtc.disableAlarm(1);
    rtc.disableAlarm(2);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    
     /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    pinMode(setItemPin, INPUT_PULLUP);
    pinMode(setValuePin, INPUT_PULLUP);
    pinMode(alarmPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING);
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void loop() {
    if(millis()%1000 == 0){ // update date/time every second
        printDateTime();
        delay(1);
    }
    
    if(setItemKeyPressed){ // item key = date/time setting key
        goIntoSetMode(DATE_TIME);
    }

    if(setValueKeyPressed){ // value key = alarm key
        goIntoSetMode(ALARM); 
    }
    
    if(rtc.alarmFired(1) && alarmOn){
        alarmAction();   // act on alarm
    }

    checkAlarmSetting();  // check whether alarm was (de-)activated
    
}

void checkAlarmSetting(){
    if(!digitalRead(alarmPin) && !alarmOn){
        alarmOn = true;
        lcd.setCursor(15,1);
        lcd.print("A");  // display alarm flag
        rtc.setAlarm1(alarmTime, DS3231_A1_Hour);
    }
    else if (digitalRead(alarmPin) && alarmOn){
        alarmOn = false;
        lcd.setCursor(15,1);
        lcd.print(" ");  // delete alam flag
        rtc.disableAlarm(1);
    }
}

void goIntoSetMode(setMode settingMode){
    alarmOn = false;
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print(F("Press 1 sec"));
    delay(1000); // press 1 second to go into setting mode
    if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){
        lcd.setCursor(0,0);
        lcd.print(F("Setting Mode"));
        lcd.setCursor(0,1);
        if(settingMode == DATE_TIME){
            lcd.print(F("Date/Time"));
        }
        else{
            lcd.print(F("Alarm"));
        }
        /* wait until button is released */
        while(digitalRead(setItemPin) == LOW){}
        while(digitalRead(setValuePin) == LOW){}
        delay(50); // debouncing
        if(settingMode == DATE_TIME){
            setDateTime();
        }
        else{
            setAlarmTime();
        }
    }
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void setDateTime(){  // date/time setting procedure
    DateTime now = rtc.now(); // get current time
    uint16_t tItem = 0; 
    tItem = setDateTimeElement(&tElementYear, now.year());
    adjustRTC(YEAR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMonth, now.month());
    adjustRTC(MONTH, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementDay, now.day());
    adjustRTC(DAY, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementHour, now.hour());
    adjustRTC(HOUR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMinute, now.minute());
    adjustRTC(MINUTE, tItem);
    adjustRTC(SECOND, 0);
    lcd.clear();
    if(alarmOn){
        lcd.setCursor(15,1);
        lcd.print("A");  // display "alarm is on" flag
    }
    printDateTime();
}

void setAlarmTime(){  // procedure to set an alarm
    uint16_t value = 0;
    value = setDateTimeElement(&tElementHour, alarmTime.hour());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0);
    value = setDateTimeElement(&tElementMinute, alarmTime.minute());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0);
    lcd.clear();
    char alarmTimeBuf[] = "hh:mm";
    alarmTime.toString(alarmTimeBuf);
    lcd.setCursor(0,0);
    lcd.print("Alarm: ");
    lcd.print(alarmTimeBuf);
    delay(1000); // Show Alarm Time for one second
    lcd.clear();
    printDateTime();
}

void alarmAction(){
    lcd.clear();
    unsigned long alarmStart = millis();
    /* alarm was fired, not key pressed, and alarm did not exceed max. duration */
    while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){
        lcd.setCursor(5,0);
        lcd.print(F("Alarm!"));
        delay(500); 
        lcd.clear();
        delay(500);
        if(millis() < alarmStart) {   // if millis() overflow occured
            alarmStart = 0;
        }
    }
    /* wait till buttons are released: */
    while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){}
    rtc.clearAlarm(1);
    delay(50); // debouncing
    DateTime now = rtc.now();
    if(millis() - alarmStart < MAX_ALARM_DURATION){
        alarmTime = now + TimeSpan(SNOOZE_TIME);  // set new alarm (snooze)
        rtc.setAlarm1(alarmTime, DS3231_A1_Hour);
        lcd.setCursor(15,1);
        lcd.print("S"); // display snooze flag
    }
    else{
        alarmOn = false;
    }
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

/* This functions sets a time element. Pressing the value key increments the value until
   the maximum value is reached. Pressing the item key will return. */ 
uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){
    char buf[13];
    clearLineLCD(0);
    lcd.setCursor(0,0);
    sprintf(buf, "Set %s: ", tE->tname);
    lcd.print(buf);
    lcd.setCursor(12,0);
    sprintf(buf, "%02d", currentSet);
    lcd.print(buf);
    setItemKeyPressed = false;
    while(!setItemKeyPressed){    
        if(setValueKeyPressed){
            while(digitalRead(setValuePin) == LOW){
                currentSet++;
                if(currentSet > tE->tMax){
                    currentSet = tE->tMin;
                }
                lcd.setCursor(12,0);
                sprintf(buf, "%02d", currentSet);
                lcd.print(buf);
                delay(SET_INCREMENT_DELAY);                
            }
        }
    }
    setItemKeyPressed = false;
    return currentSet;
}

void printDateTime(){  // display date/time
    DateTime now = rtc.now();

    char datBuf[] = "DDD, DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm:ss";  // define the time format
    Serial.println(now.toString(datBuf));  // print date 
    Serial.println(now.toString(timeBuf));  // print time 
    lcd.setCursor(0,0);
    lcd.print(datBuf); // display date
    lcd.setCursor(0,1);
    lcd.print(timeBuf); // display time
}

void adjustRTC(timeUnit tU, uint16_t value){  // adjust year, month, day etc.
    DateTime now = rtc.now();
    switch(tU){
        case(YEAR):
            rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(MONTH):
            rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(DAY):
            rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second()));
            break;
        case(HOUR):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second()));
            break;
        case(MINUTE):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second()));
            break;
        case(SECOND):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value));
            break;
    }
    delay(10);
}

void clearLineLCD(int line){ // clears on line on the display
    char buf[17];
    sprintf(buf, "%16s", " ");
    lcd.setCursor(0, line);
    lcd.print(buf);
}

void setItemKeyISR(){ // ISR for pressed item key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setItemKeyPressed = true;
    }
    lastLow = millis();
}

void setValueKeyISR(){ // ISR for pressed value key
    if(millis()-lastLow >= 50){ // ignore bouncing when button is released
        setValueKeyPressed = true;
    }
    lastLow = millis();
}

 

Power consumption of versions 1 – 3

This alarm clock concept has a current consumption of ~44 mA. The display draws around 24 mA, the Nano around 16.5 mA and the DS3231 a good 3.5 mA. This is far too much for a battery power supply.

If you supply the DS3231 module with power via a button cell and cut the line to VCC, the module’s consumption is no significant. You could save approx. 20 mA by switching off the display’s backlight. To do this, you could insert the following lines into loop():

if(millis()-lastLow < 20000){
    lcd.backlight();
}
else lcd.noBacklight();

If you press one of the setting buttons, the backlight comes on for 20 seconds. This might suit you if the bright display is annoying at night. But admittedly, this is not ideal. At least during the day you would like to be able to read the display without having to press any buttons. And the remaining ~20 mA is still a killer for battery operation, unless you want to change batteries every few days.

Concept for battery operation

So what we need is a low-power microcontroller board and a low-power display.

Microcontroller board

As the board, I chose the 3.3 V / 8 MHz version of the Arduino Pro Mini. Like the classic Arduino Nano, the Pro Mini is based on the ATmega328P. In normal operation, the ATmega328P is not a world champion in power saving, but in deep sleep mode its consumption can be reduced to less than < 1 µA (see also my article on sleep modes here).  

To wake up every second, we use a pin change interrupt, which we trigger via a second alarm of the DS3231. The interrupts triggered via the push-buttons also wake up the Pro Mini.

Like almost all boards, the Pro Mini has an LED that lights up permanently when it is supplied with power. You should remove this. This can be done with a soldering iron or simply with some force, e.g. by carefully scratching it off with a screwdriver.

Display

During my research for energy-efficient displays, I found the following alternatives:

  • The Nokia 5110 display.
  • EA DOG Displays from Display Visions.
  • E-paper displays, e.g. from Waveshare.
  • ST7567 based display from OPEN-SMART.

In the following, I will discuss the Nokia 5110, the DOGS164 display from the EA DOGS series and the ST7567 based display. You find an example with an e-paper display (MH-ET LIVE) in the appendix, but only as a simple clock.

E-paper displays are the world champions when it comes to saving energy, but they do have certain peculiarities. Among other things, they require a lot of RAM and most do not have a backlight, which is not good for an alarm clock in the dark. Or you can build your own display lighting.

DS3231

The DS3231 module consumes very little power in battery mode (i.e. with a button cell in the module). However, I had problems waking up the Arduino Pro Mini with the alarm interrupt of the DS3231 in battery mode. I therefore switched back to power supply via VCC. As the power LED then lights up and consumes power, I simply removed it. This means that the DS3231 module consumes around 80 µA which I considered as acceptable.

Alarm clock – Version 4 with Nokia 5110 display

Nokia 5110 Display
Nokia 5110 display, 84 x 48 pixels

First of all, the bad news about the Nokia 5110 display: The quality of most displays leaves a lot to be desired. I have now ordered a whole series of displays from various stores. In Germany, I ordered three displays twice from a well-known store and both times two out of three did not work straight away. However, I was able to get three of the non-functioning displays to work. More on this in the appendix 1.

Then I made a few test purchases on AliExpress. Here I got three different versions. One version was the same as the one I had already bought in Germany – but I couldn’t get it to work at all. The other versions had other problems. More about this in appendix_2.

The good sides of this display (if it works!) are:

  • Very low power consumption (approx. 100 – 200 µA without illumination).
  • Its “nostalgic charm”.

The following setup was used:

Alarm clock, version 4 – with Nokia 5110 display

You can use the top or bottom connectors of the Nokia 5110 display pins, it doesn’t matter. I have connected them as follows:

  • RST (Reset) – Arduino pin 6
  • CE (Chip Enable) – Arduino Pin 7
  • DC (Data/Control) – Arduino Pin 8
  • DIN (Data In) – Arduino Pin 9
  • CLK (Clock) – Arduino pin 10
  • VCC – 3.3 Volt
  • LIGHT – Arduino pin 11
  • GND – GND
Nokia 5110 display, back
Nokia 5110 display, back

A few remarks

The backlight lights up (in this version!) when you connect the LIGHT pin to GND. Or you can connect the LIGHT pin to an I/O pin and switch the light on by setting the I/O pin to OUTPUT/LOW. Via PWM or analogWrite() you could dim it.

The Nokia 5110 display is incompatible with 5 volts. However, as we are using a 3.3 volt Arduino Pro Mini, we do not need a level shifter or voltage divider. A USB-to-TTL adapter, which can be set to 3.3 volts, is ideal for programming the Arduino Pro Mini. In standard operation, you can alternatively supply the circuit with higher voltages via the 3.3-volt voltage regulator of the Arduino Pro Mini. To do this, use the RAW pin. But once again: this only works in this form with the 3.3 volt version of the Pro Mini!

The alarm clock sketch – Version 4

I used the Nokia 5110 LCD library from Dimitris Platis(platisd) to control the Nokia 5110 display. I won’t go into the details here. A special feature of the library is that the cursor position is specified vertically in lines (0 to 5) and horizontally in pixels (0 to 83).

Then the hardware-specific sleep functions for the ATmega328P are added. If you want to know more details about this, take a look at this article. The pin change interrupt functions are also hardware-specific.

As alarm1 offers the option of triggering an alarm every second (DS3231_A1_PerSecond), we use it to wake up the Pro Mini regularly from sleep mode. We moved the alarm clock’s wake-up function to alarm 2.

I think that’s basically it. The code is simply too long to go through line by line.

#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <Nokia_LCD.h>  // Nokia 5110 display lib
#include "Bold_LCD_Fonts.h" // bold fonts for time display
#include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs
#define SNOOZE_TIME 60  // set snooze time
#define SET_INCREMENT_DELAY 260 // incrementation time for settings
#define MAX_ALARM_DURATION 20000 // maximum duration of an alarm
volatile bool setItemKeyPressed = false; // flag for item key
volatile bool setValueKeyPressed = false; // flag for value key
volatile unsigned long lastLow; // needed to handle bouncing
bool alarmOn = false; // alarm activated
const int setItemPin = 2; // choose item to set / time setting mode
const int setValuePin = 3; // set (increment) value / alarm setting mode
const int alarmPin = 4;  // Pin for the alarm switch
const int wakeUpPin = 5;
const int backlightPin = 11;

typedef enum TIME_UNIT { // needed for struct "timeElement"
    YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
} timeUnit;

typedef enum SET_MODE { // setting modes
    DATE_TIME, ALARM
} setMode;

char daysOfTheWeek[7][12] = {"Sunday   ", "Monday   ", "Tuesday  ", "Wednesday", "Thursday ", "Friday   ", "Saturday "};

struct timeElement{ // for date/time/alarm setting
    timeUnit tUnit;
    uint16_t tMin; // minimum value
    uint16_t tMax; // maximum value
    char tname[7]; // name to display
};

const LcdFont BoldFont{  // Bold Font for Nokia 5110 Display
    [](char c) {
        return Bold_LCD_Fonts::kFont_Table[c - 0x20];
    },                                       // method to retrieve the character
    Bold_LCD_Fonts::kColumns_per_character,  // width of each char
    Bold_LCD_Fonts::hSpace,                  // horizontal spacing array
    1                                        // size of horizontal spacing array
};

timeElement tElementYear = {YEAR, 2025, 2050, "Year"};
timeElement tElementMonth = {MONTH, 1, 12, "Month"};
timeElement tElementDay = {DAY, 1, 31, "Day"};
timeElement tElementHour = {HOUR, 0, 23, "Hour"};
timeElement tElementMinute = {MINUTE, 0, 59, "Minute"};

RTC_DS3231 rtc;   // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);
Nokia_LCD lcd(10 /* CLK */, 9 /* DIN */, 8 /* DC */, 7 /* CE */, 6 /* RST */);

void setup () {
    Serial.begin(115200);
    ADCSRA = 0; // ADC off (would consume current in deep sleep)

    lcd.begin(); 
    lcd.setContrast(60);
    lcd.clear(false);
    setBacklight(false); // Light off

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.clearAlarm(1);
    rtc.clearAlarm(2);
    rtc.disableAlarm(1);
    rtc.disableAlarm(2);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond);
    
     /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    pinMode(setItemPin, INPUT_PULLUP);
    pinMode(setValuePin, INPUT_PULLUP);
    pinMode(alarmPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING);
    PCICR = (1<<PCIE2);    // enable PCINT[23:16] interrupts
    PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void loop() {

    printDateTime();
        
    if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key
        goIntoSetMode(DATE_TIME);
    }

    if(digitalRead(setValuePin) == LOW){ // value key = alarm key
        goIntoSetMode(ALARM); 
    }
    
    if(rtc.alarmFired(2) && alarmOn){
        alarmAction();   // act on alarm
    }

    checkAlarmSetting();  // check whether alarm was (de-)activated

    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode
    sleep_mode(); // sleep now!
    rtc.clearAlarm(1);   
}

void checkAlarmSetting(){
    if(!digitalRead(alarmPin) && !alarmOn){
        alarmOn = true;
        lcd.setCursor(77,5);
        lcd.print("A");  // display alarm flag
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour);
    }
    else if (digitalRead(alarmPin) && alarmOn){
        alarmOn = false;
        lcd.setCursor(77,5);
        lcd.print(" ");  // delete alarm flag
        rtc.disableAlarm(2);
    }
}

void goIntoSetMode(setMode settingMode){
    alarmOn = false;
    lcd.clear(false);
    lcd.setCursor(0,0);
    lcd.print("Press 1 sec");
    delay(1000); // press 1 second to go into setting mode
    clearLineLCD(0);
    if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){
        lcd.setCursor(0,0);
        lcd.print("Setting Mode");
        lcd.setCursor(0,2);
        if(settingMode == DATE_TIME){
            lcd.print("Set Date/Time");
        }
        else{
            lcd.print("Set Alarm");
        }
        /* wait until button is released */
        while(digitalRead(setItemPin) == LOW){}
        while(digitalRead(setValuePin) == LOW){}
        delay(50); // debouncing
        if(settingMode == DATE_TIME){
            setDateTime();
        }
        else{
            setAlarmTime();
        }
    }
    rtc.clearAlarm(1);   
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void setDateTime(){  // date/time setting procedure
    DateTime now = rtc.now(); // get current time
    uint16_t tItem = 0; 
    tItem = setDateTimeElement(&tElementYear, now.year());
    adjustRTC(YEAR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMonth, now.month());
    adjustRTC(MONTH, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementDay, now.day());
    adjustRTC(DAY, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementHour, now.hour());
    adjustRTC(HOUR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMinute, now.minute());
    adjustRTC(MINUTE, tItem);
    adjustRTC(SECOND, 0);
    lcd.clear(false);
    if(alarmOn){
        lcd.setCursor(77,5);
        lcd.print("A");  // display "alarm is on" flag
    }
    printDateTime();
}

void setAlarmTime(){  // procedure to set an alarm
    uint16_t value = 0;
    value = setDateTimeElement(&tElementHour, alarmTime.hour());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0);
    value = setDateTimeElement(&tElementMinute, alarmTime.minute());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0);
    lcd.clear(false);
    char alarmTimeBuf[] = "hh:mm";
    alarmTime.toString(alarmTimeBuf);
    lcd.setCursor(0,0);
    lcd.print("Alarm: ");
    lcd.print(alarmTimeBuf);
    delay(1000); // Show Alarm Time for one second
    lcd.clear();
    printDateTime();
}

void alarmAction(){
    lcd.clear();
    unsigned long alarmStart = millis();
    /* alarm was fired, not key pressed, and alarm did not exceed max. duration */
    while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){
        lcd.setCursor(23,3);
        lcd.print("Alarm!");
        delay(500); 
        lcd.clear();
        delay(500);
        if(millis() < alarmStart) {   // if millis() overflow occurred
            alarmStart = 0;
        }

    }
    /* wait till buttons are released: */
    while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){}
    delay(50); // debouncing
    DateTime now = rtc.now();
    if(millis() - alarmStart < MAX_ALARM_DURATION){
        alarmTime = now + TimeSpan(SNOOZE_TIME);  // set new alarm (snooze)
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match
        lcd.setCursor(77,5);
        lcd.print("S"); // display snooze flag
    }
    else{
        alarmOn = false;
    }
    rtc.clearAlarm(2);
    rtc.clearAlarm(1);
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

/* This functions sets a time element. Pressing the value key increments the value until
   the maximum value is reached. Pressing the item key will return. */ 
uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){
    char buf[13];
    clearLineLCD(0);
    lcd.setCursor(0,0);
    sprintf(buf, "%s:", tE->tname);
    lcd.print(buf);
    lcd.setCursor(47,0);
    sprintf(buf, "%02d", currentSet);
    lcd.print(buf);
    setItemKeyPressed = false;
    while(!setItemKeyPressed){    
        if(setValueKeyPressed){
            while(digitalRead(setValuePin) == LOW){
                currentSet++;
                if(currentSet > tE->tMax){
                    currentSet = tE->tMin;
                }
                lcd.setCursor(47,0);
                sprintf(buf, "%02d", currentSet);
                lcd.print(buf);
                delay(SET_INCREMENT_DELAY);                
            }
        }
    }
    setItemKeyPressed = false;
    return currentSet;
}

void printDateTime(){  // display date/time
    DateTime now = rtc.now();

    char datBuf[] = "DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm:ss";  // define the time format
    Serial.println(now.toString(datBuf));  // print date 
    Serial.println(now.toString(timeBuf));  // print time 
    lcd.setCursor(0,0);
    lcd.print(daysOfTheWeek[now.dayOfTheWeek()]);
    lcd.setCursor(0,2);
    lcd.print(datBuf); // display date
    lcd.setCursor(0,4);
    lcd.setFont(&BoldFont);
    lcd.print(timeBuf); // display time
    lcd.setDefaultFont();
}

void adjustRTC(timeUnit tU, uint16_t value){  // adjust year, month, day etc.
    DateTime now = rtc.now();
    switch(tU){
        case(YEAR):
            rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(MONTH):
            rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(DAY):
            rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second()));
            break;
        case(HOUR):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second()));
            break;
        case(MINUTE):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second()));
            break;
        case(SECOND):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value));
            break;
    }
    delay(10);
}

void clearLineLCD(int line){ // clears on line on the display
    char buf[15];
    sprintf(buf, "%14s", " ");
    lcd.setCursor(0, line);
    lcd.print(buf);
}

void setBacklight(bool on){
    if(on){
        pinMode(backlightPin, OUTPUT);
    }
    else pinMode(backlightPin, INPUT);
}

void setItemKeyISR(){ // ISR for pressed item key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setItemKeyPressed = true;
    }
    lastLow = millis();
}

void setValueKeyISR(){ // ISR for pressed value key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setValueKeyPressed = true;
    }
    lastLow = millis();
}

ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD

 

The result

This is how the date and time appear on the display.

Alarm clock, version 4 - with Nokia 5110 display
Alarm clock, version 4 – with Nokia 5110 display

With this setup (including the removed LEDs), I measured a current consumption of 0.224 mA in the sleep phase. I have determined a power consumption of 2.8 mA for the waking phase. The waking phase takes 166 ms and is therefore quite relevant. The total current consumption is approx. 0.65 mA. You can move more in the direction of 0.224 mA by dispensing with the display of seconds. We’ll get to that shortly.

Variations

Minutes instead of seconds display

If you want to do without the seconds display, only a few changes are required in the sketch:

  • Change line 85 to: PCMSK2 = (1<<PCINT21) | (1<<PCINT20);
    • This will also wake up the Arduino Pro Mini when you press the slide switch on pin 4 (= PCINT20).
  • To change alarm 1 from seconds to minutes, replace line 75 with : rtc.setAlarm1(alarmTime, DS3231_A1_Second);
  • You define displaying only hours and minutes in line 272: char timeBuf[] = "hh:mm";
  • To center the time display, write in line 279: lcd.setCursor(23,4);
  • Then I noticed that the date/time display had disappeared after setting the clock and alarm time. To change this, insert an additional line after line 231: printDateTime();

Using the backlight

For alarm clock version 3, I have shown you how to switch on the backlight for a limited period. This does not work if you are using sleep mode, as the timer0 responsible for millis() is off. The pragmatic solution is to only count the waking time and reduce the value for the backlight period accordingly. However, you would then have to leave the light on for at least one minute if you choose the option to display only minutes.

Alternatively, you can use the RTC instead of millis() to measure the backlight period. With now.unixtime() you get the time since 1.1.1970 in seconds and can use it for convenient calculations. Or you can add another slide switch that activates the backlight and switches off the sleep function. However, there is a risk of forgetting to turn off the light, draining the battery and then oversleeping. But you could also install an upper limit here. Or, or, or… – there are many options you can choose from.

Alarm clock – Version 5 with DOGS164 display

The DOGS164 belongs to the EA DOG display family. They are based on an interesting modular concept. You put together the display, the backlight module and, if necessary, a touch panel according to your wishes. You can choose between various text or graphic displays. An overview is available here.

The letter after “DOG” gives you an indication of the (font) size, i.e. S, M, L or XL. For the text displays, the next two numbers indicate the number of characters per line. The third number tells you the number of lines.  

You can then select how the characters are to be shown on the display. For example, there is a DOGS164B, DOGS164W and a DOGS164N:

  • B: blue background, transmissive, i.e. the font color is determined by the backlight (i.e. similar appearance to the LCD used in clock versions 1-3).
  • W: black font, background color is determined by the backlight module.
  • N: black font, no backlight possible, looks like “W”-version without backlight (bottom right image).

The backlight panels are also available in different versions, e.g. in amber or green/red/white (switchable). So take a close look at what you order! The B version, for example, is only really legible with a backlight.

I opted for the DOGS164W text display with amber-colored backlight. I’ll show you the result in advance:  

Alarm clock, version 5 with DOGS164 display, left with backlight, right without
Alarm clock, version 5 with DOGS164 display, left with backlight, right without

A certain disadvantage of the EA DOG displays is that once they are fixed to the backlight panel, they can no longer be inserted into breadboards. The pins are then too short. I cut off jumper cables on one side and soldered them to the pins for my trials:


DOGS164W Display with backlight and connection cables
DOGS164W Display with backlight and connection cables

According to the data sheet, the backlight connections should be soldered “with a little tin from the top”. Well, it worked anyway.

Connection to the Arduino Pro Mini

The various EA DOG displays differ in terms of the number and assignment of their pins. With some displays, you have the choice between SPI (3-wire), SPI (4-wire) and I2C. The DOGS164 can be addressed via I2C or SPI (3-wire). As the DS3231 module is already connected via I2C, I did the same with the DOG display. This saved three Arduino pins compared to SPI.

DOGS164 pinout when using I2C (source: data sheet)
DOGS164 pinout when using I2C (source: data sheet)

Here is the circuit without connected backlight:

Alarm clock, version 5 - with DOGS164 display
Alarm clock, version 5 – with DOGS164 display

A few more tips (especially for the DOGS164 / I2C):

  • All connections only tolerate 3.3 volts. If you are using a 5V board, you must use level shifters or voltage dividers.
  • You can leave VOUT and CS unconnected.
  • Use SA0 to set the I2C address. The specification above is the 8-bit I2C address! In the Arduino world, the 7-bit address is used. The last bit must therefore be deleted. 0x7A becomes 0x3D and 0x78 becomes 0x3C.
  • Use IM1 to set I2C (GND) or SPI (VDD).
  • SID and SOD are connected SDA.
  • SCLK is attached to SCL.
  • The backlight is controlled via the pins A1/A2 (anode) and C1/C2 (cathode).
    • Series resistors are mandatory! The backlight current can exceed the limit of the Arduino pins. You could control the backlight via MOSFETs with the Arduino.
  • Read the data sheet! It is easy to understand and available in German / English. It also contains the recommended resistor sizes for the backlight.

The alarm clock sketch – Version 5

I used the SSD1803A_I2C library from Stefan Staub(sstaub) to control the DOGS164 display. It is convenient to use. The code should be easy to understand. Otherwise, everything remains the same.

#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <SSD1803A_I2C.h> // DOGS164 Lib
#include <avr/sleep.h>
#define SNOOZE_TIME 60  // set snooze time
#define SET_INCREMENT_DELAY 260 // incrementation time for settings
#define MAX_ALARM_DURATION 20000 // maximum duration of an alarm
volatile bool setItemKeyPressed = false; // flag for item key
volatile bool setValueKeyPressed = false; // flag for value key
volatile unsigned long lastLow; // needed to handle bouncing
bool alarmOn = false; // alarm activated
const int setItemPin = 2; // choose item to set / time setting mode
const int setValuePin = 3; // set (increment) value / alarm setting mode
const int alarmPin = 4;  // Pin for the alarm switch
const int wakeUpPin = 5;
const uint8_t i2caddr = 0x3D;
const uint8_t resetPin = 8;


typedef enum TIME_UNIT { // needed for struct "timeElement"
    YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
} timeUnit;

typedef enum SET_MODE { // setting modes
    DATE_TIME, ALARM
} setMode;

char daysOfTheWeek[7][12] = {"Sunday   ", "Monday   ", "Tuesday  ", "Wednesday", "Thursday ", "Friday   ", "Saturday "};

struct timeElement{ // for date/time/alarm setting
    timeUnit tUnit;
    uint16_t tMin; // minimum value
    uint16_t tMax; // maximum value
    char tname[7]; // name to display
};

timeElement tElementYear = {YEAR, 2025, 2050, "Year"};
timeElement tElementMonth = {MONTH, 1, 12, "Month"};
timeElement tElementDay = {DAY, 1, 31, "Day"};
timeElement tElementHour = {HOUR, 0, 23, "Hour"};
timeElement tElementMinute = {MINUTE, 0, 59, "Minute"};

RTC_DS3231 rtc;   // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);
SSD1803A_I2C lcd(i2caddr);

void setup () {
    Serial.begin(115200);
    ADCSRA = 0; // ADC off

    lcd.begin(DOGS164, resetPin);

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.clearAlarm(1);
    rtc.clearAlarm(2);
    rtc.disableAlarm(1);
    rtc.disableAlarm(2);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond);
    
     /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    pinMode(setItemPin, INPUT_PULLUP);
    pinMode(setValuePin, INPUT_PULLUP);
    pinMode(alarmPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING);
    PCICR = (1<<PCIE2);    // enable PCINT[23:16] interrupts
    PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void loop() {
    
    printDateTime();
        
    if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key
        goIntoSetMode(DATE_TIME);
    }

    if(digitalRead(setValuePin) == LOW){ // value key = alarm key
        goIntoSetMode(ALARM); 
    }
    
    if(rtc.alarmFired(2) && alarmOn){
        alarmAction();   // act on alarm
    }

    checkAlarmSetting();  // check whether alarm was (de-)activated

    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode
    sleep_mode(); // sleep now!
    rtc.clearAlarm(1);   
}

void checkAlarmSetting(){
    if(!digitalRead(alarmPin) && !alarmOn){
        alarmOn = true;
        lcd.locate(4,16);
        lcd.print("A");  // display alarm flag
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour);
    }
    else if (digitalRead(alarmPin) && alarmOn){
        alarmOn = false;
        lcd.locate(4,16);
        lcd.print(" ");  // delete alam flag
        rtc.disableAlarm(2);
    }
}

void goIntoSetMode(setMode settingMode){
    alarmOn = false;
    lcd.cls();
    lcd.locate(1,1);
    lcd.print("Press 1 sec");
    delay(1000); // press 1 second to go into setting mode
    lcd.clr(1);
    if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){
        lcd.locate(1,1);
        lcd.print("Setting Mode");
        lcd.locate(2,1);
        if(settingMode == DATE_TIME){
            lcd.print("Set Date/Time");
        }
        else{
            lcd.print("Set Alarm");
        }
        /* wait until button is released */
        while(digitalRead(setItemPin) == LOW){}
        while(digitalRead(setValuePin) == LOW){}
        delay(50); // debouncing
        if(settingMode == DATE_TIME){
            setDateTime();
        }
        else{
            setAlarmTime();
        }
    }
    rtc.clearAlarm(1);   
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void setDateTime(){  // date/time setting procedure
    DateTime now = rtc.now(); // get current time
    uint16_t tItem = 0; 
    tItem = setDateTimeElement(&tElementYear, now.year());
    adjustRTC(YEAR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMonth, now.month());
    adjustRTC(MONTH, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementDay, now.day());
    adjustRTC(DAY, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementHour, now.hour());
    adjustRTC(HOUR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMinute, now.minute());
    adjustRTC(MINUTE, tItem);
    adjustRTC(SECOND, 0);
    lcd.cls();
    if(alarmOn){
        lcd.locate(4,16);
        lcd.print("A");  // display "alarm is on" flag
    }
    printDateTime();
}

void setAlarmTime(){  // procedure to set an alarm
    uint16_t value = 0;
    value = setDateTimeElement(&tElementHour, alarmTime.hour());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0);
    value = setDateTimeElement(&tElementMinute, alarmTime.minute());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0);
    lcd.cls();
    char alarmTimeBuf[] = "hh:mm";
    alarmTime.toString(alarmTimeBuf);
    lcd.locate(1,1);
    lcd.print("Alarm: ");
    lcd.print(alarmTimeBuf);
    delay(1000); // Show Alarm Time for one second
    lcd.cls();
    printDateTime();
}

void alarmAction(){
    lcd.cls();
    unsigned long alarmStart = millis();
    /* alarm was fired, not key pressed, and alarm did not exceed max. duration */
    while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){
        lcd.locate(2,5);
        lcd.print("Alarm!");
        delay(500); 
        lcd.cls();
        delay(500);
        if(millis() < alarmStart) {   // if millis() overflow occured
            alarmStart = 0;
        }

    }
    /* wait till buttons are released: */
    while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){}
    delay(50); // debouncing
    DateTime now = rtc.now();
    if(millis() - alarmStart < MAX_ALARM_DURATION){
        alarmTime = now + TimeSpan(SNOOZE_TIME);  // set new alarm (snooze)
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match
        lcd.locate(4,16);
        lcd.print("S"); // display snooze flag
    }
    else{
        alarmOn = false;
    }
    rtc.clearAlarm(2);
    rtc.clearAlarm(1);
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

/* This functions sets a time element. Pressing the value key increments the value until
   the maximum value is reached. Pressing the item key will return. */ 
uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){
    char buf[13];
    lcd.clr(1);
    lcd.locate(1,1);
    sprintf(buf, "%s:", tE->tname);
    lcd.print(buf);
    lcd.locate(1,9);
    sprintf(buf, "%02d", currentSet);
    lcd.print(buf);
    setItemKeyPressed = false;
    while(!setItemKeyPressed){    
        if(setValueKeyPressed){
            while(digitalRead(setValuePin) == LOW){
                currentSet++;
                if(currentSet > tE->tMax){
                    currentSet = tE->tMin;
                }
                lcd.locate(1,9);
                sprintf(buf, "%02d", currentSet);
                lcd.print(buf);
                delay(SET_INCREMENT_DELAY);                
            }
        }
    }
    setItemKeyPressed = false;
    return currentSet;
}

void printDateTime(){  // display date/time
    DateTime now = rtc.now();

    char datBuf[] = "DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm:ss";  // define the time format
    Serial.println(now.toString(datBuf));  // print date 
    Serial.println(now.toString(timeBuf));  // print time 
    lcd.locate(1,4);
    lcd.print(daysOfTheWeek[now.dayOfTheWeek()]);
    lcd.locate(2,4);
    lcd.print(datBuf); // display date
    lcd.locate(4,4);
    lcd.print(timeBuf); // display time
}

void adjustRTC(timeUnit tU, uint16_t value){  // adjust year, month, day etc.
    DateTime now = rtc.now();
    switch(tU){
        case(YEAR):
            rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(MONTH):
            rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(DAY):
            rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second()));
            break;
        case(HOUR):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second()));
            break;
        case(MINUTE):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second()));
            break;
        case(SECOND):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value));
            break;
    }
    delay(10);
}

void setItemKeyISR(){ // ISR for pressed item key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setItemKeyPressed = true;
    }
    lastLow = millis();
}

void setValueKeyISR(){ // ISR for pressed value key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setValueKeyPressed = true;
    }
    lastLow = millis();
}

ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD

 

The result

I measured a total current of approx. 0.85 milliamperes. This is slightly more than with the Nokia display, but still within a range that makes battery operation possible. According to the manufacturer, the display itself draws 440 µA, according to my measurement it is 560 µA.

The EA DOG display appears to be much faster than the Nokia 5110, with a waking phase of just 8 ms. In this respect, it is not really worth switching from displaying seconds to minutes only.

Alarm clock -Version 6 with ST7567 based display

I found the ST7567-based display from OPEN-SMART on AliExpress. It is representative of other LCDs that work without a backlight. I ordered two and both worked(!). I’ll give you the result straight away:

Alarm clock -Version 6 with ST7567 based display
Alarm clock -Version 6 with ST7567 based display

I won’t provide a Fritzing scheme for the circuit here. It should be clear from the previous examples and the sketch. It was controlled via the U8x8 library, which is part of the U8g2 library from olikraus. With it, I achieved a current consumption of approx. 0.8 mA in the sleep phase.

To provide a little variety, I have added a function to the sketch that switches on the backlight for 10 seconds (= 10x wake-up) when one of the push-buttons is pressed:

#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <U8x8lib.h>
#include <SPI.h>
#include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs
#define SNOOZE_TIME 60  // set snooze time
#define SET_INCREMENT_DELAY 260 // incrementation time for settings
#define MAX_ALARM_DURATION 20000 // maximum duration of an alarm
#define BACKLIGHT_ON_PERIOD 10 // backlight on period in seconds
volatile bool setItemKeyPressed = false; // flag for item key
volatile bool setValueKeyPressed = false; // flag for value key
volatile unsigned long lastLow; // needed to handle bouncing
unsigned long wakeUpNo = 0;
unsigned long int lastKeyPress = 0;
bool alarmOn = false; // alarm activated
const int setItemPin = 2; // choose item to set / time setting mode
const int setValuePin = 3; // set (increment) value / alarm setting mode
const int alarmPin = 4;  // Pin for the alarm switch
const int wakeUpPin = 5;
const int backlightPin = 6;

typedef enum TIME_UNIT { // needed for struct "timeElement"
    YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
} timeUnit;

typedef enum SET_MODE { // setting modes
    DATE_TIME, ALARM
} setMode;

char daysOfTheWeek[7][12] = {"SUNDAY   ", "MONDAY   ", "TUESDAY  ", "WEDNESDAY", "THURSDAY ", "FRIDAY   ", "SATURDAY "};

struct timeElement{ // for date/time/alarm setting
    timeUnit tUnit;
    uint16_t tMin; // minimum value
    uint16_t tMax; // maximum value
    char tname[7]; // name to display
};

timeElement tElementYear = {YEAR, 2025, 2050, "YEAR"};
timeElement tElementMonth = {MONTH, 1, 12, "MONTH"};
timeElement tElementDay = {DAY, 1, 31, "DAY"};
timeElement tElementHour = {HOUR, 0, 23, "HOUR"};
timeElement tElementMinute = {MINUTE, 0, 59, "MINUTE"};

RTC_DS3231 rtc;   // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);
U8X8_ST7567_JLX12864_4W_HW_SPI u8x8(/* cs=*/ 7, /* dc=*/ 9, /* reset=*/ 8); 

void setup () {
    Serial.begin(115200);
    ADCSRA = 0; // ADC off (would consume current in deep sleep)

    u8x8.begin();
    u8x8.setPowerSave(0);
    pinMode(backlightPin, OUTPUT);
    u8x8.setFont(u8x8_font_saikyosansbold8_u);
    setBacklight(true); // Light on

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.clearAlarm(1);
    rtc.clearAlarm(2);
    rtc.disableAlarm(1);
    rtc.disableAlarm(2);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    rtc.setAlarm1(alarmTime, DS3231_A1_PerSecond);
    
     /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    pinMode(setItemPin, INPUT_PULLUP);
    pinMode(setValuePin, INPUT_PULLUP);
    pinMode(alarmPin, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(setItemPin), setItemKeyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(setValuePin), setValueKeyISR, FALLING);
    PCICR = (1<<PCIE2);    // enable PCINT[23:16] interrupts
    PCMSK2 = (1<<PCINT21); // D5 = PCINT21 for wake up
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void loop() {

    printDateTime();
        
    if(digitalRead(setItemPin) == LOW){ // item key = date/time setting key
        goIntoSetMode(DATE_TIME);
        lastKeyPress = wakeUpNo; 
    }

    if(digitalRead(setValuePin) == LOW){ // value key = alarm key
        goIntoSetMode(ALARM);
        lastKeyPress = wakeUpNo;
    }
    
    if(rtc.alarmFired(2) && alarmOn){
        alarmAction();   // act on alarm
    }

    checkAlarmSetting();  // check whether alarm was (de-)activated

    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode
    sleep_mode(); // sleep now!
    rtc.clearAlarm(1); 
    
    wakeUpNo++;
    backlightCheck();  
}

void checkAlarmSetting(){
    if(!digitalRead(alarmPin) && !alarmOn){
        alarmOn = true;
        u8x8.drawString(15,0,"A"); // display alarm flag
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour);
    }
    else if (digitalRead(alarmPin) && alarmOn){
        alarmOn = false;
        u8x8.drawString(15,0," "); // delete alarm flag
        rtc.disableAlarm(2);
    }
}

void goIntoSetMode(setMode settingMode){
    alarmOn = false;
    u8x8.clear();
    u8x8.drawString(0,0,"PRESS 1 SEC");
    delay(1000); // press 1 second to go into setting mode
    u8x8.clearLine(0);
    if (digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){
        u8x8.drawString(0,0,"SETTING MODE");
        if(settingMode == DATE_TIME){
            u8x8.drawString(0,2,"SET DATE/TIME");
        }
        else{
            u8x8.drawString(0,2,"SET ALARM");
        }
        /* wait until button is released */
        while(digitalRead(setItemPin) == LOW){}
        while(digitalRead(setValuePin) == LOW){}
        delay(50); // debouncing
        if(settingMode == DATE_TIME){
            setDateTime();
        }
        else{
            setAlarmTime();
        }
    }
    rtc.clearAlarm(1);   
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

void setDateTime(){  // date/time setting procedure
    DateTime now = rtc.now(); // get current time
    uint16_t tItem = 0; 
    tItem = setDateTimeElement(&tElementYear, now.year());
    adjustRTC(YEAR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMonth, now.month());
    adjustRTC(MONTH, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementDay, now.day());
    adjustRTC(DAY, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementHour, now.hour());
    adjustRTC(HOUR, tItem);
    now = rtc.now();
    tItem = setDateTimeElement(&tElementMinute, now.minute());
    adjustRTC(MINUTE, tItem);
    adjustRTC(SECOND, 0);
    u8x8.clear();
    if(alarmOn){
        u8x8.drawString(15,0,"A"); // display "alarm is on" flag
    }
    printDateTime();
}

void setAlarmTime(){  // procedure to set an alarm
    uint16_t value = 0;
    value = setDateTimeElement(&tElementHour, alarmTime.hour());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), value, alarmTime.minute(), 0);
    value = setDateTimeElement(&tElementMinute, alarmTime.minute());
    alarmTime = DateTime(alarmTime.year(), alarmTime.month(), alarmTime.day(), alarmTime.hour(), value, 0);
    u8x8.clear();
    char alarmTimeBuf[] = "hh:mm";
    alarmTime.toString(alarmTimeBuf);
    u8x8.drawString(0,0,"ALARM: ");
    u8x8.drawString(8,0, alarmTimeBuf);
    delay(1000); // Show Alarm Time for one second
    u8x8.clear();
    printDateTime();
}

void alarmAction(){
    u8x8.clear();
    unsigned long alarmStart = millis();
    /* alarm was fired, not key pressed, and alarm did not exceed max. duration */
    while(!setItemKeyPressed && !setValueKeyPressed && (millis() - alarmStart < MAX_ALARM_DURATION)){
        u8x8.drawString(4,3,"ALARM!");
        delay(500); 
        u8x8.clear();
        delay(500);
        if(millis() < alarmStart) {   // if millis() overflow occurred
            alarmStart = 0;
        }

    }
    /* wait till buttons are released: */
    while(digitalRead(setItemPin) == LOW || digitalRead(setValuePin) == LOW){}
    delay(50); // debouncing
    DateTime now = rtc.now();
    if(millis() - alarmStart < MAX_ALARM_DURATION){
        alarmTime = now + TimeSpan(SNOOZE_TIME);  // set new alarm (snooze)
        rtc.setAlarm2(alarmTime, DS3231_A2_Hour); // alarm if hours/minutes match
        u8x8.drawString(15,0,"S"); // display snooze flag
    }
    else{
        alarmOn = false;
    }
    rtc.clearAlarm(2);
    rtc.clearAlarm(1);
    setItemKeyPressed = false;
    setValueKeyPressed = false;
}

/* This functions sets a time element. Pressing the value key increments the value until
   the maximum value is reached. Pressing the item key will return. */ 
uint16_t setDateTimeElement(timeElement *tE, uint16_t currentSet){
    char buf[13];
    u8x8.clearLine(0);
    u8x8.setCursor(0,0);
    sprintf(buf, "%s:", tE->tname);
    u8x8.drawString(0,0,buf);
    sprintf(buf, "%02d", currentSet);
    u8x8.drawString(8,0,buf);
    setItemKeyPressed = false;
    while(!setItemKeyPressed){    
        if(setValueKeyPressed){
            while(digitalRead(setValuePin) == LOW){
                currentSet++;
                if(currentSet > tE->tMax){
                    currentSet = tE->tMin;
                }
                sprintf(buf, "%02d", currentSet);
                u8x8.drawString(8,0,buf);
                delay(SET_INCREMENT_DELAY);                
            }
        }
    }
    setItemKeyPressed = false;
    return currentSet;
}

void printDateTime(){  // display date/time
    DateTime now = rtc.now();

    char datBuf[] = "DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm:ss";  // define the time format
    Serial.println(now.toString(datBuf));  // print date 
    Serial.println(now.toString(timeBuf));  // print time 
    
    u8x8.drawString(0,0,daysOfTheWeek[now.dayOfTheWeek()]);
    u8x8.drawString(0,2,datBuf); // display date
    u8x8.setFont(u8x8_font_courB18_2x3_f);
    u8x8.drawString(0,4,timeBuf); // display time
    u8x8.setFont(u8x8_font_saikyosansbold8_u);
}

void adjustRTC(timeUnit tU, uint16_t value){  // adjust year, month, day etc.
    DateTime now = rtc.now();
    switch(tU){
        case(YEAR):
            rtc.adjust(DateTime(value, now.month(), now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(MONTH):
            rtc.adjust(DateTime(now.year(), value, now.day(), now.hour(), now.minute(), now.second()));
            break;
        case(DAY):
            rtc.adjust(DateTime(now.year(), now.month(), value, now.hour(), now.minute(), now.second()));
            break;
        case(HOUR):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), value, now.minute(), now.second()));
            break;
        case(MINUTE):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), value, now.second()));
            break;
        case(SECOND):
            rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), value));
            break;
    }
    delay(10);
}

void setBacklight(bool on){
    if(on){
        digitalWrite(backlightPin, LOW);
    }
    else digitalWrite(backlightPin, HIGH);
}

void backlightCheck(){
    if(wakeUpNo - lastKeyPress < BACKLIGHT_ON_PERIOD){
        setBacklight(true);
    }
    else setBacklight(false);
}

void setItemKeyISR(){ // ISR for pressed item key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setItemKeyPressed = true;
    }
    lastLow = millis();
}

void setValueKeyISR(){ // ISR for pressed value key
    if(millis()-lastLow >= 300){ // ignore bouncing when button is released
        setValueKeyPressed = true;
    }
    lastLow = millis();
}

ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD

 

Transfer of the concept to other microcontrollers

If you want to transfer the alarm clock concept in battery mode to other microcontrollers or boards, you first need to familiarize yourself with the available sleep modes, their power consumption and the wake-up methods.

Let’s take the ESP32 as an example. This is basically a real power guzzler. In deep sleep, however, some ESP32 boards (e.g. Adafruit ESP32 Feather V2 or FireBeetle) consume very little current. The timer, external interrupts or touch pins are available as wake-up methods. So, enough options are available.

However, the ESP32 performs a restart when it wakes up from deep sleep. In other words, it has forgotten everything if you do not take any further action. You can query the time and alarm time in the DS3231. But how is the ESP32 supposed to know that the alarm clock is in snooze mode, for example? This could be written to the RTC memory or the flash (e.g. via the EEPROM function).  

So, there is a solution for everything. But the point is: changing the microcontroller can be a challenge.

Appendix 1: Repairing Nokia 5110 displays

If your Nokia display does not show anything and you have ruled out any wiring or code problems, then it could be due to contact problems with the elastomeric connector. What looks like a damper or spacer is an important electrical component that establishes contact between the circuit board and the display.

To access the rubber, detach the display from the circuit board by bending the four retaining clips outwards. The rubber is located at the top edge. Clean the rubber and the contact points with alcohol and reassemble the display. In my experience, there is a certain chance that it will then work.

"Repair" of the Nokia 5110 display
“Repair” of the Nokia 5110 display, right: fit of the elastomeric connector

Appendix 2: Alternative versions of the Nokia 5110 display

There are (at least) two other versions of the display. Here is another red one, which differs first of all in the pinout:

Alternative version of the Noia 5110 display.
Alternative version of the Noia 5110 display.

The pins are basically the same, but the order is different. In addition, the backlight lights up when you connect it to VCC. Now to the problem with this version: With a low contrast setting (setContrast(30)), the display was easy to read. However, when the backlight was switched on, everything was blurred. At a higher contrast (setContrast(60)) the display was readable with backlight, but without backlight all pixels were so dark that nothing could be recognized.

Nokia 5110 display - poor quality
Nokia 5110 display – poor quality

And finally, I tested this blue module:

Nokia 5110 Display - blue version
Nokia 5110 Display – blue version

Apart from the backlight, nothing worked! Very unsatisfactory.

Appendix 3: A simple clock using an e-paper display

As already mentioned, e-paper displays are somewhat unique. They should be used for applications where the screen content does not change too frequently. In addition, larger microcontrollers should be used due to the high flash and SRAM requirements. However, they then stand out due to their excellent display and low power consumption. Here is the result of the simple clock on a 1.54-inch display from MH-ET LIVE:

Clock using an e-paper display
Clock using an e-paper display

Here is the circuit (with 3.3 V version of the Arduino Pro Mini!):

Circuit for the e-paper-based clock
Circuit for the e-paper-based clock

My example sketch below occupied 29254 bytes of the flash, which is pretty tight. The whole thing only works on the Pro Mini with “Paged Writing”. This means that the screen content is rendered in one goo, but piece-wise. It works, but not quite ideally. Incidentally, the power consumption was 0.32 mA in the sleep phase.

I will refrain from further explanations here, but only present the sketch. Maybe I’ll publish a separate post for e-paper displays sometime. 

I used the GxEPD2 library from Jean-Marc Zingg to control the display.

#include <Wire.h>
#include <RTClib.h>  // library for the DS3231
#include <avr/sleep.h> // hardware-specific sleep library for AVR MCUs
#include <GxEPD2_BW.h>
#include <GxEPD2_3C.h>
#include <GxEPD2_4C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeMonoBold24pt7b.h>
#include "GxEPD2_display_selection_new_style.h"
#define COMPILE_DURATION 40 // to 
const int wakeUpPin = 5;  char daysOfTheWeek[7][12] = {"Sunday   ", "Monday   ", "Tuesday  ", "Wednesday", "Thursday ", "Friday   ", "Saturday "};

RTC_DS3231 rtc;   // create RTC_DS3231 object
DateTime alarmTime = DateTime(2014, 1, 1, 7, 0, 0);

void setup () {
    Serial.begin(115200);
    ADCSRA = 0; // ADC off (would consume current in deep sleep)

    display.init(115200); // default 10ms reset pulse, e.g. for bare panels with DESPI-C02
    //display.init(115200, true, 2, false); // USE THIS for Waveshare boards with "clever" reset circuit, 2ms reset pulse
    display.setRotation(1);
    display.setFont(&FreeMonoBold12pt7b);
    display.setTextColor(GxEPD_BLACK);
    display.fillScreen(GxEPD_WHITE);

    if (! rtc.begin()) {
        Serial.println(F("Couldn't find RTC"));
        while(1){}
    }
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // // set rtc to date/time of compilation
    rtc.disable32K();  // stop signal at the 32K pin
    rtc.clearAlarm(1);
    rtc.disableAlarm(1);
    rtc.writeSqwPinMode(DS3231_OFF); // stop signal at the SQW pin
    rtc.setAlarm1(alarmTime, DS3231_A1_Second);
    rtc.adjust(rtc.now() + TimeSpan(COMPILE_DURATION));
    
     /* pressing the item or value button will cause a low signal at the corresponding pin
       and trigger an interrupt */
    PCICR = (1<<PCIE2);    // enable PCINT[23:16] interrupts
    PCMSK2 = (1<<PCINT21); // 
}

void loop() {

    printDateTime();
        
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); // choose power down mode
    sleep_mode(); // sleep now!
    rtc.clearAlarm(1);   
}

void printDateTime(){  // display date/time
    DateTime now = rtc.now();

    char datBuf[] = "DD.MM.YYYY";  // define the date format
    char timeBuf[] = "hh:mm";  // define the time format
    Serial.println(now.toString(datBuf));  // print date 
    Serial.println(now.toString(timeBuf));  // print time 

    display.firstPage();
    do
    {
        display.setCursor(30, 20);
        display.print(daysOfTheWeek[now.dayOfTheWeek()]);
        display.setCursor(30, 60);
        display.print(datBuf);
        display.setFont(&FreeMonoBold24pt7b);
        display.setCursor(30, 150);
        display.print(timeBuf);
        display.setFont(&FreeMonoBold12pt7b);
    }
    while (display.nextPage());
    display.hibernate();  
}


ISR (PCINT2_vect){} // PCINT2_vect: interrupt vector for PORTD

 

#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DRIVER_CLASS GxEPD2_150_BN  // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1

#ifndef EPD_CS
#define EPD_CS SS
#endif

#if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS)
#define GxEPD2_BW_IS_GxEPD2_BW true
#define GxEPD2_3C_IS_GxEPD2_3C true
#define GxEPD2_4C_IS_GxEPD2_4C true
#define GxEPD2_7C_IS_GxEPD2_7C true
#define GxEPD2_1248_IS_GxEPD2_1248 true
#define GxEPD2_1248c_IS_GxEPD2_1248c true
#define IS_GxEPD(c, x) (c##x)
#define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x)
#define IS_GxEPD2_3C(x) IS_GxEPD(GxEPD2_3C_IS_, x)
#define IS_GxEPD2_4C(x) IS_GxEPD(GxEPD2_4C_IS_, x)
#define IS_GxEPD2_7C(x) IS_GxEPD(GxEPD2_7C_IS_, x)
#define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x)
#define IS_GxEPD2_1248c(x) IS_GxEPD(GxEPD2_1248c_IS_, x)


#if defined(ARDUINO_ARCH_AVR)
#define MAX_DISPLAY_BUFFER_SIZE 800 // 
#endif
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif

 


2 thoughts on “DS3231-based alarm clock

  1. Dear Mr Wolfgang,
    I am an avid reader of your blog and have used the DS3231 in a handful of my projects to keep them OFF until it’s time to do something (taking a photo; actuating a motor or reading a sensor). Thank you for another great article. Coincidentally, today I came across a new library that you may found useful, so I will share it here: https://github.com/drmpf/RTC_NTP_replacement

    1. Hi Francesco, thank you for the feedback and for the library. I will test it.

Leave a Reply

Your email address will not be published. Required fields are marked *