Raspberry Pi Pico: DS3231 RTC (Real-Time Clock) – Get Time and Set Alarms (MicroPython)

This is a getting started guide that shows how to interface the DS3231 Real Time Clock Module with the Raspberry Pi Pico board programmed with MicroPython: learn how to set and get the time, set alarms, and get temperature readings.

Raspberry Pi Pico with DS3231 RTC (Real-Time Clock): Getting Time and Setting Alarms (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

Table of Contents

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

New to the Raspberry Pi Pico? Check out our eBook: Learn Raspberry Pi Pico/Pico W with MicroPython.

Prerequisites – MicroPython Firmware

To follow this tutorial you need MicroPython firmware installed in your Raspberry Pi Pico board. You also need an IDE to write and upload the code to your board.

The recommended MicroPython IDE for the Raspberry Pi Pico is Thonny IDE. Follow the next tutorial to learn how to install Thonny IDE, flash MicroPython firmware, and upload code to the board.

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 Raspberry Pi Pico 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 Raspberry Pi Pico 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 Raspberry Pi Pico and trigger interrupts. This feature is extremely useful for 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 Raspberry Pi Pico

Wiring the DS3231 Module to the Raspberry Pi Pico

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 Raspberry Pi Pico. You can use the following table as a reference. We’re using the default I2C pins.

DS3231 RTC ModuleRaspberry Pi Pico
SQWGPIO 3 (or any other digital pin)
SCLGPIO 5
SDAGPIO 4
VCC3V3
GNDGND

You may also like: Raspberry Pi Pico and Pico W Pinout Guide: GPIOs Explained.

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 Raspberry Pi Pico;
  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
    _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)



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 with the Raspberry Pi Pico: Set and Get Time

RPi Pico DS3231 Set and Get Time

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/raspberry-pi-pico-ds3231-rtc-micropython/

import time
import urtc
from machine import I2C, Pin

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

# Initialize RTC (connected to I2C)
i2c = I2C(0, 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 = (2024, 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.

# Initialize RTC (connected to I2C)
i2c = I2C(0, scl=Pin(5), sda=Pin(4))
rtc = urtc.DS3231(i2c)

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 = (2024, 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 MicroPython script on Thonny IDE

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.

Raspberry Pi Pico with DS3231 with MicroPython Get Date, Time, and Temperature

DS3231 with the Raspberry Pi Pico – 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.

Raspberry Pi Pico DS3231 - 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 Pico;
  • 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 an LED when the alarm fires. To test this example, add an LED with a 220 Ohm resistor to the circuit. Connect the LED to GPIO 2.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-ds3231-rtc-micropython/

import time
import urtc
from machine import Pin, I2C

# Pin setup for SQW pin and LED
CLOCK_INTERRUPT_PIN = 3  # Adjust to your specific GPIO pin for SQW
LED_PIN = 2  # Adjust to your specific GPIO pin for the LED

# Initialize RTC (connected to I2C)
i2c = I2C(0, 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, 04, None, 12, 32, 00, 0)  # Alarm 1 uses full datetime
alarm2_time = urtc.datetime_tuple(2025, 01, 04, None, 12, 35, 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. We chose to connect it to GPIO 3, but you can choose any other pins as long as the circuit connections match.

CLOCK_INTERRUPT_PIN = 3  # Adjust to your specific GPIO pin for SQW

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, 04, None, 12, 32, 00, 0)  # Alarm 1 uses full datetime
alarm2_time = urtc.datetime_tuple(2025, 01, 04, None, 12, 35, 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 Raspberry Pi Pico using MicroPython, check the following tutorial:

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 MicrppPython script Thonny IDE

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 LED will toggle.

DS3231 RTC with the Raspberry Pi Pico 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/raspberry-pi-pico-ds3231-rtc-micropython/

from machine import Pin, I2C
import time
import urtc

# Pin setup for SQW pin and LED
CLOCK_INTERRUPT_PIN = 3  # Adjust to your specific GPIO pin for SQW
LED_PIN = 2  # Adjust to your specific GPIO pin for the LED

# Initialize RTC (connected to I2C)
i2c = I2C(0, 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 MicroPython script on Thonny IDE

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 LED.

Raspberry Pi Pico with DS3231 - Setting periodic alarms

Wrapping Up

In this tutorial, you learned how use the DS3231 Real-Time Clock module with the Raspberry Pi Pico 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 many other applications.

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

Learn more about the Raspberry Pi Pico with 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.