MicroPython: ESP32/ESP8266 с модулем часов реального времени DS3231

Узнайте, как подключить модуль часов реального времени DS3231 (RTC) к платам ESP32 и ESP8266, программируемым с помощью MicroPython. Модуль DS3231 RTC отлично подходит для точного хронометража, а также позволяет устанавливать будильники, генерировать прямоугольные импульсы различных частот и считывать показания температуры. В этом руководстве вы узнаете, как установить и получить текущее время, настроить будильники и считать температуру.

MicroPython ESP32 ESP8266 с модулем DS3231 часы реального времени

В этом руководстве рассматриваются следующие темы:

Впервые работаете с MicroPython? Ознакомьтесь с нашей книгой: MicroPython Programming with ESP32 and ESP8266 eBook (2nd Edition)

Предварительные требования

Для выполнения этого руководства вам нужна прошивка MicroPython, установленная на вашу плату ESP32 или ESP8266. Вам также понадобится IDE для написания и загрузки кода на плату. Мы рекомендуем использовать Thonny IDE или uPyCraft IDE:

Знакомство с модулями часов реального времени (RTC)

Модули RTC, такие как DS3231 и DS1307, имеют собственные миниатюрные часы для самостоятельного отслеживания времени. Обычно они поставляются с держателем батареи для подключения элемента питания, чтобы они продолжали работать даже при перезагрузке или отключении питания ESP32/ESP8266.

Модули RTC DS3231 и RTC DS1307

DS3231 и DS1307 – одни из самых популярных модулей для использования с микроконтроллерами. Оба совместимы с ESP32 и ESP8266 и взаимодействуют по протоколу I2C. DS3231 более точен, потому что выдаёт результаты с температурной компенсацией. Кроме того, DS3231 позволяет устанавливать внешние будильники, что может быть чрезвычайно полезно.

Знакомство с модулем DS3231 RTC

На следующей фотографии показан модуль DS3231 RTC. Он использует кварцевый генератор 32 кГц с температурной компенсацией (TCXO) для точного отслеживания времени (устойчив к изменениям температуры). Благодаря этому он также позволяет получать данные о температуре.

Модуль RTC DS3231

Помимо точного отслеживания даты и времени, модуль также имеет встроенную память для хранения до двух будильников и может выводить прямоугольные импульсы на различных частотах: 1 Гц, 4 кГц, 8 кГц и 32 кГц.

Связь с модулем RTC осуществляется по протоколу I2C. Обычно его адрес – 0x68.

Этот модуль также поставляется с EEPROM-памятью 24C32 объёмом 32 байта, которую можно использовать для хранения любых энергонезависимых данных. Вы можете связываться с этой EEPROM-памятью по I2C, обращаясь по адресу 0x57.

Держатель батареи DS3231

DS3231 поставляется с держателем батареи для подключения элемента питания, обеспечивающего точный хронометраж. В случае отключения питания он всё ещё может точно отслеживать время и сохранять все будильники.

Вам следует использовать аккумулятор LIR2032, который является перезаряжаемым. Не используйте CR2032 (неперезаряжаемый).

Держатель батареи RTC DS3231 Модуль DS3231 RTC с резервным питанием от батареи

Если вы хотите использовать батарею CR2032, которая является неперезаряжаемой, необходимо отключить цепь зарядки батареи, отпаяв и удалив резистор (обозначенный R4 на моём модуле) рядом с диодом.

RTC DS3231 удаление резистора для неперезаряжаемой батареи

Будильники DS3231

DS3231 может хранить до двух будильников: будильник 1 и будильник 2. Эти будильники можно настроить на срабатывание по определённому времени и/или дате. При срабатывании будильника пин SQW модуля выдаёт сигнал LOW.

Вы можете обнаружить этот сигнал с помощью ESP32/ESP8266 и запустить прерывания или даже разбудить устройство из глубокого сна. Таким образом, эта функция чрезвычайно полезна для настройки периодического пробуждения из глубокого сна и других периодических задач, автоматизации на основе времени, а также одноразовых оповещений (поскольку вы можете сбросить будильник после его срабатывания).

I2C адрес модуля DS3231 RTC

