MicroPython: ESP32/ESP8266 with DS3231 Real Time Clock (Get Time and Set Alarms)

Learn how to interface the DS3231 Real Time Clock Module with the ESP32 and ESP8266 boards programmed with MicroPython. The DS3231 RTC module is a great module for accurate timekeeping, it also allows you to set alarms, output square waves with different frequencies, and get temperature readings. In this tutorial, you’ll learn how to set and get the time, set alarms, and get a temperature reading.

MicroPython: ESP32/ESP8266 with DS3231 Real Time Clock (Getting Time and Setting Alarms)

Table of Contents

In this tutorial, we’ll cover the following topics:

New to MicroPython? Check out our eBook: MicroPython Programming with ESP32 and ESP8266 eBook (2nd Edition)

Prerequisites

To follow this tutorial you need MicroPython firmware installed in your ESP32 or ESP8266 boards. You also need an IDE to write and upload the code to your board. We suggest using Thonny IDE or uPyCraft IDE:

Introducing Real-Time Clock (RTC) Modules

RTC modules, such as the DS3231 and DS1307, have their own tiny clock inside to keep track of time by themselves. Usually, they come with a battery holder to connect a battery so that they keep working even if the ESP32/ESP8266 resets or loses power.

RTC DS3231 and RTC DS1307 Modules

The DS3231 and the DS1307 are some of the most popular choices to use with microcontrollers. Both are compatible with the ESP32 and ESP8266 and communicate via I2C communication protocol. The DS3231 is more accurate because it gives temperature-compensated results. Additionally, it’s also possible to set external alarms with the DS3231, which can be extremely useful.

Introducing the DS3231 RTC Module

The following picture shows the DS3231 RTC Module. It uses a 32kHz temperature-compensated crystal oscillator (TCXO) to keep track of time in a precise way (it’s resistant to temperature changes). Because of that, it also allows you to get temperature data.

RTC DS3231 Module

Besides keeping track of the date and time precisely, it also has built-in memory for storing up to two alarms and can output square waves at different frequencies: 1Hz, 4kHz, 8kHz, and 32kHz.

You communicate with the RTC module using I2C communication protocol. Usually, it’s on address 0x68.

This module also comes with a 24C32 32-byte EEPROM that you can use to store any non-volatile data that you want. You can communicate with this EEPROM memory via I2C by addressing the right address (0x57).

DS3231 Battery Holder

The DS3231 comes with a battery holder to connect a battery to keep accurate timekeeping. In the event of a power outage, it can still keep track of time accurately and keep all the alarms.

You should use a LIR2032 battery, which is rechargeable. Don’t use a CR2032 (not rechargeable).

RTC DS3231 Battery Holder
DS3231 RTC Module with Battery Back-up

If you want to use a CR2032 battery, which is non-rechargeable, you must disconnect the battery charging circuit by unsoldering and removing the resistor (labeled R4 in my module) next to the diode.

RTC DS3231 Remove Resistor for non re-chargeable battery

DS3231 Alarms

The DS3231 can store up to two alarms: alarm 1 and alarm 2. These alarms can be configured to trigger based on a specific time and/or date. When an alarm is triggered, the SQW pin of the module outputs a LOW signal.

You can detect this signal with the ESP32/ESP8266 and trigger interrupts, or even to wake it up from deep sleep. Thus, this feature is extremely useful for setting a periodic deep sleep wake-up and other periodic tasks, time-based automation and also one-time alerts (because you can clear an alarm after it’s triggered).

DS3231 RTC Module I2C Address

By default, the address of the DS3231 RTC is 0x68 and the EEPROM connected to the module is 0x57. You can run an I2C scanner sketch to double-check the addresses

DS3231 RTC Module Pinout

The following table quickly describes the DS3231 RTC Module Pinout.

32K32kHz oscillator output — can be used as a clock reference
SQWSquare wave/Interrupt output
SCLSCL pin for I2C
SDASDA pin for I2C
VCCProvides power to the module (3.3V or 5V)
GNDGND

Connecting the DS3231 RTC Module to the ESP32 or ESP8266

ESP32 with RTC Module DS3231 Set and Read the Time

Here’s a list of the parts required for this tutorial:

You can use the preceding links or go directly to MakerAdvisor.com/tools to find all the parts for your projects at the best price!

Wire the DS3231 to the ESP32 or ESP8266 board. You can use the following table as a reference or take a look at the schematic diagrams.

DS3231 RTC ModuleESP32ESP8266 NodeMCU
SQWGPIO 4 (or any other digital pin)GPIO 14 (D5) (or any other digital pin)
SCLGPIO 22GPIO 5 (D1)
SDAGPIO 21GPIO 4 (D2)
VCC3V33V3
GNDGNDGND

ESP32

