Projects »

ESP32 low-power accurate clock using network time

Calibrated time-keeping library for ESP32 deep sleep modes, without added hardware or increased power consumption

For projects that need to keep (reasonably) accurate time there are a number of options, from just setting the clock once and trusting the hardware, to fetching the latest time from a remote NTP server many times a day. One of my projects maximises battery life by using an ESP32 in deep sleep mode and only goes online for less than one second each day. Unfortunately the low power clock in the ESP32 is particularly bad at keeping time, so this article looks at some ways to minimise the clock drift, including an open-source calibration library.

Simple calibration improved the ESP32 clock drift from 8 minutes per day to just 8 seconds per day

I’m writing an article about my ESP32 IoT with 3× temperature+humidity sensors project (coming soon) that uses this clock calibration library.

Clock source options

The ESP32 has 2 built-in clock sources: the default 150kHz oscillator, and a 8.5MHz which has much less drift but reduces sleep battery life by 33%. Alternatively you can use an external clock signal from an crystal (XTAL) oscillator such as the ABS07-120-32.768KHZ-T, which can reduce the extra current needed but the crystal plus associated components take up PCB space and of course add cost. More stable than a simple crystal (but more expensive) is a temperature-compensated oscillator (TCXO) like the SIT1552AI-JE-DCC-32.768E. However only the default oscillator can be used within the Arduino runtime, so to replace it you have to build your project with esp-idf.

Instead of replacing the default oscillator, you can regularly fetch the time from a more stable device such as the DS3231MZ. This dedicated RTC chip provides access to its internal TCXO over an I2C interface. Reading/writing the time does take more current (100µA) but it is so quick that it could be done many times a day without a significant increase in average current. I use this chip in an outdoor project that has its time set once, then runs without WiFi for up to 9 months for a total clock drift of only a few minutes. Of course it doesn’t matter how ‘perfect’ it is for a project when the Great Chip Shortage of 2022 means it’s shot up in price and out of stock for the next 8 months... :/

PCB render of ESP32 with DS3231MZ clock chip (highlighted) Ytterbium optical lattice atomic clock © Burrus/NIST, 2013

Finally, the most stable clock source are NTP servers that use signals generated from atomic clocks and distribute them across the Internet. However going online with WiFi is very expensive in terms of current, so battery-based projects will want to use NTP as infrequently as possible.

Clock sourceESP32
default
ESP32
calibrated
ESP32
8.5MHz
+Crystal
oscillator
+TCXO
oscillator
+clock IC
DS3231MZ
+NTP server
WiFi
Drift per day ±480 sec8 sec1.7 sec1.7 sec0.4 sec0.4 sec0.0 sec
Drift ppm ±5600932020550
Extra current0 µA0 µA5 µA2 µA2 µA2 µA180000 µA
Extra components---424-
Extra cost---£1.40£2.32£6.18-

The above table gives some typical numbers, but these vary with the exact components and the temperature range your project experiences. For reference, the ESP32 itself uses 10µA in deep sleep.

There are other options of course, such as using a GPS or LoRaWAN module and these come with their own trade-offs of cost, physical size and power consumption.

Auto-calibrating the ESP32 clock

The default 150kHz oscillator inside each ESP32 will have manufacturing variation, as well as vary with the age of the chip, current temperature and input voltage. Even with a stable temperature & voltage, you can still have 8 minutes of clock drift each day which will be too large for some projects.

My simple library goes online very briefly each day at midnight to contact an NTP server and compares the ESP32’s internal clock with the actual (NTP) time elapsed since the previous connection. This gives a numeric calibration factor to apply to the next 24 hours. Instead of adjusting the internal clock multiple times during the day, it is allowed to drift but the calibration factor is used to calculate longer or shorter sleep periods such that the ESP32 wakes up at the correct actual time. This simple calibration improved the ESP32 clock drift from 8 minutes per day to just 8 seconds per day, without any additional hardware or increase in power consumption. It obviously can’t match the very low drifts of temperature-compensated oscillators, but it may be “good enough” for your project.

serial.log