По умолчанию адрес DS3231 RTC – 0x68, а EEPROM, подключённая к модулю, имеет адрес 0x57. Вы можете запустить скетч I2C сканера для проверки адресов.

Распиновка модуля DS3231 RTC

В следующей таблице кратко описана распиновка модуля DS3231 RTC.

Пин

Описание

32K

Выход генератора 32 кГц – может использоваться как опорный тактовый сигнал

SQW

Выход прямоугольных импульсов / прерываний

SCL

Пин SCL для I2C

SDA

Пин SDA для I2C

VCC

Питание модуля (3.3 В или 5 В)

GND

GND

Подключение модуля DS3231 RTC к ESP32 или ESP8266

ESP32 с модулем RTC DS3231 -- установка и чтение времени

Список необходимых компонентов для этого руководства:

Подключите DS3231 к плате ESP32 или ESP8266. Используйте следующую таблицу в качестве справочника или ознакомьтесь со схемами подключения.

Модуль DS3231 RTC

ESP32

ESP8266 NodeMCU

SQW

GPIO 4 (или любой другой цифровой пин)

GPIO 14 (D5) (или любой другой цифровой пин)

SCL

GPIO 22

GPIO 5 (D1)

SDA

GPIO 21

GPIO 4 (D2)

VCC

3V3

3V3

GND

GND

GND

ESP32

Схема подключения ESP32 с модулем DS3231 RTC

Также может быть полезно: ESP32 I2C: настройка пинов, несколько шин и периферийных устройств (Arduino IDE)

ESP8266 NodeMCU

Схема подключения ESP8266 с модулем DS3231 RTC

Также может быть полезно: Справочник по распиновке ESP8266: какие GPIO пины использовать?

Работа с RTC

Использование модуля RTC в ваших проектах всегда требует двух важных шагов.

  1. Установка текущего времени: вы можете сделать это вручную, указав текущее время (или другое желаемое время) в коде; получить системное локальное время; или получить время с NTP-сервера.

  2. Сохранение времени: чтобы RTC продолжал показывать правильное время, даже при отключении питания, его необходимо подключить к батарее. Модули RTC поставляются с держателем батареи, обычно для монетной батарейки.

Модуль DS3231 RTC для MicroPython

Существуют различные библиотеки с разными функциями, которые упрощают взаимодействие с модулем DS3231 RTC. Мы будем использовать слегка модифицированную версию модуля urtc.py, разработанного Adafruit.

Выполните следующие шаги для установки необходимого модуля.

Скачивание и загрузка urtc.py

  1. Нажмите здесь, чтобы скачать код urtc.py;

  2. Скопируйте код в файл в Thonny IDE;

  3. Перейдите в File > Save as… и выберите MicroPython Device;

  4. Сохраните файл с именем urtc.py (не изменяйте название).

# 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)

Посмотреть исходный код

После загрузки модуля на плату вы можете использовать функции библиотеки в своём коде для взаимодействия с модулем DS3231 RTC.

DS3231 RTC: установка и получение времени с помощью MicroPython

Следующий код синхронизирует время RTC с системным временем и получает текущую дату, время и температуру каждую секунду.

# 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)

Посмотреть исходный код

Как работает код

Рассмотрим основные части этого кода.

Импорт библиотек

Сначала вам нужно импортировать модуль urtc, который вы загрузили ранее и который содержит функции для взаимодействия с RTC. Также необходимо импортировать классы Pin и I2C для установления I2C-связи с модулем.

import time
import urtc
from machine import I2C, Pin

Инициализация модуля RTC

Затем инициализируем I2C-связь и создаём объект rtc для обращения к нашему модулю DS3231 RTC. Мы используем пины I2C по умолчанию для ESP32 и ESP8266.

# 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)

Если вы используете ESP32, создайте объект I2C следующим образом:

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

Если вы используете ESP8266, создайте объект I2C следующим образом:

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

Синхронизация времени

Для синхронизации времени RTC необходимо использовать метод datetime() объекта rtc и передать в качестве аргумента кортеж времени в следующем формате:

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

Примечание: этот кортеж отличается от кортежа, используемого модулем time в MicroPython.

Мы можем установить время вручную на определённую дату и время следующим образом:

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