ESP32 with DS3231 RTC Module Circuit Diagram

You may also like: Guide for I2C Communication with the ESP32

ESP8266 NodeMCU

ESP32 with DS3231 RTC Module Circuit Diagram

You may also like: ESP8266 Pinout Reference: Which GPIO pins should you use?

Working with the RTC

Using an RTC module in your projects always requires two important steps.

  1. Setting the current time: you can do it manually by inserting the current time (or a different desired time) on the code; the system’s local time; or get the time from an NTP server.
  2. Retaining the time: to make sure the RTC keeps the correct time, even if it loses power, it needs to be connected to a battery. RTC modules come with a battery holder, usually for a coin cell.

DS3231 RTC MicroPython Module

There are different libraries with different features that make it easy to communicate with the DS3231 RTC module. We’ll use a slightly modified version of the urtc.py module developed by Adafruit.

Follow the next steps to install the required module.

Download and Upload the urtc.py

  1. Click here to download the urtc.py code;
  2. Copy the code to a file on Thonny IDE;
  3. Go to File > Save as… and select MicroPython Device;
  4. Save the file with the name urtc.py (don’t change the name).
# Forked and adapted from https://github.com/adafruit/Adafruit-uRTC/tree/master

import collections
import time


DateTimeTuple = collections.namedtuple("DateTimeTuple", ["year", "month",
    "day", "weekday", "hour", "minute", "second", "millisecond"])


def datetime_tuple(year=None, month=None, day=None, weekday=None, hour=None,
                   minute=None, second=None, millisecond=None):
    return DateTimeTuple(year, month, day, weekday, hour, minute,
                         second, millisecond)


def _bcd2bin(value):
    return (value or 0) - 6 * ((value or 0) >> 4)


