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
Digital Core ~25mA active
RTC Fast Memory ~500µA retention
RTC Slow Memory ~2µA retention
RTC Peripherals ~150µA
RTC Core ~6µA
CPU, caches, SRAM
8KB, code execution
8KB, variable storage
ULP, touch, ADC
RTC timer, wakeup
Digital Core ~25mA active
RTC Fast Memory ~500µA retention
RTC Slow Memory ~2µA retention
RTC Peripherals ~150µA
RTC Core ~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
31272319151173
Reserved
DG_WRAP_FORCE_PU
DG_WRAP_FORCE_PD
WIFI_FORCE_PU
WIFI_FORCE_PD
INTER_RAM4_FORCE_PU
INTER_RAM4_FORCE_PD
...
[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);
}
⚠️ ULP Compatibility

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-ups

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
31272319151173
FAST_CLK_RTC_SEL
ANA_CLK_RTC_SEL
CK8M_DIV_SEL
CK8M_DIV_SEL_VLD
...
[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

ConfigurationCurrentWake 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
Note: ESP32-WROOM-32, VDD=3.3V, 25°C ambient

Power vs Wake Time Trade-off

(µA)
Fast Wake (default) 280ms wake
147 µA
Balanced 350ms wake
43 µA
Low Power 420ms wake
12 µA
Minimum Power 420ms wake
7 µA

Common Leakage Sources

If you’re still above 10µA, check these sources:

  1. PSRAM (if equipped): Adds 20-40µA in retention mode
  2. USB-UART bridge: CP2102/CH340 can draw 1-5mA from 3.3V rail
  3. LDO quiescent current: AMS1117 draws 5mA; use HT7333 (3µA)
  4. LED power indicator: 1-20mA depending on resistor value
  5. 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.