In the course of developing an ESP32 battery based application I was stumped along with others, on how to maintain a GPIO output in a known state while in deep sleep. My plan was to hold an external peripheral in a reset state to minimize it’s power. The reset signal was an active low signal and the external peripheral had a 10K pullup resistor on the signal. I program the ESP32 in the Arduino environment so I had to find a solution that supported this IDE. I performed a bunch of searches and finally found a working example by Xabi Alaez.
The ESP32 has several “sleep” modes that are listed below. A great article about ESP32 sleep modes is found on the Last Minute Engineers web page.
- Active keeps everything running including WiFi, Bluetooth, and processing core at all times. This mode consumes more than 240mA with some power spikes as high as 790mA.
- Modem Sleep is Active minus the WiFi, Bluetooth, and radio. Dependent on the clock speed, which is programmable, consumes between 3mA (low clock speed) to 20mA (high clock speed).
- Light Sleep the CPU(s) clock is gated while the RTC and ULP co-processor are active. This sleep mode has full RAM retention. Once entering Light Sleep a wake-up source is needed to resume processing. This power consumption is less than 1mA.
- Deep Sleep the CPU(s), most RAM, and all digital peripherals are powered off while the ULP co-processor and RTC components remain operational. The power consumption is about 0.15mA with the ULP co-processor running.
- Hibernation only has one RTC timer running and a few RTC GPIOs needed to wake up the chip from hibernation mode. Power consumption is about 2.5uA.
I was most interested in Deep Sleep and Hibernation modes. It turns out you control the power domains before entering Deep Sleep to create Hibernation mode (no API call for hibernation sleep). This also means you can be selective in which power domains are enabled to create a “custom” sleep mode that has less current consumption than Deep Sleep, but not as good as Hibernation.
The ESP power domains are controlled by calling esp_deep_sleep_pd_config() with the power domain and options that include ESP_PD_OPTION_OFF, ESP_PD_OPTION_ON, and ESP_PD_OPTION_AUTO. The auto option powers down the power domain unless it is needed by one of the wakeup options. The different ESP power domains are:
- ESP_PD_DOMAIN_RTC_PERIPH for RTC IO, sensors and ULP co-processor.
- ESP_PD_DOMAIN_RTC_SLOW_MEM for RTC slow memory.
- ESP_PD_DOMAIN_RTC_FAST_MEM for RTC fast memory.
- ESP_PD_DOMAIN_XTAL for XTAL oscillator.
- ESP_PD_DOMAIN_RTC8M for internal 8M oscillator.
- ESP_PD_DOMAIN_VDDSDIO for VDD_SDIO.
During Deep Sleep the only GPIO pins that are available are the RTC IO (RTC_GPIO), which are part of the ESP_PD_DOMAIN_RTC_PERIPH power domain. The ULP co-processor is also active in this power domain. The table below shows the mapping of GPIO pins to analog and RTC functions available through the RTC Mux. The table information is from the Espressif Systems ESP32 Technical Reference Manual Version 4.2, section 5.11, table 21.
GPIO Num | RTC GPIO Num | Analog 1 | Analog 2 | Analog 3 | RTC 1 | RTC 2 |
36 | 0 | ADC_H | ADC1_CH0 | – | RTC_GPIO0 | – |
37 | 1 | ADC_H | ADC1_CH1 | – | RTC_GPIO1 | – |
38 | 2 | ADC_H | ADC1_CH2 | – | RTC_GPIO2 | – |
39 | 3 | ADC_H | ADC1_CH3 | – | RTC_GPIO3 | – |
34 | 4 | – | ADC1_CH6 | – | RTC_GPIO4 | I2C_SCL |
35 | 5 | – | ADC1_CH7 | – | RTC_GPIO5 | I2C_SDA |
25 | 6 | DAC_1 | ADC2_CH8 | – | RTC_GPIO6 | I2C_SCL |
26 | 7 | DAC_2 | ADC2_CH9 | – | RTC_GPIO7 | I2C_SDA |
33 | 8 | XTAL_32K_N | ADC1_CH5 | TOUCH8 | RTC_GPIO8 | – |
32 | 9 | XTAL_32K_P | ADC1_CH4 | TOUCH9 | RTC_GPIO9 | – |
4 | 10 | – | ADC2_CH0 | TOUCH0 | RTC_GPIO10 | – |
0 | 11 | – | ADC2_CH1 | TOUCH1 | RTC_GPIO11 | – |
2 | 12 | – | ADC2_CH2 | TOUCH2 | RTC_GPIO12 | – |
15 | 13 | – | ADC2_CH3 | TOUCH3 | RTC_GPIO13 | – |
13 | 14 | – | ADC2_CH4 | TOUCH4 | RTC_GPIO14 | – |
12 | 15 | – | ADC2_CH5 | TOUCH5 | RTC_GPIO15 | – |
14 | 16 | – | ADC2_CH6 | TOUCH6 | RTC_GPIO16 | – |
27 | 17 | – | ADC2_CH7 | TOUCH7 | RTC_GPIO17 | – |
To setup the RTC Mux and control the RTC GPIOs in the Arduino environment, calls are made to rtc_gpio_* functions. These functions allow you to initialize the pin as RTC GPIO and set it’s direction and level. My test code adapted from Xabi Alaez’s is shown below.
#include "driver/rtc_io.h"
gpio_num_t RST_PIN = GPIO_NUM_14;
void setup() {
rtc_gpio_init(RST_PIN);
rtc_gpio_set_direction(RST_PIN,RTC_GPIO_MODE_OUTPUT_ONLY);
// toggle reset pin
rtc_gpio_set_level(RST_PIN,1); //GPIO high
rtc_gpio_set_level(RST_PIN,0); //GPIO LOW
rtc_gpio_set_level(RST_PIN,1); //GPIO high
delay(5000); // wait
rtc_gpio_set_level(RST_PIN,0); //GPIO hold low
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_enable_timer_wakeup(60*1000*1000); // 60 seconds
esp_deep_sleep_start();
}
void loop() {
// none
}
The first step calls rtc_gpio_init(gpio_num_t gpio_num) with the GPIO pin number to the initialize the pad for an analog/RTC function. Next, rtc_gpio_set_direction(gpio_num_t gpio_num, rtc_gpio_mode_t mode) is used to set the pin as output. Finally rtc_gpio_set_level(gpio_num_t gpio_num, uint32_t level level) is used to set the pin output as either HIGH (1) or LOW (0).
After the pin setup and toggled, a delay of 5 seconds is introduced before setting the pin to the value needed during Deep Sleep. Note that the ESP_PD_DOMAIN_RTC_PERIPH is enabled prior to setting the wake up timer and entering Deep Sleep. After waking from Deep Sleep, setup() is repeated again.
Although this approach worked well, I was trying to save 5uA on an external peripheral. By enabling the RTC peripheral that includes the ULP co-processor, I added about 100uA+ to the ESP power. So in the end I didn’t use this approach.
The key to setting an ESP32 output to a known state during Deep Sleep is to select an RTC_GPIO, initialize and operate the pin using RTC_GPIO_* calls, and enabling the RTC peripheral power domain prior to entering Deep Sleep.