def _bin2bcd(value):
    return (value or 0) + 6 * ((value or 0) // 10)


def tuple2seconds(datetime):
    return time.mktime((datetime.year, datetime.month, datetime.day,
        datetime.hour, datetime.minute, datetime.second, datetime.weekday, 0))


def seconds2tuple(seconds):
    (year, month, day, hour, minute,
     second, weekday, _yday) = time.localtime(seconds)
    return DateTimeTuple(year, month, day, weekday, hour, minute, second, 0)


class _BaseRTC:
    _SWAP_DAY_WEEKDAY = False

    def __init__(self, i2c, address=0x68):
        self.i2c = i2c
        self.address = address

    def _register(self, register, buffer=None):
        if buffer is None:
            return self.i2c.readfrom_mem(self.address, register, 1)[0]
        self.i2c.writeto_mem(self.address, register, buffer)

    def _flag(self, register, mask, value=None):
        data = self._register(register)
        if value is None:
            return bool(data & mask)
        if value:
            data |= mask
        else:
            data &= ~mask
        self._register(register, bytearray((data,)))


    def datetime(self, datetime=None):
        if datetime is None:
            buffer = self.i2c.readfrom_mem(self.address,
                                           self._DATETIME_REGISTER, 7)
            if self._SWAP_DAY_WEEKDAY:
                day = buffer[3]
                weekday = buffer[4]
            else:
                day = buffer[4]
                weekday = buffer[3]
            return datetime_tuple(
                year=_bcd2bin(buffer[6]) + 2000,
                month=_bcd2bin(buffer[5]),
                day=_bcd2bin(day),
                weekday=_bcd2bin(weekday),
                hour=_bcd2bin(buffer[2]),
                minute=_bcd2bin(buffer[1]),
                second=_bcd2bin(buffer[0]),
            )
        datetime = datetime_tuple(*datetime)
        buffer = bytearray(7)
        buffer[0] = _bin2bcd(datetime.second)
        buffer[1] = _bin2bcd(datetime.minute)
        buffer[2] = _bin2bcd(datetime.hour)
        if self._SWAP_DAY_WEEKDAY:
            buffer[4] = _bin2bcd(datetime.weekday)
            buffer[3] = _bin2bcd(datetime.day)
        else:
            buffer[3] = _bin2bcd(datetime.weekday)
            buffer[4] = _bin2bcd(datetime.day)
        buffer[5] = _bin2bcd(datetime.month)
        buffer[6] = _bin2bcd(datetime.year - 2000)
        self._register(self._DATETIME_REGISTER, buffer)


class DS1307(_BaseRTC):
    _NVRAM_REGISTER = 0x08
    _DATETIME_REGISTER = 0x00
    _SQUARE_WAVE_REGISTER = 0x07

    def stop(self, value=None):
        return self._flag(0x00, 0b10000000, value)

    def memory(self, address, buffer=None):
        if buffer is not None and address + len(buffer) > 56:
            raise ValueError("address out of range")
        return self._register(self._NVRAM_REGISTER + address, buffer)


class DS3231(_BaseRTC):
    _CONTROL_REGISTER = 0x0e
    _STATUS_REGISTER = 0x0f
    _DATETIME_REGISTER = 0x00
    _TEMPERATURE_MSB_REGISTER = 0x11
    _TEMPERATURE_LSB_REGISTER = 0x12
    _ALARM_REGISTERS = (0x08, 0x0b)
    _SQUARE_WAVE_REGISTER = 0x0e

    def lost_power(self):
        return self._flag(self._STATUS_REGISTER, 0b10000000)

    def alarm(self, value=None, alarm=0):
        return self._flag(self._STATUS_REGISTER,
                          0b00000011 & (1 << alarm), value)

    def interrupt(self, alarm=0):
        return self._flag(self._CONTROL_REGISTER,
                          0b00000100 | (1 << alarm), 1)

    def no_interrupt(self):
        return self._flag(self._CONTROL_REGISTER, 0b00000011, 0)

    def stop(self, value=None):
        return self._flag(self._CONTROL_REGISTER, 0b10000000, value)

    def datetime(self, datetime=None):
        if datetime is not None:
            status = self._register(self._STATUS_REGISTER) & 0b01111111
            self._register(self._STATUS_REGISTER, bytearray((status,)))
        return super().datetime(datetime)

    def alarm_time(self, datetime=None, alarm=0):
        if datetime is None:
            buffer = self.i2c.readfrom_mem(self.address,
                                           self._ALARM_REGISTERS[alarm], 3)
            day = None
            weekday = None
            second = None
            if buffer[2] & 0b10000000:
                pass
            elif buffer[2] & 0b01000000:
                day = _bcd2bin(buffer[2] & 0x3f)
            else:
                weekday = _bcd2bin(buffer[2] & 0x3f)
            minute = (_bcd2bin(buffer[0] & 0x7f)
                      if not buffer[0] & 0x80 else None)
            hour = (_bcd2bin(buffer[1] & 0x7f)
                    if not buffer[1] & 0x80 else None)
            if alarm == 0:
                # handle seconds
                buffer = self.i2c.readfrom_mem(
                    self.address, self._ALARM_REGISTERS[alarm] - 1, 1)
                second = (_bcd2bin(buffer[0] & 0x7f)
                          if not buffer[0] & 0x80 else None)
            return datetime_tuple(
                day=day,
                weekday=weekday,
                hour=hour,
                minute=minute,
                second=second,
            )
        datetime = datetime_tuple(*datetime)
        buffer = bytearray(3)
        buffer[0] = (_bin2bcd(datetime.minute)
                     if datetime.minute is not None else 0x80)
        buffer[1] = (_bin2bcd(datetime.hour)
                     if datetime.hour is not None else 0x80)
        if datetime.day is not None:
            if datetime.weekday is not None:
                raise ValueError("can't specify both day and weekday")
            buffer[2] = _bin2bcd(datetime.day)
        elif datetime.weekday is not None:
            buffer[2] = _bin2bcd(datetime.weekday) | 0b01000000
        else:
            buffer[2] = 0x80
        self._register(self._ALARM_REGISTERS[alarm], buffer)
        if alarm == 0:
            # handle seconds
            buffer = bytearray([_bin2bcd(datetime.second)
                                if datetime.second is not None else 0x80])
            self._register(self._ALARM_REGISTERS[alarm] - 1, buffer)
    
    def get_temperature(self):
        """
        Reads the temperature from the DS3231's temperature registers.
        Returns the temperature as a float in Celsius.
        """
        msb = self._register(self._TEMPERATURE_MSB_REGISTER)  # 0x11
        lsb = self._register(self._TEMPERATURE_LSB_REGISTER)  # 0x12
        
        if msb is None or lsb is None:
            print("Error: Register read returned None")
            return None
        
        temp = msb + ((lsb >> 6) * 0.25)
        if msb & 0x80:
            temp -= 256
        
        return temp

class PCF8523(_BaseRTC):
    _CONTROL1_REGISTER = 0x00
    _CONTROL2_REGISTER = 0x01
    _CONTROL3_REGISTER = 0x02
    _DATETIME_REGISTER = 0x03
    _ALARM_REGISTER = 0x0a
    _SQUARE_WAVE_REGISTER = 0x0f
    _SWAP_DAY_WEEKDAY = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.init()

    def init(self):
        # Enable battery switchover and low-battery detection.
        self._flag(self._CONTROL3_REGISTER, 0b11100000, False)

    def reset(self):
        self._flag(self._CONTROL1_REGISTER, 0x58, True)
        self.init()

    def lost_power(self, value=None):
        return self._flag(self._CONTROL3_REGISTER, 0b00010000, value)

    def stop(self, value=None):
        return self._flag(self._CONTROL1_REGISTER, 0b00010000, value)

    def battery_low(self):
        return self._flag(self._CONTROL3_REGISTER, 0b00000100)

    def alarm(self, value=None):
        return self._flag(self._CONTROL2_REGISTER, 0b00001000, value)

    def datetime(self, datetime=None):
        if datetime is not None:
            self.lost_power(False) # clear the battery switchover flag
        return super().datetime(datetime)

    def alarm_time(self, datetime=None):
        if datetime is None:
            buffer = self.i2c.readfrom_mem(self.address,
                                           self._ALARM_REGISTER, 4)
            return datetime_tuple(
                weekday=_bcd2bin(buffer[3] &
                                 0x7f) if not buffer[3] & 0x80 else None,
                day=_bcd2bin(buffer[2] &
                             0x7f) if not buffer[2] & 0x80 else None,
                hour=_bcd2bin(buffer[1] &
                              0x7f) if not buffer[1] & 0x80 else None,
                minute=_bcd2bin(buffer[0] &
                                0x7f) if not buffer[0] & 0x80 else None,
            )
        datetime = datetime_tuple(*datetime)
        buffer = bytearray(4)
        buffer[0] = (_bin2bcd(datetime.minute)
                     if datetime.minute is not None else 0x80)
        buffer[1] = (_bin2bcd(datetime.hour)
                     if datetime.hour is not None else 0x80)
        buffer[2] = (_bin2bcd(datetime.day)
                     if datetime.day is not None else 0x80)
        buffer[3] = (_bin2bcd(datetime.weekday) | 0b01000000
                     if datetime.weekday is not None else 0x80)
        self._register(self._ALARM_REGISTER, buffer)

View raw code

With the module loaded to the board, now you can use the library functionalities in your code to interface with the DS3231 RTC module.

DS3231 RTC: Set and Get Time with MicroPython

The following code synchronizes the RTC time with the system time, and gets the current date, time and temperature every second.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-esp8266-ds3231/
# Code to synchronize the RTC with the local time

import time
import urtc
from machine import I2C, Pin

days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']

# Initialize RTC (connected to I2C) - ESP32
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# Initialize RTC (connected to I2C) - uncomment for ESP8266
#i2c = I2C(scl=Pin(5), sda=Pin(4))

rtc = urtc.DS3231(i2c)

# Set the current time using a specified time tuple
# Time tuple: (year, month, day, day of week, hour, minute, seconds, milliseconds)
#initial_time = (2025, 1, 30, 1, 12, 30, 0, 0)

# Or get the local time from the system
initial_time_tuple = time.localtime()  # tuple (microPython)
initial_time_seconds = time.mktime(initial_time_tuple)  # local time in seconds

# Convert to tuple compatible with the library
initial_time = urtc.seconds2tuple(initial_time_seconds)

# Sync the RTC
rtc.datetime(initial_time)

while True:
    current_datetime = rtc.datetime()
    temperature = rtc.get_temperature()
    
    # Display time details
    print('Current date and time:')
    print('Year:', current_datetime.year)
    print('Month:', current_datetime.month)
    print('Day:', current_datetime.day)
    print('Hour:', current_datetime.hour)
    print('Minute:', current_datetime.minute)
    print('Second:', current_datetime.second)
    print('Day of the Week:', days_of_week[current_datetime.weekday])
    print(f"Current temperature: {temperature}°C")    
    # Format the date and time
    formatted_datetime = (
        f"{days_of_week[current_datetime.weekday]}, "
        f"{current_datetime.year:04d}-{current_datetime.month:02d}-{current_datetime.day:02d} "
        f"{current_datetime.hour:02d}:{current_datetime.minute:02d}:{current_datetime.second:02d} "
    )
    print(f"Current date and time: {formatted_datetime}")
    
    print(" \n");
    time.sleep(1)

View raw code

How the Code Works

Let’s take a quick look at the relevant parts of this code.

Import Libraries

First, you need to import the urtc module you’ve uploaded previously that contains the functions to interact with the RTC. You also need to import the Pin and I2C classes to establish an I2C communication with the module.

import time
import urtc
from machine import I2C, Pin

Initialize the RTC Module

Then, initialize I2C communication and create an object called rtc to refer to our DS3231 RTC module. We’re using the ESP32 and ESP8266 default I2C pins.

# Initialize RTC (connected to I2C) - ESP32
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# Initialize RTC (connected to I2C) - uncomment for ESP8266
#i2c = I2C(scl=Pin(5), sda=Pin(4))
rtc = urtc.DS3231(i2c)

If you’re using the ESP32, create the I2C object as follows:

i2c = I2C(0, scl=Pin(22), sda=Pin(21))

If you’re using an ESP8266, you create an I2C object as follows:

i2c = I2C(scl=Pin(5), sda=Pin(4))

Synchronize the Time

To synchronize the RTC time, you must use the datetime() method on the rtc object and pass as an argument a time tuple with the following format:

(year, month, day, day of week, hour, minute, seconds, milliseconds)

Note: this tuple is different from the one used by the MicroPython time module.

We can set the time manually to a defined date and time as follows:

#initial_time = (2025, 1, 30, 1, 12, 30, 0, 0)

Or, we can synchronize the RTC with the system’s local time. We’ll synchronize the time with the system’s local time.

First, we get the time tuple of the local time using time.localtime().

initial_time_tuple = time.localtime()  # tuple (microPython)

The tuple returned is different from the one used by the urtc module.

So, first, we convert it to seconds using time.mktime().

initial_time_seconds = time.mktime(initial_time_tuple)  # local time in seconds

And finally, we convert it to a tuple that is compatible with the library using the seconds2tuple() function from the urtc library that accepts as an argument the number of seconds since epoch for the local time.

initial_time = urtc.seconds2tuple(initial_time_seconds)

Finally, we can pass our initial_time variable that contains the local time in a tuple compatible with the urtc module to the datetime() function as follows.

# Convert to tuple compatible with the library
initial_time = urtc.seconds2tuple(initial_time_seconds)

# Sync the RTC
rtc.datetime(initial_time)

Note: After setting the time for the first time, we only need to set the time again if the RTC loses power (you should have a battery connected to prevent this). You can use the lost_power() function on the rtc object to check if it has lost power. This function returns True or False accordingly.

Getting the Time

After these lines of code, the RTC module is synchronized with your local time and you can simply call rtc.datetime() to get the current time from the RTC. This returns an object with all the time elements.

To get and print each time element, we can do as follows:

current_datetime = rtc.datetime()
print('Current date and time:')
print('Year:', current_datetime.year)
print('Month:', current_datetime.month)
print('Day:', current_datetime.day)
print('Hour:', current_datetime.hour)
print('Minute:', current_datetime.minute)
print('Second:', current_datetime.second)
print('Day of the Week:', days_of_week[current_datetime.weekday])

We can also get the current temperature from the module by calling the get_temperature() method on the rtc object.

temperature = rtc.get_temperature()

The temperature data comes in Celsius degrees.

print(f"Current temperature: {temperature}°C")

We print the time and current temperature every second.

time.sleep(1)

Run the Code

Run this previous code to synchronize the RTC time with the local time, and get the current time and temperature.

Run the MicroPython Script Code

From now on, if the RTC has an attached battery, it will keep the time synchronized with your local time. So, you don’t need to synchronize it anymore, and you can just call rtc.datetime() to get the current time.

ESP32 ESP8266 with DS3231 with MicroPython Get Date, Time, and Temperature

DS3231 with ESP32/ESP8266 – Setting Alarms

The DS3231 RTC Module allows you to set up to two alarms: alarm 1 and alarm 2. When the time reaches the alarm time, the module changes the state of the SQW pin from HIGH to LOW (it is active low). We can check for the change on that pin and perform a certain task when the alarm fires.

DS3231 with ESP32 ESP8266 NodeMCU Setting Alarms

Important notes about the alarms:

  • the RTC allows you to save up to two alarms;
  • you can only have one alarm active at a time;
  • after an alarm is triggered, you must clear its flag to avoid triggering and crashing the ESP32/ESP8266;
  • you must deactivate an alarm before activating the other.

DS3231 – Setting Up the Alarms

The following example shows how to set up two alarms. It toggles the onboard LED when the alarm fires.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-esp8266-ds3231/
 
import time
import urtc
from machine import Pin, I2C

# Pin setup for SQW pin and LED
CLOCK_INTERRUPT_PIN = 4  # Adjust to your specific GPIO pin for SQW (ESP32)
#CLOCK_INTERRUPT_PIN = 14  # Adjust to your specific GPIO pin for SQW (ESP8266)

LED_PIN = 2  # Adjust to your specific GPIO pin for the LED

# Initialize RTC (connected to I2C) - ESP32
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# Initialize RTC (connected to I2C) - uncomment for ESP8266
#i2c = I2C(scl=Pin(5), sda=Pin(4))

rtc = urtc.DS3231(i2c)

# Setup GPIO for SQW and LED
sqw_pin = Pin(CLOCK_INTERRUPT_PIN, Pin.IN, Pin.PULL_UP)
led_pin = Pin(LED_PIN, Pin.OUT)
led_pin.off()

# Alarm times (year, month, day, weekday, hour, minute, second, millisecond)
alarm1_time = urtc.datetime_tuple(2025, 01, 02, None, 14, 58, 00, 0)  # Alarm 1 uses full datetime
alarm2_time = urtc.datetime_tuple(2025, 01, 02, None, 14, 59, 00, 0)   # Alarm 2 uses day, hour, minute, weekday

# Print the current time from the RTC
def print_current_time():
    now = rtc.datetime()
    formatted_time = f"{now.year}-{now.month:02}-{now.day:02} {now.hour:02}:{now.minute:02}:{now.second:02}"
    print(f"Current time: {formatted_time}")

#Callback for handling alarm interrupt.
def on_alarm(pin):
    print("Interrupt detected.")
    
    if rtc.alarm(alarm=0):  # Check if Alarm 1 triggered
        print("Alarm 1 triggered.")
        rtc.alarm(False, alarm=0)  # Clear Alarm 1 flag
        led_pin.value(not led_pin.value())  # Toggle LED for Alarm 1
    
    if rtc.alarm(alarm=1):  # Check if Alarm 2 triggered
        print("Alarm 2 triggered.")
        rtc.alarm(False, alarm=1)  # Clear Alarm 2 flag
        led_pin.value(not led_pin.value())  # Toggle LED for Alarm 2

# Setup alarms on the DS3231
def setup_alarms():
    # Clear any existing alarms
    rtc.alarm(False, 0)
    rtc.alarm(False, 1)
    rtc.no_interrupt()

    # Set the desired alarm times
    rtc.alarm_time(alarm1_time, 0)  # Alarm 1
    rtc.alarm_time(alarm2_time, 1)  # Alarm 2

    # Enable interrupts for the alarms
    rtc.interrupt(0)
    rtc.interrupt(1)
    print("Alarms set successfully.")

# Attach the interrupt callback
sqw_pin.irq(trigger=Pin.IRQ_FALLING, handler=on_alarm)

try:
    # Sync time to compile time if RTC lost power
    if rtc.lost_power():
        initial_time_tuple = time.localtime()  # tuple (MicroPython)
        initial_time_seconds = time.mktime(initial_time_tuple)  # local time in seconds
        initial_time = urtc.seconds2tuple(initial_time_seconds)  # Convert to tuple compatible with the library
        rtc.datetime(initial_time)  # Sync the RTC

    print("RTC initialized. Setting alarms...")
    setup_alarms()

    while True:
        print_current_time()
        time.sleep(5)
except KeyboardInterrupt:
    print("Exiting program.")

View raw code

How Does the Code Work?

Let’s take a look at the relevant parts of code to set up the alarms.

Defining the Pins

First, you need to define the GPIO that is connected to the SQW pin. This is the pin that will change state when the alarms fire. If you’re using an ESP32, we’re defining GPIO 4. For the ESP8266, we’re defining GPIO 14. You can choose any other pins as long as the circuit connections match.

CLOCK_INTERRUPT_PIN = 4  # Adjust to your specific GPIO pin for SQW (ESP32)
#CLOCK_INTERRUPT_PIN = 14  # Adjust to your specific GPIO pin for SQW (ESP8266)

We set the SQW pin as an input with an internal pull-up resistor (because it is an active-low pin).

sqw_pin = Pin(CLOCK_INTERRUPT_PIN, Pin.IN, Pin.PULL_UP)

Alarm Times

Then, we create two variables of type datetime_tuple to save the alarm times in variables as follows:

# Alarm times (year, month, day, weekday, hour, minute, second, millisecond)
alarm1_time = urtc.datetime_tuple(2025, 01, 02, None, 14, 44, 00, 0)  # Alarm 1 uses full datetime
alarm2_time = urtc.datetime_tuple(2025, 01, 02, None, 14, 46, 00, 0)   # Alarm 2 uses day, hour, minute, weekday

Adjust the alarms to your desired times.

Setting the SQW Pin as an Interrupt

Then, we set the SQW pin as an interrupt by calling the irq() method.

sqw_pin.irq(trigger=Pin.IRQ_FALLING, handler=on_alarm)

The irq() method accepts the following arguments:

  • handler: this is a function that will be called when an interrupt is detected, in this case the on_alarm() function.
  • trigger: this defines the trigger mode. There are 3 different conditions. In our case, we’ll use IRQ_FALLING to trigger the interrupt whenever the pin goes from HIGH to LOW.

To learn more about setting interrupts with the ESP32/ESP8266 using MicroPython, check the following tutorial: MicroPython: Interrupts with ESP32 and ESP8266

on_alarm Function

The on_alarm function checks which alarm was triggered and deactivates that alarm to clear its flag. This function will be called when an alarm fires.

def on_alarm(pin):

The following line checks if alarm1 was triggered. This allows us to deactivate right after it’s triggered and also run a specific task at a determined time.

if rtc.alarm(alarm=0):  # Check if Alarm 1 triggered
    print("Alarm 1 triggered.")
    rtc.alarm(False, alarm=0)  # Clear Alarm 1 flag
    led_pin.value(not led_pin.value())  # Toggle LED for Alarm 1

This line clears the alarm1 flag (to reset it to its initial state).

rtc.alarm(False, alarm=0)  # Clear Alarm 1 flag

Then, we toggle the state of the LED.

led_pin.value(not led_pin.value())  # Toggle LED for Alarm 1

We act similarly for the alarm2.

if rtc.alarm(alarm=1):  # Check if Alarm 2 triggered
    print("Alarm 2 triggered.")
    rtc.alarm(False, alarm=1)  # Clear Alarm 2 flag
    led_pin.value(not led_pin.value())  # Toggle LED for Alarm 2

Setting Up the Alarms

We create a function called setup_alarms() to set up the alarms.

To set up the alarms, first we must clear any previous existing alarms as follows.

def setup_alarms():
    # Clear any existing alarms
    rtc.alarm(False, 0)
    rtc.alarm(False, 1)
    rtc.no_interrupt()

Then, we can set the alarm times by calling the alarm_time() method on the rtc object. Pass as arguments the alarm time (a datetime_tuple type variable), and the alarm number (0=alarm1; 1=alarm2).

# Set the desired alarm times
rtc.alarm_time(alarm1_time, 0)  # Alarm 1
rtc.alarm_time(alarm2_time, 1)  # Alarm 2

We enable the interrupt for the alarms, so that the SQW pin is triggered when an alarm fires.

rtc.interrupt(0)
rtc.interrupt(1)

Then we have a try statement that will adjust the time of the RTC if needed (if it has lost power), and set up the alarms.

try:
    # Sync time to compile time if RTC lost power
    if rtc.lost_power():
        initial_time_tuple = time.localtime()  # tuple (MicroPython)
        initial_time_seconds = time.mktime(initial_time_tuple)  # local time in seconds
        initial_time = urtc.seconds2tuple(initial_time_seconds)  # Convert to tuple compatible with the library
        rtc.datetime(initial_time)  # Sync the RTC

    print("RTC initialized. Setting alarms...")
    setup_alarms()

Then, we’ll constantly print the current time every five seconds.

while True:
    print_current_time()
    time.sleep(5)

Detecting the alarms is done in the background because we’ve set up an interrupt on the SQW pin.

Testing the Alarms

Set the alarms to a close-up time so that you can see them in action. Then, run the code.

Run the MicroPython Script Code

The current time will be printed into the shell every five seconds. When the alarm fires, you’ll get a message on the shell and the onboard LED of the ESP32 or ESP8266 will toggle.

DS3231 RTC with ESP32 and ESP8266 MicroPython - Triggering Alarms

DS3231 – Setting Up Periodic Alarms

To set up periodic alarms we can adjust the alarm time every time an alarm is triggered. For example, imagine you want to trigger an alarm every 10 minutes. To do that:

  • read the current time when the alarm is triggered;
  • set up a new alarm by adding 600 seconds (10 minutes) to the current time;
  • after 10 minutes the alarm will be triggered;
  • repeat the previous steps.

That’s exactly what we’ll implement in the following example.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/micropython-esp32-esp8266-ds3231/
 
from machine import Pin, I2C
import time
import urtc

# Pin setup for SQW pin and LED
CLOCK_INTERRUPT_PIN = 4  # Adjust to your specific GPIO pin for SQW (ESP32)
#CLOCK_INTERRUPT_PIN = 14  # Adjust to your specific GPIO pin for SQW (ESP8266)

LED_PIN = 2  # Adjust to your specific GPIO pin for the LED

# Initialize RTC (connected to I2C) - ESP32
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# Initialize RTC (connected to I2C) - uncomment for ESP8266
#i2c = I2C(scl=Pin(5), sda=Pin(4))

rtc = urtc.DS3231(i2c)

# Setup GPIO for SQW and LED
sqw_pin = Pin(CLOCK_INTERRUPT_PIN, Pin.IN, Pin.PULL_UP)
led_pin = Pin(LED_PIN, Pin.OUT)
led_pin.off()

# Add minutes to a datetime tuple and return a new datetime tuple
def add_minutes_to_time(dt, minutes):
    timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, 0, 0))
    new_timestamp = timestamp + (minutes * 60)
    new_time = time.localtime(new_timestamp)
    return urtc.datetime_tuple(new_time[0], new_time[1], new_time[2], None, new_time[3], new_time[4], new_time[5], 0)

