Arduino

DS1302: модуль часов реального времени

Введение

DS1302 — модуль часов реального времени (RTC) с собственной батарейкой, продолжающий отслеживать текущую дату и время даже когда Arduino полностью обесточен — что отличает его от простого программного счётчика времени на основе millis(), сбрасывающегося при каждой перезагрузке платы. В статье разберём установку и считывание текущего времени, что станет основой для журналирования событий в итоговом мини-проекте цикла.

Концепция

DS1302 хранит время в специальных регистрах (секунды, минуты, часы, день, месяц, год), доступных через простой трёхпроводной интерфейс (CLK, DATA, RST, отдельный от стандартного SPI/I2C). Библиотека RtcDS1302 (или аналогичная, например Rtc by Makuna) предоставляет удобный объектный интерфейс для установки и считывания времени, а также позволяет проверить, было ли время уже когда-то установлено (важно для первого включения с разряженной или впервые установленной батарейкой) или модуль работает с «обнулённым» состоянием.

Пример кода

// firmware/src/main.cpp — DS1302: установка времени при первом запуске, периодическая отправка
#include <ThreeWire.h>
#include <RtcDS1302.h>

ThreeWire myWire(6, 7, 8); // DAT, CLK, RST
RtcDS1302<ThreeWire> rtc(myWire);

void setup()
{
    Serial.begin(9600);
    rtc.Begin();

    // Время компиляции скетча — удобный способ единоразовой установки времени при первой загрузке
    RtcDateTime compiledTime = RtcDateTime(__DATE__, __TIME__);

    if (!rtc.IsDateTimeValid()) {
        Serial.println("RTC:NOT_SET_USING_COMPILE_TIME");
        rtc.SetDateTime(compiledTime);
    }

    if (!rtc.GetIsRunning()) {
        rtc.SetIsRunning(true); // модуль может быть остановлен, например, после полной замены батарейки
    }

    delay(100);
    Serial.println("READY");
}

void loop()
{
    static unsigned long lastTimeSend = 0;
    if (millis() - lastTimeSend >= 1000) {
        const RtcDateTime now = rtc.GetDateTime();

        // Формат YYYY-MM-DD HH:MM:SS — однозначный, легко разбираемый на стороне Qt
        char buffer[20];
        snprintf(buffer, sizeof(buffer), "%04u-%02u-%02u %02u:%02u:%02u",
                 now.Year(), now.Month(), now.Day(),
                 now.Hour(), now.Minute(), now.Second());

        Serial.print("RTC:");
        Serial.println(buffer);

        lastTimeSend = millis();
    }

    static String inputBuffer = "";
    while (Serial.available() > 0) {
        char c = Serial.read();
        if (c == 'n') {
            processCommand(inputBuffer);
            inputBuffer = "";
        } else if (inputBuffer.length() < 64) {
            inputBuffer += c;
        }
    }
}

void processCommand(String command)
{
    command.trim();
    if (command.startsWith("RTC:SET:")) {
        // Установка времени от Qt-приложения — формат: RTC:SET:2026,6,23,14,30,0
        int y, mo, d, h, mi, s;
        sscanf(command.c_str() + 8, "%d,%d,%d,%d,%d,%d", &y, &mo, &d, &h, &mi, &s);
        rtc.SetDateTime(RtcDateTime(y, mo, d, h, mi, s));
        Serial.println("ACK:RTC:SET");
    }
}

Пояснения к коду

rtc.IsDateTimeValid() проверяет, действительно ли время, хранящееся в модуле, корректно — это важно при самом первом использовании нового модуля или после полной разрядки его резервной батарейки, когда хранящееся значение может быть произвольным «мусором»; в этом случае прошивка автоматически устанавливает время компиляции самого скетча (RtcDateTime(__DATE__, __TIME__)) как разумное начальное приближение, что удобнее, чем оставлять время полностью неустановленным.

RTC:SET: — команда, позволяющая Qt-приложению явно установить точное текущее время на модуле (например, синхронизировав его с системным временем компьютера) — это более точный способ установки времени по сравнению с фиксированным временем компиляции скетча, которое отражает момент компиляции, а не момент реальной установки часов на конкретном физическом устройстве, и может отличаться на минуты или часы от реального текущего момента, особенно если прошивка собиралась заранее.

Подводные камни

  • Разряженная резервная батарейка модуля DS1302 (обычно небольшая батарейка типа CR2032 или аналогичная) приводит к сбросу хранимого времени при каждом отключении основного питания — для устройств, развёрнутых на продолжительный срок без постоянного питания, состояние батарейки RTC-модуля нужно периодически проверять, поскольку без неё сам смысл использования RTC (сохранение времени между включениями) теряется, и модуль фактически вырождается в более сложную альтернативу простому millis().
  • Время компиляции скетча (__DATE__, __TIME__) фиксируется в момент именно компиляции исходного кода, а не момент его реальной загрузки на конкретное устройство — если прошивка собрана заранее и загружена на устройство значительно позже (или одна и та же скомпилированная прошивка используется для нескольких устройств, прошиваемых в разное время), автоматически подставленное время окажется неточным, и для продакшен-устройств обязательна последующая точная установка времени через RTC:SET, а не только полагание на время компиляции.
  • Различие часовых поясов между временем, установленным на RTC-модуле, и тем, что отображается в Qt-интерфейсе пользователю, если они не явно синхронизированы относительно одной согласованной системы отсчёта (UTC или конкретный локальный часовой пояс) — для проектов, где это важно (журналирование событий с точными временными метками для последующего анализа), стоит явно зафиксировать, в каком часовом поясе хранится время на устройстве, и выполнять соответствующее преобразование на стороне Qt при необходимости отображения в другом поясе.
  • Прямой разбор строки времени через sscanf() без проверки количества успешно разобранных полей (аналогично подводному камню про RGB) — при получении искажённой или неполной команды установки времени часть переменных останется неинициализированной, что может установить на RTC-модуле произвольное, некорректное значение времени без какой-либо явной индикации ошибки.