ESP8266 (ESP-12F) deep sleep and light sleep with Arduino
02 Apr 2021 | all notes
Tutorials on the ESP8266’s sleep modes typically focus on the differences in chip activity states and power consumption between modes, but I found them lacking when it came to documenting their respective support/usage of external wake-up vs. timer-based sleep. This document summarizes my empirical findings/insights into less well-documented aspects of deep sleep and (so-called ‘forced’) light sleep, with code examples.
Contents
- Code continuation after wake-up
- Output pin states during sleep
- External (hardware) wake-up
- Timer-based wake-up
- Determining the reason for waking
- Time-keeping during/across sleep
- Sources & links
Code continuation after wake-up
The Wi-Fi modem is turned off in all sleep modes, so it is necessary to restart/reconnect the WiFi when waking up from any sleep mode.
- deep sleep:
- waking up from deep sleep is through resetting the chip, code starts again at the beginning of
setup()
- the only memory that is maintained between resets is that of the RTC chip, including information about the cause of the last reset (external or timer-based wake-up). Up to 508 bytes of user memory can be used to preserve data between sleep cycles (see Arduino documentation of
ESP.rtcUserMemoryWrite()
/ESP.rtcUserMemoryRead()
and the handy RTCMemory library; the Low Power Demo usesmemcpy
andmemset
to write/read to RTC)
- waking up from deep sleep is through resetting the chip, code starts again at the beginning of
- light sleep:
- light sleep only suspends the CPU, so after waking up the code continues to run where it left off
- light sleep is entered fast, so you might want to
flush()
any pending output streams before entering it - optionally one can register a callback method that is invoked on waking (this is run just before the main code continues to execute)
- in order to get timer-based light sleep to work correctly, this callback is actually mandatory (see below)
// registering a on-wakeup callback
void fpm_wakup_cb_func1() {
// ok to use blocking functions in the callback, but not
// delay(), which appears to cause a reset
Serial.println("Light sleep is over");
}
wifi_fpm_set_wakeup_cb(fpm_wakup_cb_func1);
Output pin states during sleep
- deep sleep: does not maintain output pin states during sleep
D1
andD2
are pulledLOW
D4
-D8
are pulledHIGH
D3
outputs 2V
- light sleep:
- maintains output pin states during sleep but PWM is not working
- any pin which was outputting a PWM signal at the time of entering light sleep will stay stuck in whatever digital state it was in at the time of CPU shutdown (i.e. permanently either
HIGH
orLOW
)
External (hardware) wake-up
- deep sleep
- waking up from deep sleep is done by resetting the chip via the
RESET
pin. This pin is internally pulledHIGH
and many sources state that reset is triggered by pulling itLOW
, but according to my own tests it’s actually the finalLOW-to-HIGH
transition after having pulled itLOW
that causes the reset. - the
RESET
pin behaviour is fixed in the hardware, i.e. it is not possible to deactive it while the chip is running! Whatever external device normally triggers wake-up from deep sleep is responsible for not sending the same signal during normal runtime. If you have a stupid external component (e.g. PIR sensor), you might want to use light sleep instead. - the Arduino API method for entering deep sleep requires specification of a timeout, passing a value of
0
disconnects the timer so that the chip will remain in deep sleep indefinitely, until it is woken through an external reset. - an optional second
mode
argument allows you to control the state of the WiFi (RF) chip on wake (default ofRF_DEFAULT
seems to work fine for most cases, see Arduino docs for available modes)
- waking up from deep sleep is done by resetting the chip via the
// minimal example for entering interrupt-based deep sleep
// no #include necessary
// enter deep sleep
ESP.deepSleep(0);
// enter deep sleep, specifying the desired state of the WiFi chip
// on wakeup (not usually necessary, for options see link above)
//ESP.deepSleep(0, RF_DEFAULT);
// ESP.deepSleep() waits for WiFi chip shutdown before going to sleep.
// use ESP.deepSleepInstant() to go to sleep without waiting for shutdown
//ESP.deepSleepInstant(0);
//ESP.deepSleepInstant(0, RF_DEFAULT);
- light sleep
- programmable during run-time – GPIO pins
D1
throughD8
can be dynamically configured to cause the chip to exit light sleep when the pin detects a configurable input state - it is possible to configure wake interrupts on more than one pin at the same time, and with different target states (see code example below)
- waking is based on states, not transitions: when the pin is already in the target state, the call to light sleep returns and continues normal execution after ~20ms (on NodeMCU there is already support for interrupting based on states as well as transitions, current work on implementing the same for Arduino is documented in issue esp8266/Arduino#7055)
- The desired pin state needs to be specified using the
GPIO_PIN_INTR_HILEVEL
andGPIO_PIN_INTR_LOLEVEL
constants (don’t just use Arduino’sHIGH
andLOW
, they have different values!) - there is also
GPIO_ID_PIN(number)
wrapper for resolving pin numbers, but this seems to return the same thing as the ArduinoD1
,D2
,… constants.
- programmable during run-time – GPIO pins
// minimal example for entering interrupt-based light sleep
// include required for LIGHT_SLEEP_T, among others
#include "user_interface.h"
// enable light sleep
wifi_fpm_set_sleep_type(LIGHT_SLEEP_T);
wifi_fpm_open();
// register one or more wake-up interrupts
gpio_pin_wakeup_enable(D2, GPIO_PIN_INTR_HILEVEL);
//gpio_pin_wakeup_enable(D3, GPIO_PIN_INTR_LOLEVEL);
// ...
// function for clearing all previously set wake interrupts:
//gpio_pin_wakeup_disable();
// optionally, can register a callback function using
//wifi_fpm_set_wakeup_cb(function_name);
// actually enter light sleep:
// the special timeout value of 0xFFFFFFF triggers indefinite
// light sleep (until any of the GPIO interrupts above is triggered)
wifi_fpm_do_sleep(0xFFFFFFF);
// the CPU will only enter light sleep on the next idle cycle, which
// can be triggered by a short delay()
delay(10);
// code will continue here after the interrupt
Timer-based wake-up
Both sleep modes support timeout-based wakeup, and in both cases timeout-based wakeup and external wakeup are not mutually exclusive: sleep mode is ended by the specified timeout or an external wakeup signal, whichever occurs first. (Determining the cause of waking up is a different cup of tea, see further below.)
- deep sleep:
- use
ESP.deepSleep(microseconds)
for a maximum ofESP.deepSleepMax()
microseconds- the value of
ESP.deepSleepMax()
can change even during runtime based on changes in temperature to the RTC chip which tracks time during sleep. In practice the longest possible timeout is between 3 and 4 hours (see the cautionary notes in the Arduino LowPowerDemo)
- the value of
- to enable timer-based wake-up, the
RESET
pin needs to be connected to the ‘wake’ pin (D0
/GPIO16
). This pin goesLOW
when the timer runs out, thus waking the chip in the same way that an external wake would do (namely by resetting it) - despite what I wrote above about the final
LOW-to-HIGH
transition actually being responsible for the reset, whenD0
/GPIO16
is disconnected from reset it does not just execute a shortHIGH-LOW-HIGH
transition, but keeps holding theLOW
state, so that connecting it toRESET
later on still manages to trigger a reset. I’m not sure how it does this, possiblyD0
/GPIO16
stays low until theRESET
pin goes low which in turn causesD0
/GPIO16
to go high again which finally triggers theLOW-to-HIGH
transition on the reset pin which actually resets the chip? - when the timer runs out there is some change to the pin states (evidence: some garbage is printed over the Serial channel), but without the connection to the
RESET
pin, no restart happens
- use
// minimal example for entering timer-based deep sleep
// no #include necessary
// ESP.deepSleep() requires a timeout argument, but unless D0/GPIO16 is
// physically connected to the RESET pin, the chip will actually remain
// in deep sleep indefinitely
uint64_t sleepTimeMicroSeconds = 10e6;
// could use up to this, but caution is warranted, see:
// https://github.com/esp8266/Arduino/tree/master/libraries/esp8266/examples/LowPowerDemo#test-10---deep-sleep-instant-wake-with-rf_disabled
//uint64_t sleepTimeMicroSeconds = ESP.deepSleepMax();
// enter deep sleep
ESP.deepSleep(sleepTimeMicroSeconds);
// enter deep sleep, specifying the desired state of the WiFi chip
// on wakeup (not usually necessary, for options see link above)
//ESP.deepSleep(sleepTimeMicroSeconds, RF_DEFAULT);
// ESP.deepSleep() waits for WiFi chip shutdown before going to sleep.
// use ESP.deepSleepInstant() to go to sleep without waiting for shutdown
//ESP.deepSleepInstant(sleepTimeMicroSeconds);
//ESP.deepSleepInstant(sleepTimeMicroSeconds, RF_DEFAULT);
- light sleep: the necessary steps to enter timed sleep are a bit fickle, so read this closely (see esp8266/Arduino#7055 for more details):
- timer-based light sleep can be between ~10.000 and
0xFFFFFFE = 2^28-1 = 268435454
microseconds (~4 1/2 minutes) - for light sleep to be entered successfully:
- the OS timers need to be cleared/disconnected by setting
timer_list = nullptr
- the light sleep command needs to be followed by a
delay()
(specified in milliseconds) that is at least 1ms longer than the sleep time
- the OS timers need to be cleared/disconnected by setting
- for light sleep to be resumed from immediately after a time-out or interrupt (without waiting out the full length of the
delay()
), it is also necessary to:- register a callback function…
- …which takes a minimum amount of computation/IO time (in particular a
Serial.println()
followed bySerial.flush()
seems to do the trick)
- timed sleep is compatible with interrupt-based wakeup, so sleep will be exited prematurely if a previously registered interrupt is triggered
- timer-based light sleep can be between ~10.000 and
// minimal example for entering timer-based light sleep
// include required for LIGHT_SLEEP_T, among others
#include "user_interface.h"
// for timer-based light sleep to work, the os timers need to be disconnected
extern os_timer_t *timer_list;
timer_list = nullptr;
// enable light sleep
wifi_fpm_set_sleep_type(LIGHT_SLEEP_T);
wifi_fpm_open();
void fpm_wakup_cb_func(void) {
Serial.println("Light sleep is over, either because timeout or external interrupt");
Serial.flush();
}
wifi_fpm_set_wakeup_cb(fpm_wakup_cb_func);
// optional: register one or more wake-up interrupts. the chip
// will wake from whichever (timeout or interrupt) occurs earlier
//gpio_pin_wakeup_enable(D2, GPIO_PIN_INTR_HILEVEL);
// sleep for 10 seconds
long sleepTimeMilliSeconds = 10e3;
// light sleep function requires microseconds
wifi_fpm_do_sleep(sleepTimeMilliSeconds * 1000);
// timed light sleep is only entered when the sleep command is
// followed by a delay() that is at least 1ms longer than the sleep
delay(sleepTimeMilliSeconds + 1);
// code will continue here after the time-out (or interrupt)
Determining the reason for waking
- deep sleep:
- if the chip is reset while it is in deep sleep, then the
ESP.getResetReason()
after reboot will return"Deep-Sleep Wake"
, independently of whether the wake was actually trigger by timer timeout, external reset or manually pushing the on-board reset button. I therefore haven’t found a way to effectively distinguish between external and timer-based deep sleep wake-up (i.e. reset) - if the chip is reset during normal operation (e.g. by pressing the onboard button)
ESP.getResetReason()
will return"External System"
- if the chip is reset while it is in deep sleep, then the
- light sleep: there doesn’t seem to be a way to determine whether waking from light sleep was due to an external interrupt or the timer, but there are two heuristic approaches that can give some information:
- rough information about how much time elapsed during light sleep can be obtained from the RTC chip, which can be compared against the originally scheduled sleep duration
- when pin interrupts are configured, one could use rapid
digitalRead()
s on registered pins in the wake-up callback to see if the responsible interrupt signal can still be caught
Time-keeping during/across sleep
As the CPU is turned off in all sleep modes, the value of millis()
is not advanced while the chip is in sleep. Time information can only be based on the (somewhat inaccurate) RTC chip which stays active at all times (as it is also the basis of timer-based waking up).
- deep sleep: waking up resets the chip, which doesn’t just wipe the flash memory but also appears to clear/overwrite the time-keeping part of the RTC memory. The user memory parts of the RTC are preserved between resets, however I can’t think of a use of this that would allow one to figure out the time that the chip spent in deep sleep before the reset/wake-up.
- light sleep: information about how much time the chip spent sleeping before woken up again (by timeout or external interrupt) can be gathered from the following RTC hardware functions. There is +-20% drift based on changes in temperature etc):
// required include
#include "user_interface.h"
uint32_t RTCmillis() {
// system_get_rtc_time() is in us (but very inaccurate anyway)
return (system_get_rtc_time() * (system_rtc_clock_cali_proc() >> 12)) / 1000;
}
Sources & links
- by far the best overview of which pins to use for what is the Random Nerd Tutorials ESP8266 Pinout Reference
- in terms of power consumption for different modes, the documentation of the Arduino LowPowerDemo example contains an expanded table of power consumption that goes beyond the oft-quoted table from the official documentation (see below)
- a detailed analysis of power consumption savings in real life can be found at blog.creations.de
Power consumption during different modes (according to official documentation):