23:59:56.743 -> Wake #0 midnight 23:59:56.946 -> WiFi connect took 0.223 sec 23:59:58.948 -> NTP+runtask took 2.001 sec, calibrated drift -3.2 sec (-37 ppm) 23:59:58.948 -> Sleeping... 00:14:59.461 -> Wake #1, internal drift 4.9 sec 00:15:01.465 -> Sleeping... 00:29:58.900 -> Wake #2, internal drift 9.7 sec 00:30:00.897 -> Sleeping... ... 23:29:57.925 -> Wake #94, internal drift 452.8 sec 23:29:59.929 -> Sleeping... 23:44:58.087 -> Wake #95, internal drift 457.7 sec 23:45:00.088 -> Sleeping... 23:59:58.288 -> Wake #0 midnight 23:59:58.491 -> WiFi connect took 0.189 sec 00:00:00.489 -> NTP+runtask took 2.001 sec, calibrated drift -1.6 sec (-18 ppm)

Auto-calibrating at run-time means there is no need to calibrate individual ESP32s and hard-code a different calibration factor into the firmware: after a full 24 hour cycle each chip adjusts to its oscillator and the local temperature. And if changing weather means the average daily temperature becomes colder or warmer, then the calibration adjusts.

Possible improvement? You could record the clock drift at the min/max/average temperatures your project will experience, and continuously adjust the clock calibration (on a curve) based on the current temperature. Whether this would give a useful improvement over the simple “today will be similar to yesterday” algorithm I don’t know. It might be worth it if your project is outdoors and so experiences a wide temperature range?

Efficient NTP fetches

The daily fetch from a remote NTP server can be optimised to improve battery life. The slowest option is to connect to a distant NTP server by name (eg, pool.ntp.org), which involves a DNS lookup as well as a slower response. Your ISP likely has a public NTP server that will be closer, and it is even faster if you hard-code the server IP address to avoid the DNS lookup (although the sketch will stop working if they change their network). Fastest of all is running an NTP server on your local network (eg, on a Raspberry Pi) where you can guarantee a fixed IP address.

In practice the NTP fetch works in the background, so your sketch gets on with executing runTask() without waiting for the fetch to complete.

sleepy.ino

const char* ntpServer = "pool.ntp.org"; // 0.300 sec. Slowest, finds a 'nearby' NTP server in your country/region //const char* ntpServer = "ntp.my-isp.net"; // 0.200 sec. Your ISP's NTP server //const char* ntpServer = "110.251.16.270"; // 0.045 sec. Your ISP's NTP server, no DNS //const char* ntpServer = "192.168.1.120"; // 0.010 sec. Fastest, NTP server on local network, no DNS

Installing NTP on a local machine is simple, for example Using NTP on Linux with Chrony.

Sleepy library

This open-source library combines the ESP32 deep-sleep modes with a calibrated clock, making it very easy to write sketches that wake up at accurate times of day to perform tasks, between sleeping for minutes or hours. Rather than sleep for a fixed number of minutes, it divides each day into time slots of SLEEPY_PERIOD (eg, 15 minutes) and wakes up the ESP32 at 12:00pm, 12:15pm, 12:30pm, etc. So it doesn’t matter if your tasks run for 1 second or 1 minute, the ESP32 will then sleep until the next time slot.

test_sleepy.ino

const uint16_t SLEEPY_PERIOD = 900; // seconds // Called at power on, and each wake from sleep void setup(){ if (sleepyEndDay()) { wifiConnect(); sleepyCalibrate(runTask); // Your end-of-day tasks here } else { runTask(); } sleepyNext(); // Doesn't return } // Your regular task here, called every SLEEP_PERIOD void runTask(){ }

The internal clock is left to drift during the day, so time() will report the wrong time (except at midnight, after the NTP fetch). Instead call sleepyTime() for the calibrated time, which you can then pass to other standard time/date functions:

time_t calibrated = sleepyTime(NULL);
Serial.printf("Calibrated time/date is %s", ctime(&calibrated));

The library uses 16 bytes of RTC memory and adds 1900 bytes to your compiled sketch, or 1250 bytes if SLEEPY_VERBOSE is commented out (removes messages logged to the serial port).

Deep Sleep vs Hibernation mode

By default the library uses the ESP32 deep sleep mode (10µA) with variables stored in RTC memory. You can uncomment SLEEPY_HIBERNATE and the ESP32 will instead go into hibernation mode (5µA) and use NVS (flash memory) because the RTC memory is powered-down. The flash is only written to once a day, so there will be no issues with it wearing out.

Possible improvements?

Download files


Fb Share this on Facebook
Nikki Smith, January 2022.