ESP32 NTP and DS3231 RTC

by Pratik (A PCBArtist)

The ESP32 contains a whole host of networking and communication features that require some sort of timekeeping to fully function. Many APIs in IoT applications expect a timestamp when communicating over an HTTPS link. In this article, we look at using ESP32 NTP and DS3231 RTC using ESP-IDF for timekeeping when power is out, and synchronizing RTC and NTP time when power is present.

To keep things clear, we use these terms in this article:

  • System time: The local system timestamp in seconds. This is the time spent since the ESP32 powered up. This timestamp is what you see on ESP_LOG based prints.
  • NTP time: This is the Unix time in seconds since epoch, as obtained by using NTP.
  • RTC time: This is the time stored in the RTC.

The aim is usually to sync these three as closely as possible and avoid one of them drifting away from the actual NTP time.

Interfacing ESP32 with DS3231 RTC

Interfacing the ESP32 with DS3231 is very easy, all you need are the SCL and SDA lines. If relevant, you might want to use the interrupt and 32kHz clock output lines for some applications.
Here is a simple schematic, for reference.
ESP32 NTP and DS3231 RTC
ESP32 and DS3231 RTC Interfacing Schematic

To find an article on ESP32 DS3231 interfacing and some handy power-saving tips, take a look at our articles on ESP32 DS3231 schematic and low power ESP32 RTC application tips.

If you did not know already, the ESP32 maintains system time in milliseconds and microseconds (epoch time format) and this time stamp can be accessed (read or written) using ESP-IDF system APIs. To maintain the time on ESP32 locally, instead of reading it from the RTC every time we need a time stamp, we can simply update the ESP32 internal timekeeping every one in a while.

How we keep system time with DS3231 without losing it during power loss is based on two simple steps:
1. Updating the RTC time with ESP32 NTP time at boot, if possible.
2. Reading the DS3231 RTC time when needed and setting the system time based on that, when needed.

ESP32 DS3231 I2C interface initialization

The code snippet below initializes the I2C interface for the ESP32 to enable communication with the RTC. Once the communication is established, ESP32 NTP and DS3231 RTC and help maintain time through all power states.

esp_err_t ds3231_init (void)
{
    i2c_config_t cfg;
    esp_err_t res;
    i2c_port_t port = I2C_NUM_0;

    cfg.sda_io_num = I2C_SDA_GPIO;
    cfg.scl_io_num = I2C_SCL_GPIO;
    cfg.master.clk_speed = I2C_FREQ_HZ;
    cfg.sda_pullup_en = GPIO_PULLUP_DISABLE;
    cfg.scl_pullup_en = GPIO_PULLUP_DISABLE;
    cfg.mode = I2C_MODE_MASTER;

    if ((res = i2c_param_config(port, &cfg)) != ESP_OK)
            return res;

    if ((res = i2c_driver_install(port, cfg.mode, 0, 0, 0)) != ESP_OK)
            return res;

    return ESP_OK;
} 

We have implemented two very simple functions to fetch the time value from DS3231 and also to write the time value back into the DS3231. Here is the function that writes the ESP32 NTP time to the DS3231 RTC.

esp_err_t ds3231_set_time(struct tm *time)
{
    uint8_t data[7];

    /* time/date data */
    data[0] = dec2bcd(time->tm_sec);
    data[1] = dec2bcd(time->tm_min);
    data[2] = dec2bcd(time->tm_hour);
    /* The week data must be in the range 1 to 7, and to keep the start on the
     * same day as for tm_wday have it start at 1 on Sunday. */
    data[3] = dec2bcd(time->tm_wday + 1);
    data[4] = dec2bcd(time->tm_mday);
    data[5] = dec2bcd(time->tm_mon + 1);
    data[6] = dec2bcd(time->tm_year - 100);

    //I2C_DEV_TAKE_MUTEX(dev);
    i2c_dev_write_reg(DS3231_ADDR_TIME, data, 7);
    //I2C_DEV_GIVE_MUTEX(dev);

    return ESP_OK;
} 

The snippet below shows the function to read time from DS3231 for setting the ESP32 internal system time based on the RTC timestamp in case NTP is not available.

esp_err_t ds3231_get_time(struct tm *time)
{
    uint8_t data[7];

    /* read time */
    i2c_dev_read_reg(DS3231_ADDR_TIME, data, 7);

    /* convert to unix time structure */
    time->tm_sec = bcd2dec(data[0]);
    time->tm_min = bcd2dec(data[1]);
    if (data[2] & DS3231_12HOUR_FLAG)
    {
        /* 12H */
        time->tm_hour = bcd2dec(data[2] & DS3231_12HOUR_MASK) - 1;
        /* AM/PM? */
        if (data[2] & DS3231_PM_FLAG) time->tm_hour += 12;
    }
    else time->tm_hour = bcd2dec(data[2]); /* 24H */
    time->tm_wday = bcd2dec(data[3]) - 1;
    time->tm_mday = bcd2dec(data[4]);
    time->tm_mon  = bcd2dec(data[5] & DS3231_MONTH_MASK) - 1;
    time->tm_year = bcd2dec(data[6]) + 100;
    time->tm_isdst = 0;

    return ESP_OK;
} 