# Print the current time from the RTC
def print_current_time():
    now = rtc.datetime()
    formatted_time = f"{now.year}-{now.month:02}-{now.day:02} {now.hour:02}:{now.minute:02}:{now.second:02}"
    print(f"Current time: {formatted_time}")

# Callback for handling alarm interrupt
def on_alarm(pin):
    print("Alarm triggered! Toggling the LED.")
    led_pin.value(not led_pin.value())  # Toggle the LED

    # Clear the alarm flag and schedule the next alarm
    if rtc.alarm(alarm=0):  # Check if Alarm 0 triggered
        print("Clearing Alarm 0 flag.")
        rtc.alarm(False, alarm=0)  # Clear alarm flag

        # Schedule the alarm to repeat 10 minutes from now
        now = rtc.datetime()
        next_alarm_time = add_minutes_to_time(now, 10)  # Add 10 minutes
        rtc.alarm_time(next_alarm_time, alarm=0)  # Set new alarm
        rtc.interrupt(0)  # Ensure Alarm 0 interrupt is enabled
        print(f"Next alarm set for: {next_alarm_time}")

def setup_alarm():
    # Clear any existing alarms
    rtc.alarm(False, 0)
    rtc.no_interrupt()

    # Get the current time and set the first alarm 1 minute from now
    now = rtc.datetime()
    first_alarm_time = add_minutes_to_time(now, 1)  # Set first alarm for 1 minute from now
    rtc.alarm_time(first_alarm_time, alarm=0)  # Alarm 0

    # Enable the interrupt for Alarm 0
    rtc.interrupt(0)

    print(f"Alarm set for: {first_alarm_time}")