Или мы можем синхронизировать RTC с системным локальным временем. Мы будем синхронизировать время с системным локальным временем.

Сначала получаем кортеж локального времени с помощью time.localtime().

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

Возвращаемый кортеж отличается от используемого модулем urtc.

Поэтому сначала преобразуем его в секунды с помощью time.mktime().

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

И наконец, преобразуем его в кортеж, совместимый с библиотекой, с помощью функции seconds2tuple() из библиотеки urtc, которая принимает в качестве аргумента количество секунд с эпохи для локального времени.

initial_time = urtc.seconds2tuple(initial_time_seconds)

Наконец, мы можем передать нашу переменную initial_time, содержащую локальное время в кортеже, совместимом с модулем urtc, в функцию datetime() следующим образом.

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

# Sync the RTC
rtc.datetime(initial_time)

Примечание: После первой установки времени его нужно устанавливать снова только в том случае, если RTC потеряет питание (для предотвращения этого следует подключить батарею). Вы можете использовать функцию lost_power() объекта rtc, чтобы проверить, было ли потеряно питание. Эта функция возвращает True или False соответственно.

Получение времени

После этих строк кода модуль RTC синхронизирован с вашим локальным временем, и вы можете просто вызвать rtc.datetime() для получения текущего времени с RTC. Это возвращает объект со всеми элементами времени.

Для получения и вывода каждого элемента времени можно выполнить следующее:

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])

Мы также можем получить текущую температуру с модуля, вызвав метод get_temperature() объекта rtc.

temperature = rtc.get_temperature()

Данные о температуре приходят в градусах Цельсия.

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

Мы выводим время и текущую температуру каждую секунду.

time.sleep(1)

Запуск кода

Запустите приведённый выше код для синхронизации времени RTC с локальным временем и получения текущего времени и температуры.

Запуск MicroPython скрипта

С этого момента, если у RTC подключена батарея, время будет синхронизировано с вашим локальным временем. Таким образом, синхронизировать его больше не нужно, и вы можете просто вызвать rtc.datetime() для получения текущего времени.

ESP32 ESP8266 с DS3231 MicroPython получение даты, времени и температуры

DS3231 с ESP32/ESP8266 – настройка будильников

Модуль DS3231 RTC позволяет устанавливать до двух будильников: будильник 1 и будильник 2. Когда время достигает времени будильника, модуль изменяет состояние пина SQW с HIGH на LOW (активный низкий уровень). Мы можем отслеживать изменение состояния этого пина и выполнять определённую задачу при срабатывании будильника.

DS3231 с ESP32 ESP8266 NodeMCU настройка будильников

Важные замечания о будильниках:

  • RTC позволяет сохранять до двух будильников;

  • одновременно может быть активен только один будильник;

  • после срабатывания будильника необходимо сбросить его флаг, чтобы избежать повторного срабатывания и зависания ESP32/ESP8266;

  • необходимо деактивировать один будильник перед активацией другого.

DS3231 – настройка будильников

Следующий пример показывает, как настроить два будильника. При срабатывании будильника переключается встроенный светодиод.

# 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, 1, 2, None, 14, 58, 0, 0)  # Alarm 1 uses full datetime
alarm2_time = urtc.datetime_tuple(2025, 1, 2, None, 14, 59, 0, 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.")

Посмотреть исходный код

Как работает код

Рассмотрим основные части кода для настройки будильников.

Определение пинов

Сначала вам нужно определить GPIO, подключённый к пину SQW. Это пин, состояние которого изменится при срабатывании будильника. Если вы используете ESP32, мы определяем GPIO 4. Для ESP8266 – GPIO 14. Вы можете выбрать любые другие пины, при условии, что подключения в схеме совпадают.

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)

Мы устанавливаем пин SQW как вход с внутренним подтягивающим резистором (поскольку он работает с активным низким уровнем).

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

Время будильников

Затем мы создаём две переменные типа datetime_tuple для сохранения времени будильников в переменных следующим образом:

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

Настройте будильники на желаемое время.

Установка пина SQW как прерывания

Затем мы устанавливаем пин SQW как прерывание, вызывая метод irq().

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

