NTP with Day Light Saving Time and RTC Backup for the ESP8266
Retrieving the correct time on the ESP8266 with NTP is quite easy. On this page I want to show how to add a RTC like a D3231 as fallback if for any reason NTP is not available or a proper time should be available immediately after startup.
I started with my code "NTP-TZ-DST bare minimum", it's quite simple and you can read more about on the page for NTP and DST. It uses the time.h and the build in ESP functions to retrieve NTP without an additional UDP library. As RTC I'm using a DS3231 and the "RTClib.h" from Adafruit.
Newer ESP Cores support NTP and Daylight Saving Time. The internal time runs in UTC. Timezone and DST offset is added when you request actual time information. Therefore I decided to let the RTC also run in UTC.
The example sketch will do following:
- on startup: if at the end of setup() the time information is to old (date before September 2000) and if RTC is connected the internal time gets updated based on the RTC time.
- print time information every second
- each time we have a successful NTP response, we also update the time information in the RTC.
Some Functions of time.h
There are some important functions in the time.h:
- time_t mktime(struct tm *tm)
- takes an argument
representing broken-down time - struct tm *localtime(const time_t *timep)
- converts a time_t into a tm structure in your local time (timezone and DST offset)
- struct tm *gmtime(const time_t *timep)
- converts a time_t into a tm structure in UTC
- struct tm *gmtime_r(const time_t *restrict timep,
struct tm *restrict result); - The gmtime() function converts the calendar time timep
to broken-
down time representation, expressed in Coordinated Universal Time
(UTC). It may return NULL when the year does not fit into an
integer. The return value points to a statically allocated
struct which might be overwritten by subsequent calls to any of
the date and time functions. - struct tm *gmtime(const time_t *timep)
- The gmtime_r() function does the
same, but stores the data in a user-supplied struct. - int settimeofday(const struct timeval *tv, const struct timezone *tz)
- can set the time as well as a timezone.
- size_t strftime (char* ptr, size_t maxsize, const char* format, const struct tm* timeptr );
- Copies into ptr the content of format, expanding its format specifiers into the corresponding values that represent the time described in timeptr, with a limit of maxsize characters.
The POSIX functions timelocal() and timegm() are not available in the ESP8266 Core 3.0.2. Therefore I adopted a function getTimestamp() which will accept a broken down time information and return the Unix timestamp.
The NTP DST RTC example
The NTP sketch for the ESP looks like following:
/* NTP TZ DST - RTC fallback NetWork Time Protocol - Time Zone - Daylight Saving Time - Real Time Clock Target for this sketch is: - get the SNTP request running - set the timezone - (implicit) respect daylight saving time - how to "read" time to be printed to Serial.Monitor - get date/time from RTC until first NTP - Update date/time of RTC if NTP was successful This example is a stripped down version of the NTP-TZ-DST (v2) from the ESP8266 based on the idea of https://forum.arduino.cc/t/set-ds3231-with-ntp-server/939845/9 Hardware: NodeMCU / ESP8266 and DS3231 wiring D0 GPI16 (use for reset after deepsleep) D1 GPIO5 - I2C SCL D2 GPIO4 - I2C SDA Dependencies: ESP8266 Core 3.0.2 (we need at least 3.0.0 because of the parameter in the callback) by noiasca https://werner.rothschopf.net/microcontroller/202112_arduino_esp_ntp_rtc_en.htm 2021-12-30 OK */ #if ARDUINO_ESP8266_MAJOR < 3 #pragma message("This sketch requires at least ESP8266 Core Version 3.0.0") #endif #ifndef STASSID #define STASSID "your-ssid" // set your SSID #define STAPSK "your-password" // set your wifi password #endif /* Configuration of NTP */ // choose the best fitting NTP server pool for your country #define MY_NTP_SERVER "at.pool.ntp.org" // choose your time zone from this list // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv #define MY_TZ "CET-1CEST,M3.5.0/02,M10.5.0/03" // Berlin, Vienna, Rom, ... /* Necessary includes */ #include <ESP8266WiFi.h> // we need wifi to get internet access #include <time.h> // for time() ctime() ... #include <coredecls.h> // optional settimeofday_cb() callback to check on server #include <RTClib.h> // Adafruit 2.0.2 (1.12.5 also tested) for RTC like DS3231 RTC_DS3231 rtc; /* Sets the internal time epoch (seconds in GMT) microseconds */ void setInternalTime(uint64_t epoch = 0, uint32_t us = 0) { struct timeval tv; tv.tv_sec = epoch; tv.tv_usec = us; settimeofday(&tv, NULL); } /* prints an one digit integer with a leading 0 */ void print10(int value) { if (value < 10) Serial.print("0"); Serial.print(value); } /* ESP8266 has no timegm, so we need to create our own... Take a broken-down time and convert it to calendar time (seconds since the Epoch 1970) Expects the input value to be Coordinated Universal Time (UTC) Parameters and values: - year [1970..2038] - month [1..12] ! - start with 1 for January - mday [1..31] - hour [0..23] - min [0..59] - sec [0..59] Code based on https://de.wikipedia.org/wiki/Unixzeit example "unixzeit" */ int64_t getTimestamp(int year, int mon, int mday, int hour, int min, int sec) { const uint16_t ytd[12] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; /* Anzahl der Tage seit Jahresanfang ohne Tage des aktuellen Monats und ohne Schalttag */ int leapyears = ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400; /* Anzahl der Schaltjahre seit 1970 (ohne das evtl. laufende Schaltjahr) */ int64_t days_since_1970 = (year - 1970) * 365 + leapyears + ytd[mon - 1] + mday - 1; if ( (mon > 2) && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ) days_since_1970 += 1; /* +Schalttag, wenn Jahr Schaltjahr ist */ return sec + 60 * (min + 60 * (hour + 24 * days_since_1970) ); } /* print time of RTC to Serial */ void printRTC() { DateTime dtrtc = rtc.now(); // get date time from RTC i if (!dtrtc.isValid()) { Serial.println(F("E103: RTC not valid")); } else { time_t newTime = getTimestamp(dtrtc.year(), dtrtc.month(), dtrtc.day(), dtrtc.hour(), dtrtc.minute(), dtrtc.second()); Serial.print(F("RTC:")); Serial.print(newTime); Serial.print(" "); Serial.print(dtrtc.year()); Serial.print("-"); print10(dtrtc.month()); Serial.print("-"); print10(dtrtc.day()); Serial.print(" "); print10(dtrtc.hour()); Serial.print(":"); print10(dtrtc.minute()); Serial.print(":"); print10(dtrtc.second()); Serial.println(F(" UTC")); // remember: the RTC runs in UTC } } /* get date/time from RTC and take over to internal clock */ void getRTC() { Serial.println(F("getRTC --> update internal clock")); DateTime dtrtc = rtc.now(); // get date time from RTC i if (!dtrtc.isValid()) { Serial.print(F("E127: RTC not valid")); } else { time_t newTime = getTimestamp(dtrtc.year(), dtrtc.month(), dtrtc.day(), dtrtc.hour(), dtrtc.minute(), dtrtc.second()); setInternalTime(newTime); //Serial.print(F("UTC:")); Serial.println(newTime); printRTC(); } } /* set date/time of external RTC */ void setRTC() { Serial.println(F("setRTC --> from internal time")); time_t now; // this are the seconds since Epoch (1970) (UTC) tm tm; // the structure tm holds time information in a convenient way time(&now); // read the current time and store to now gmtime_r(&now, &tm); // update the structure tm with the current GMT rtc.adjust(DateTime(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec)); } void time_is_set(bool from_sntp) { if (from_sntp) // needs Core 3.0.0 or higher! { Serial.println(F("The internal time is set from SNTP.")); setRTC(); printRTC(); } else { Serial.println(F("The internal time is set.")); } } /* optional: by default, the NTP will be started after 60 secs lets start at a random time in 5 seconds */ uint32_t sntp_startup_delay_MS_rfc_not_less_than_60000() { randomSeed(A0); return random(5000 + millis()); } /* opional: set SNTP interval */ /* uint32_t sntp_update_delay_MS_rfc_not_less_than_15000 () { return 60 * 1000UL; // 12 hours } */ /* print local time to serial */ void showTime() { time_t now; // this are the seconds since Epoch (1970) GMT tm tm; // a readable structure time(&now); // read the current time and store to now localtime_r(&now, &tm); // update the structure tm with the current time char buf[50]; strftime(buf, sizeof(buf), " %F %T %Z wday=%w", &tm); // https://www.cplusplus.com/reference/ctime/strftime/ Serial.print("now:"); Serial.print(now); // in UTC! Serial.print(buf); if (tm.tm_isdst == 1) // Daylight Saving Time flag Serial.print(" DST"); else Serial.print(" standard"); Serial.println(); } void setup() { Serial.begin(115200); Serial.println("\n\nNTP TZ DST - RTC Fallback"); if (!rtc.begin()) Serial.println(F("Couldn't find RTC")); else Serial.println(F("RTC found")); configTime(MY_TZ, MY_NTP_SERVER); // --> for the ESP8266 you only need this for Timezone and DST // start network WiFi.persistent(false); WiFi.mode(WIFI_STA); WiFi.begin(STASSID, STAPSK); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(200); static uint16_t counter = 0; counter++; if (counter % 30 == 0) Serial.println(); } Serial.println(F("\nWiFi connected")); settimeofday_cb(time_is_set); // register callback if time was sent if (time(nullptr) < 1600000000) getRTC(); // Fallback to RTC on startup if we are before 2020-09-13 } void loop() { showTime(); delay(1000); // dirty delay }
Remeber to set the NTP server pool according to your location:
#define MY_NTP_SERVER "at.pool.ntp.org" // set the best fitting NTP server (pool) for your location
Remarks for the ESP8266 and Dependencies
The sketch uses the callback when the time was set. Only if the time was set by SNTP, we will also update the date/time of the RTC. Therefore we need at least ESP Core 3.0.0. The development was done on a NodeMCU with ESP8266-12E and ESP Core 3.0.2. For the DS3231 I'm using the RTClib from Adafruit Version 2.0.2 (and did a test with the older 1.12.5 which worked also).
History
first upload: 2021-12-30 | Version: 2023-08-12