# Attach the interrupt callback
sqw_pin.irq(trigger=Pin.IRQ_FALLING, handler=on_alarm)

try:
    # Sync time to compile time if RTC lost power
    if rtc.lost_power():
        initial_time_tuple = time.localtime()  # tuple (MicroPython)
        initial_time_seconds = time.mktime(initial_time_tuple)  # local time in seconds
        initial_time = urtc.seconds2tuple(initial_time_seconds)  # Convert to tuple compatible with the library
        rtc.datetime(initial_time)  # Sync the RTC

    print("RTC initialized. Setting up the periodic alarm...")
    setup_alarm()

    while True:
        print_current_time()
        time.sleep(5)
except KeyboardInterrupt:
    print("Exiting program.")

View raw code

In this example, we create a function called add_minutes_to_time() that will add minutes to the current time.

# Add minutes to a datetime tuple and return a new datetime tuple
def add_minutes_to_time(dt, minutes):
    timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, 0, 0))
    new_timestamp = timestamp + (minutes * 60)
    new_time = time.localtime(new_timestamp)
    return urtc.datetime_tuple(new_time[0], new_time[1], new_time[2], None, new_time[3], new_time[4], new_time[5], 0)

This function accepts as arguments a datetime tupple (like the current time) and the minutes you want to add to the current time to set up a new alarm in the future. This function returns a new datetime tuple with the new alarm time.