Метод irq() принимает следующие аргументы:

  • handler: это функция, которая будет вызвана при обнаружении прерывания, в данном случае функция on_alarm().

  • trigger: определяет режим срабатывания. Существуют 3 различных условия. В нашем случае мы используем IRQ_FALLING для запуска прерывания, когда пин переходит из HIGH в LOW.

Подробнее о настройке прерываний с ESP32/ESP8266 на MicroPython читайте в следующем руководстве: MicroPython: Прерывания (Interrupts) с ESP32 и ESP8266

Функция on_alarm

Функция on_alarm проверяет, какой будильник сработал, и деактивирует этот будильник для сброса его флага. Эта функция будет вызвана при срабатывании будильника.

def on_alarm(pin):

Следующая строка проверяет, сработал ли будильник 1. Это позволяет нам деактивировать его сразу после срабатывания и выполнить определённую задачу в заданное время.

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

Эта строка сбрасывает флаг будильника 1 (чтобы вернуть его в исходное состояние).

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

Затем переключаем состояние светодиода.

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

Аналогично действуем для будильника 2.

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() для настройки будильников.

Для настройки будильников сначала необходимо сбросить все предыдущие существующие будильники следующим образом.

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

Затем мы можем установить время будильника, вызвав метод alarm_time() объекта rtc. Передайте в качестве аргументов время будильника (переменную типа datetime_tuple) и номер будильника (0=будильник1; 1=будильник2).

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

Мы включаем прерывание для будильников, чтобы пин SQW срабатывал при активации будильника.

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

Затем у нас есть оператор try, который при необходимости корректирует время RTC (если он потерял питание) и настраивает будильники.

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)

Обнаружение будильников происходит в фоновом режиме, поскольку мы настроили прерывание на пине SQW.

Тестирование будильников

Установите будильники на ближайшее время, чтобы увидеть их в действии. Затем запустите код.

Запуск MicroPython скрипта

Текущее время будет выводиться в консоль каждые пять секунд. При срабатывании будильника вы получите сообщение в консоли, а встроенный светодиод ESP32 или ESP8266 переключит своё состояние.

DS3231 RTC с ESP32 и ESP8266 MicroPython -- срабатывание будильников

DS3231 – настройка периодических будильников

Для настройки периодических будильников мы можем изменять время будильника каждый раз, когда будильник срабатывает. Например, представьте, что вы хотите запускать будильник каждые 10 минут. Для этого:

  • считываем текущее время при срабатывании будильника;

  • устанавливаем новый будильник, добавив 600 секунд (10 минут) к текущему времени;

  • через 10 минут будильник сработает;

  • повторяем предыдущие шаги.

Именно это мы реализуем в следующем примере.

# 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.")

Посмотреть исходный код

В этом примере мы создаём функцию add_minutes_to_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)

Эта функция принимает в качестве аргументов кортеж datetime (например, текущее время) и количество минут, которые вы хотите добавить к текущему времени для установки нового будильника в будущем. Функция возвращает новый кортеж datetime с новым временем будильника.

При срабатывании будильника выполняется функция on_alarm(). Сначала она переключает состояние встроенного светодиода.

# 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

Затем она деактивирует (сбрасывает флаг будильника 1).

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

После этого устанавливается новое время будильника путём добавления заданного количества минут к текущему времени. В этом примере мы настраиваем будильник каждые 10 минут.

# 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}")

Вы можете легко изменить интервал, настроив желаемое время в следующей строке.

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

Тестирование периодических будильников

Установите будильники на ближайшее время, чтобы увидеть их в действии. Затем запустите код.

Запуск MicroPython скрипта

Первый будильник сработает через одну минуту. Последующие будильники будут срабатывать каждые 10 минут. При срабатывании будильника также переключается состояние встроенного светодиода.

ESP32 ESP8266 периодические будильники с DS3231 MicroPython

Заключение

В этом руководстве вы узнали, как подключить модуль DS3231 RTC к платам ESP32 и ESP8266, программируемым с помощью MicroPython, для получения текущей даты и времени, а также настройки будильников. Этот модуль может быть очень полезен в проектах логирования данных, автоматизации, периодических задачах и многом другом.

Надеемся, это краткое руководство оказалось полезным. У нас есть руководства по другим датчикам и модулям, которые могут быть вам полезны:

Подробнее о MicroPython: