LiberIT Logo

Call Now: +1-226-256-1632

Exploring the DS3231 RTC with ESPHome: Setting Time,

Published: 2024-12-30

By LiberIT Editorial Team

Exploring the DS3231 RTC with ESPHome: Setting Time, Reading Time, and Alarms

The DS3231 Real-Time Clock (RTC) module is a versatile and accurate component that can help you manage timekeeping in your microcontroller projects. In this blog post, we’ll walk through what we learned today about using the DS3231 RTC with ESPHome, including setting the time, reading the time, and configuring alarms.


1. Setting the Time on the DS3231 RTC

When first using the DS3231 RTC module, you may need to set the current time so that the RTC can maintain accurate timekeeping. This step is essential for projects requiring precise timing, such as scheduling or logging.

Key Steps

  • The time is written to the DS3231 by sending the year, month, day, hour, minute, and second to its registers over I²C.
  • Here’s the method we used to set the time:
    void set_time(int year, int month, int day, int hour, int minute, int second) {
        Wire.beginTransmission(0x68);
        Wire.write(0);  // Start at register 0
        Wire.write(dec_to_bcd(second));
        Wire.write(dec_to_bcd(minute));
        Wire.write(dec_to_bcd(hour));
        Wire.write(dec_to_bcd(day));
        Wire.write(dec_to_bcd(month));
        Wire.write(dec_to_bcd(year - 2000));
        Wire.endTransmission();
    }
    
  • Once set, the DS3231 maintains the time even when powered down, thanks to its onboard battery.

Lessons Learned

  • Always verify the time set on the DS3231 by reading it back (see the next section).
  • Use a precise time source, such as SNTP, to ensure accuracy when setting the time.

2. Reading the Time from the DS3231 RTC

Reading the current time from the DS3231 is straightforward. The time registers can be queried over I²C, and the values are converted from Binary-Coded Decimal (BCD) format to human-readable time.

Key Steps

  • The time is read from the DS3231 by accessing its registers and decoding the values.
  • Here’s the method we used to read the time:
    Time get_time() {
        Wire.beginTransmission(0x68);
        Wire.write(0);  // Start at register 0
        Wire.endTransmission();
    

    if (Wire.requestFrom(0x68, 7) != 7) { ESP_LOGE("DS3231", "Failed to read time registers"); return Time{0, 0, 0, 0, 0, 0}; }

    uint8_t second = bcd_to_dec(Wire.read()); uint8_t minute = bcd_to_dec(Wire.read()); uint8_t hour = bcd_to_dec(Wire.read()); Wire.read(); // Skip day-of-week uint8_t day = bcd_to_dec(Wire.read()); uint8_t month = bcd_to_dec(Wire.read()); uint16_t year = bcd_to_dec(Wire.read()) + 2000;

    return Time{year, month, day, hour, minute, second}; }

Lessons Learned

  • Always check the validity of the time read from the DS3231. A default time of 2000-01-01 may indicate the RTC hasn’t been set.
  • Logs can be a helpful debugging tool to verify the time being read:
    [I][DS3231]: Current RTC Time: 2024-12-30 16:31:00
    

3. Configuring Alarms on the DS3231 RTC

The DS3231 features two alarms (Alarm1 and Alarm2) that can trigger based on specific time conditions. These alarms are useful for periodic wakeups or notifications.

Configuring Alarm1

We configured Alarm1 to trigger at a specific second past every minute:

void set_alarm() {
    Wire.beginTransmission(0x68);
    Wire.write(0x07);        // Start at Alarm1 seconds register
    Wire.write(0x06);        // Match exactly 6 seconds
    Wire.write(0x80);        // Match any minute
    Wire.write(0x80);        // Match any hour
    Wire.write(0x80);        // Match any day
    Wire.endTransmission();

Wire.beginTransmission(0x68); Wire.write(0x0E); // Control register Wire.write(0x05); // Enable Alarm1 interrupt Wire.endTransmission(); }

Lessons Learned

  • Alarms trigger the SQW/INT pin, which can be used as an interrupt to wake up a microcontroller from deep sleep.
  • The DS3231 alarm doesn’t natively support recurring intervals like "every 6 seconds." However, you can manually update the alarm after each trigger to achieve this.

Example Log Output

When the alarm triggers:

[I][DS3231]: Alarm Triggered!

4. Observations and Challenges

  • Alarm Behavior: Setting the alarm to trigger at specific intervals (like every 6 seconds) required additional logic. By default, alarms work best for specific times or daily schedules.
  • Deep Sleep Integration: The SQW/INT pin can wake the ESP32-C3 from deep sleep, making the DS3231 an excellent choice for low-power applications.
  • Debugging: Logging the status registers and verifying pin behavior helped us debug alarm configurations effectively.

Conclusion

Today’s exploration of the DS3231 RTC with ESPHome provided a comprehensive look at its capabilities. From setting and reading time to configuring alarms, we learned how to leverage this module for accurate timekeeping and periodic wakeups. The DS3231 is a reliable and versatile component for projects requiring precise timing. I've also shared my results with github open issue for adding support for this chip to esphome on Github

Have you used the DS3231 RTC in your projects? Share your experiences and insights by contacting us!

Appendix 1: Setting code:

esphome:
  name: ds3231-set-time

esp32: board: esp32-c3-devkitm-1 framework: type: arduino

i2c: sda: GPIO20 scl: GPIO21 scan: true

logger:

wifi: output_power: 8.5dB networks: - ssid: "yourWifiNetwork" password: !secret wifi_password

time:

  • platform: sntp id: sntp_time timezone: America/Toronto

custom_component:

  • lambda: |- class DS3231Component : public PollingComponent { public: DS3231Component(esphome::time::RealTimeClock *rtc) : PollingComponent(10000), rtc_(rtc) {}

    void setup() override { if (!Wire.requestFrom(0x68, 1)) { ESP_LOGE("DS3231", "RTC not found at I2C address 0x68"); return; } ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

    auto now = this->rtc_->now(); if (now.is_valid()) { set_time(now.year, now.month, now.day_of_month, now.hour, now.minute, now.second, now.day_of_week); ESP_LOGI("DS3231", "RTC time set to %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, now.minute, now.second); } else { ESP_LOGE("DS3231", "SNTP time is not valid; cannot set RTC"); } }

    void update() override { auto time = get_time(); if (time.year > 0) { ESP_LOGI("DS3231", "Current RTC Time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day, time.hour, time.minute, time.second); } else { ESP_LOGE("DS3231", "Failed to read time from RTC"); } }

    void set_time(int year, int month, int day, int hour, int minute, int second, int day_of_week) { Wire.beginTransmission(0x68); Wire.write(0); // Start at register 0 Wire.write(dec_to_bcd(second)); // Seconds Wire.write(dec_to_bcd(minute)); // Minutes Wire.write(dec_to_bcd(hour)); // Hours Wire.write(dec_to_bcd(day_of_week)); // Day of the week Wire.write(dec_to_bcd(day)); // Day of the month Wire.write(dec_to_bcd(month)); // Month Wire.write(dec_to_bcd(year - 2000)); // Year Wire.endTransmission(); }

    struct Time { int year; int month; int day; int hour; int minute; int second; int day_of_week; };

    Time get_time() { Wire.beginTransmission(0x68); Wire.write(0); // Start at register 0 Wire.endTransmission();

    if (Wire.requestFrom(0x68, 7) != 7) { ESP_LOGE("DS3231", "Failed to read time registers"); return Time{0, 0, 0, 0, 0, 0, 0}; }

    uint8_t second = bcd_to_dec(Wire.read()); uint8_t minute = bcd_to_dec(Wire.read()); uint8_t hour = bcd_to_dec(Wire.read()); uint8_t day_of_week = bcd_to_dec(Wire.read()); uint8_t day = bcd_to_dec(Wire.read()); uint8_t month = bcd_to_dec(Wire.read()); uint16_t year = bcd_to_dec(Wire.read()) + 2000;

    return Time{year, month, day, hour, minute, second, day_of_week}; }

    private: esphome::time::RealTimeClock *rtc_;

    uint8_t dec_to_bcd(int val) { return ((val / 10 * 16) + (val % 10)); }

    int bcd_to_dec(uint8_t val) { return ((val / 16 * 10) + (val % 16)); } };

    auto my_rtc = new DS3231Component(id(sntp_time)); App.register_component(my_rtc); return {};

Appendix 2: Reading code

esphome:
  name: ds3231-read-time
  platform: ESP32
  board: esp32-c3-devkitm-1

i2c: sda: GPIO20 # Adjust to your setup scl: GPIO21 scan: true

logger:

Use the DS1307 platform to interact with the DS3231 RTC

time:

  • platform: ds1307 id: rtc_time timezone: America/Toronto on_time_sync: then: - logger.log: "Time synchronized with RTC." - lambda: |- auto now = id(rtc_time).now(); ESP_LOGI("RTC", "Current time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);

interval:

  • interval: 5s then:
    • lambda: |- auto now = id(rtc_time).now(); ESP_LOGI("Time", "Current time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, now.minute, now.second);

Appendix 3: set alarm code

esphome:
  name: ds3231-alarm-test
  platform: ESP32
  board: esp32-c3-devkitm-1

i2c: sda: GPIO20 # Adjust to your wiring scl: GPIO21 scan: true

logger:

binary_sensor:

  • platform: gpio pin: number: GPIO9 # Pin connected to SQW/INT from DS3231 mode: INPUT_PULLUP name: "DS3231 Alarm Trigger" filters:
    • delayed_off: 10ms # Debounce to avoid false positives on_press:
    • logger.log: "Alarm Detected!" # Log when interrupt occurs

custom_component:

  • lambda: |- class DS3231Alarm : public Component { public: void setup() override { if (!Wire.requestFrom(0x68, 1)) { ESP_LOGE("DS3231", "RTC not found at I2C address 0x68"); return; } ESP_LOGI("DS3231", "RTC found at I2C address 0x68");

    // Set up the alarm to trigger every 6 seconds set_alarm(); }

    void set_alarm() { Wire.beginTransmission(0x68); Wire.write(0x07); // Start at Alarm1 seconds register Wire.write(0x01); // A1M1=0: Match on first second Wire.write(0x80); // A1M2=1: Match any minute Wire.write(0x80); // A1M3=1: Match any hour Wire.write(0x80); // A1M4=1: Match any day Wire.endTransmission();

    // Enable Alarm1 interrupt and disable square wave Wire.beginTransmission(0x68); Wire.write(0x0E); // Control register Wire.write(0x05); // INTCN=1 (enable interrupt), A1IE=1 (enable Alarm1) Wire.endTransmission();

    ESP_LOGI("DS3231", "Alarm set to trigger every 6 seconds."); }

    void loop() override { // Check if Alarm1 triggered by reading the status register Wire.beginTransmission(0x68); Wire.write(0x0F); // Status register Wire.endTransmission(); Wire.requestFrom(0x68, 1); uint8_t status = Wire.read();

    if (status & 0x01) { // Alarm1 flag (A1F) is set ESP_LOGI("DS3231", "Alarm Triggered!");

    // Clear the Alarm1 flag Wire.beginTransmission(0x68); Wire.write(0x0F); // Status register Wire.write(status & ~0x01); // Clear A1F Wire.endTransmission(); } } };

    auto my_alarm = new DS3231Alarm(); App.register_component(my_alarm); return {};

Back to Blog