Now that we have the ability to read and write the current time and date into the RTC, we need to ensure that we can interface this RTC time with the internal system timekeeping function of the ESP32. Read on to find out an easy way of matching ESP32 NTP and DS3231 RTC time.

Set ESP32 NTP time from RTC

If you take a close look at the ds3231_get_time() function above, you will notice that it returns time as a tm structure.
To update the ESP32 system time using the RTC time, we need to use the settimeofday() function of ESP-IDF.

The catch is that settimeofday() only accepts time as a timeval structure.

The solution? We have to convert the RTC (in struct tm format) to ESP32 system time (in struct timeval).

Here is code snippet to set the ESP32 system time or NTP time using the RTC timestamp:

struct tm tm_t;
time_t tt_t;
struct timeval ct;

// Initialize the DS3231 RTC I2C port
ds3231_init();

// Fetch RTC time as a tm struct
ds3231_get_time(&tm_t);

if ((tm_t.tm_year + 1900) >= 2020)     // Seems like a valid time on the RTC
{
    // Update system with this time
    // Convert struct tm to time_t
    tt_t = mktime (&tm_t);
    // Get current system time
    gettimeofday (&ct, NULL);
    // Set the seconds part, ignore microsec counter
    ct.tv_sec = tt_t;
    // Update ESP NTP time from RTC
    settimeofday (&ct, NULL);

    ESP_LOGW (TAG, "Time restored from RTC: %04d-%02d-%02d %02d:%02d:%02d",
        1900 + tm_t.tm_year, 1 + tm_t.tm_mon, tm_t.tm_mday,
        tm_t.tm_hour, tm_t.tm_min, tm_t.tm_sec);
} 

Set RTC time from ESP32 NTP time

So what happens when you are able to sync the time stamp over NTP and the ESP32 obtains a correct timestamp that way? You will need to store this value into the DS3231 RTC to be able to maintain time through a power failure.

You can do this by using gettimeofday() function to fetch the ESP32 system time, which is updated via NTP and write it to the RTC if needed. To do this, again, you will need to convert the system time (timeval struct) back to a tm_t struct and then write it to the RTC via I2C.

Here is how we did that:

struct timeval ct;
struct tm *tm_tp;
time_t tt_t;

// Get current system time
gettimeofday (&ct, NULL);

// Extract seconds count
tt_t = ct.tv_sec;
// Convert to struct tm format
tm_tp = gmtime (&tt_t);

if (ds3231_set_time (tm_tp) == ESP_OK)
{
    ESP_LOGW (TAG, "Setting RTC time to: %04d-%02d-%02d %02d:%02d:%02d",
        1900 + tm_tp->tm_year, 1 + tm_tp->tm_mon, tm_tp->tm_mday,
        tm_tp->tm_hour,tm_tp->tm_min, tm_tp->tm_sec);
} 

When to set or read the RTC time?

To make good use of the RTC, you have to keep in mind that the UNIX timestamp might also contain a millisecond component. In cases like that where the system time must be accurate to the order of milliseconds, the RTC will not be able to provide you with an accurate millisecond count if it does not support it. Also, the RTC I2C communication will add an unknown amount of time offset.

Therefore, reading the system time frequently from the RTC will never give you a millisecond level accuracy and can cause a skew of up to plus or minus 1 second.

For accurate timekeeping without frequent random drifts, using an RTC that only has I2C:

  • Only update system time (from NTP) once in a month or less often if the RTC time is valid during boot. You can store the “last good update” timestamp in EEPROM.
  • Sync the ESP32 system time to the RTC once in every 6 to 24 hours depending on the temperature of the operating environment. Large temperature swings or extreme temperature change will mean larger drifts in the ESP32 RTC time.
  • Rely on the internal ESP32 system time (struct timeval.tv_usec) for millisecond-accuracy timestamps.

Getting millisecond level accuracy in ESP32 timestamps

This is a whole another topic in itself and something we would be discussing in a future article.

The upcoming article will cover:

  • Maintaining ESP32 time with millisecond accuracy after sync with NTP time.
  • EXACTLY synchronizing the DS3231 RTC and ESP32 system time with millsecond accuracy.
  • Ensuring millisecond accuracy is maintained within ESP32 even on power failure, using coin cell.

Make sure you are subscribed from the sidebar to receive this article when it is out!

Change Log
  • Initial Release: 10 January 2021

You may also like

Leave a Comment

fourteen − 5 =