When an alarm is triggered, the on_alarm() function will run. It will first toggle the state of the onboard LED.

# Callback for handling alarm interrupt
def on_alarm(pin):
    print("Alarm triggered! Toggling the LED.")
    led_pin.value(not led_pin.value())  # Toggle the LED

Then, it will deactivate (clear the flag for alarm1).

if rtc.alarm(alarm=0):  # Check if Alarm 0 triggered
    print("Clearing Alarm 0 flag.")
    rtc.alarm(False, alarm=0)  # Clear alarm flag

After that, it will set up a new alarm time by adding a predefined number of minutes to the current time. In this example, we’re setting up an alarm every 10 minutes

# Schedule the alarm to repeat 10 minutes from now
now = rtc.datetime()
next_alarm_time = add_minutes_to_time(now, 10)  # Add 10 minutes
rtc.alarm_time(next_alarm_time, alarm=0)  # Set new alarm
rtc.interrupt(0)  # Ensure Alarm 0 interrupt is enabled
print(f"Next alarm set for: {next_alarm_time}")

You can easily change this by adjusting to your desired time on the following line.

next_alarm_time = add_minutes_to_time(now, 10)  # Add 10 minutes

Testing the Periodic Alarms

Set the alarms to a close-up time so that you can see them in action. Then, run the code.

Run the MicroPython Script Code

The first alarm will trigger after one minute. The following alarms will trigger every 10 minutes. When an alarm is triggered, we also toggle the state of the onboard LED.

ESP32/ESP8266 Periodic Alarms with DS3231 MicroPython

Wrapping Up

In this tutorial, you learned how to interface the DS3231 RTC module with the ESP32 and ESP8266 boards programmed with MicroPython to get the current date and time and set up alarms. This module can be very useful in data logging projects, automation, periodic tasks and more.

We hope you found this quick guide useful. We have guides for other sensors and modules that you may find useful:

To learn more about MicroPython, check out our resources:

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.