The ESP32 datasheet claims 10µA deep sleep current. In practice, most developers see 150-500µA. This gap stems from undocumented power domains, peripheral leakage, and default configurations optimized for quick wake-up rather than minimum power. Let’s fix that.
Power Domain Architecture
The ESP32 contains multiple independently-controllable power domains:
ESP32 Power Domain Hierarchy
Domain 0 Domain 1 Domain 2 Domain 3 Domain 4 ~25mA active ~500µA retention ~2µA retention ~150µA ~6µA The critical insight: default deep sleep keeps Domain 1 (RTC Fast Memory) powered for fast wake-up. This alone adds 500µA.
The RTC_CNTL Register Set
Power management is controlled through the RTC_CNTL peripheral. The key registers:
RTC_CNTL_DIG_PWC_REG 0x3FF48090 [31:22] Reserved Reserved [21] DG_WRAP_FORCE_PU Force digital wrapper power up [20] DG_WRAP_FORCE_PD Force digital wrapper power down [19] WIFI_FORCE_PU Force WiFi power up [18] WIFI_FORCE_PD Force WiFi power down [17] INTER_RAM4_FORCE_PU Force internal SRAM 4 power up [16] INTER_RAM4_FORCE_PD Force internal SRAM 4 power down [15:0] ... Additional RAM controls Systematic Power Reduction
Step 1: Disable RTC Fast Memory (Save ~500µA)
#include "soc/rtc_cntl_reg.h"
#include "soc/rtc.h"
void configure_rtc_memory_powerdown() {
// Disable RTC Fast Memory retention during deep sleep
// This prevents ULP program execution but saves ~500µA
REG_CLR_BIT(RTC_CNTL_PWC_REG, RTC_CNTL_FASTMEM_FORCE_PU);
REG_SET_BIT(RTC_CNTL_PWC_REG, RTC_CNTL_FASTMEM_FORCE_PD);
// Verify the change
uint32_t pwc_reg = REG_READ(RTC_CNTL_PWC_REG);
assert((pwc_reg & RTC_CNTL_FASTMEM_FORCE_PU) == 0);
assert((pwc_reg & RTC_CNTL_FASTMEM_FORCE_PD) != 0);
}
Powering down RTC Fast Memory prevents ULP coprocessor program execution during deep sleep. If you need ULP functionality, you cannot use this optimization.
Step 2: Disable RTC Peripherals (Save ~150µA)
void disable_rtc_peripherals() {
// Power down touch sensor controller
REG_SET_BIT(RTC_CNTL_PWC_REG, RTC_CNTL_TOUCH_FORCE_PD);
// Power down SAR ADC
REG_SET_BIT(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR);
REG_CLR_BIT(SENS_SAR_MEAS_WAIT2_REG, SENS_FORCE_XPD_SAR);
// Disable brown-out detector during sleep (risky but saves ~10µA)
// Only do this if your power supply is stable
REG_SET_BIT(RTC_CNTL_BROWN_OUT_REG, RTC_CNTL_BROWN_OUT_PD_RF_ENA);
}
Step 3: Configure GPIO Isolation (Save 10-100µA)
GPIO leakage is often the hidden power thief:
void configure_gpio_isolation() {
// Isolate all GPIOs during deep sleep
esp_sleep_config_gpio_isolate();
// For each GPIO, explicitly set the hold state
for (int i = 0; i < GPIO_NUM_MAX; i++) {
if (GPIO_IS_VALID_GPIO(i)) {
// Enable hold during deep sleep
gpio_hold_en((gpio_num_t)i);
// Configure as input with pull-down to prevent floating
gpio_set_direction((gpio_num_t)i, GPIO_MODE_INPUT);
gpio_set_pull_mode((gpio_num_t)i, GPIO_PULLDOWN_ONLY);
}
}
// Critical: Enable deep sleep hold for ALL GPIOs
gpio_deep_sleep_hold_en();
}
External pull-up resistors on GPIOs configured as inputs with internal pull-down create a resistor divider drawing continuous current. A 10kΩ pull-up to 3.3V draws 165µA per GPIO!
Step 4: Clock Configuration
The RTC uses an internal 150kHz oscillator by default. The 32kHz external crystal is more power-efficient:
void configure_rtc_clock() {
// Use external 32.768kHz crystal (lower power, more accurate)
rtc_clk_32k_enable(true);
// Wait for crystal to stabilize
rtc_clk_32k_bootstrap(512);
// Switch RTC clock source
rtc_clk_slow_src_set(RTC_SLOW_FREQ_32K_XTAL);
// Verify switch was successful
rtc_slow_freq_t current = rtc_clk_slow_src_get();
assert(current == RTC_SLOW_FREQ_32K_XTAL);
}
RTC_CNTL_CLK_CONF_REG 0x3FF48070 [31:30] FAST_CLK_RTC_SEL Fast clock source: 0=XTAL_DIV, 1=CK8M_D256 [29:28] ANA_CLK_RTC_SEL Slow clock source: 0=RC, 1=32K_XTAL, 2=8M_D256 [27] CK8M_DIV_SEL CK8M divider select [26:24] CK8M_DIV_SEL_VLD Divider valid [23:0] ... Additional clock controls The Complete Low-Power Configuration
#include "esp_sleep.h"
#include "driver/rtc_io.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"
void enter_minimum_power_deep_sleep(uint64_t sleep_duration_us) {
// Step 1: Configure wake-up source
esp_sleep_enable_timer_wakeup(sleep_duration_us);
// Step 2: Disable RTC Fast Memory
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
// Step 3: Disable RTC Slow Memory (if not using RTC variables)
// WARNING: This loses all RTC_DATA_ATTR variables
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
// Step 4: Disable RTC peripherals
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF);
// Step 5: Configure GPIOs
for (int i = 0; i < GPIO_NUM_MAX; i++) {
if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) {
rtc_gpio_isolate((gpio_num_t)i);
}
}
// Step 6: Disable WiFi/BT completely
esp_wifi_stop();
esp_bt_controller_disable();
// Step 7: Enter deep sleep
esp_deep_sleep_start();
}
Measurement Results
Using a Keithley 6514 electrometer with 100ms integration time:
ESP32 Deep Sleep Current Measurements
| Configuration | Current | Wake Time |
|---|---|---|
| Default deep sleep | 147.3 µA | 280ms |
| + Disable Fast Mem | 42.1 µA | 350ms |
| + Disable Slow Mem | 28.4 µA | 380ms |
| + Disable RTC Periph | 12.8 µA | 420ms |
| + GPIO Isolation | 8.7 µA | 420ms |
| + 32kHz XTAL | 6.9 µA | 420ms |
Power vs Wake Time Trade-off
(µA)Common Leakage Sources
If you’re still above 10µA, check these sources:
- PSRAM (if equipped): Adds 20-40µA in retention mode
- USB-UART bridge: CP2102/CH340 can draw 1-5mA from 3.3V rail
- LDO quiescent current: AMS1117 draws 5mA; use HT7333 (3µA)
- LED power indicator: 1-20mA depending on resistor value
- External sensors: I2C devices often have 1-100µA standby draw
Oscilloscope Validation
To validate sleep current, use current probe measurements:
# Sample measurement setup
# - Keysight MSOX3104T oscilloscope
# - Keysight N2820A current probe (10mA range)
# - 1Ω shunt resistor for cross-validation
# Capture deep sleep entry transition
python capture_power_profile.py --duration 10s --rate 100kHz --output sleep_entry.csv
The current waveform should show:
- Sharp drop from ~40mA to ~10µA over ~100ms
- Stable plateau at target current
- No periodic spikes (would indicate timer or peripheral activity)
Conclusion
Achieving datasheet-specified deep sleep current requires understanding the ESP32’s power domain architecture and systematically disabling unused subsystems. The trade-off is increased wake-up latency (280ms → 420ms), which may be acceptable for battery-powered IoT applications sleeping for minutes or hours.
For truly battery-powered applications, consider the ESP32-S2 or ESP32-C3 which achieve 5µA deep sleep with default configurations.