Waking the ESP32-C6 from Deep Sleep Using a Button

Every milliampere saved during idle time directly extends the field life of a battery-powered device. While developers spend considerable effort tuning sleep current and duty cycles, the wakeup trigger itself is worth equal attention. A polling loop that checks a button state keeps the CPU active and running at full current. An interrupt-based approach does better, but the core still has to stay powered. Deep sleep GPIO wakeup takes a different path entirely: the chip powers down to around 5–20 µA, and a single button press brings it back in milliseconds. In this tutorial, you’ll learn how to implement this power-saving approach correctly on the ESP32-C6 DevKitC.

The Wake-Sleep Power Cycle

The ESP32-C6 does not power down uniformly during deep sleep. It maintains a Low Power (LP) domain that stays energized even when the main high-performance core, the main GPIO peripheral, and most other subsystems are off. This LP domain is what makes GPIO wakeup possible — it monitors designated pins continuously while the rest of the chip draws near-zero current.

The implication for wakeup source selection is significant. Only GPIO0 through GPIO7 are connected to the LP domain. Higher-numbered pins belong to the main GPIO peripheral, which powers down completely during deep sleep. A button wired to GPIO10, for example, will never trigger a wakeup regardless of how the firmware configures it. The LP GPIO constraint is a hardware boundary, not a software setting.

There is also a subtlety in how the main GPIO peripheral loses state. When the chip enters deep sleep, every input mode, pull resistor, and interrupt configuration on the main GPIO pins is erased. The firmware that runs after wakeup sees a clean slate. This affects the wakeup pin itself: the pull-up that prevents false triggers during sleep needs to be reconfigured at the start of every boot, not just on first power-up.

Configuring GPIO Wakeup on the ESP32-C6

Setting up button wakeup involves three steps: choosing and wiring the pin, configuring the GPIO, and registering the wakeup source before entering sleep.

Selecting a Wakeup Pin

GPIO0 is a practical choice on the ESP32-C6 DevKitC. It sits in the LP domain, has an internal pull-up resistor, and does not overlap with the JTAG interface pins. Wire a simple push-button between GPIO0 and GND. At rest, the pull-up holds the pin high. Pressing the button drives it low, and that falling edge is what the LP domain detects.

Configuring the GPIO

At the start of app_main(), configure GPIO0 as an input with the internal pull-up enabled. The gpio_config_t struct sets the pin mask, mode, pull direction, and disables the interrupt — the wakeup is handled by the sleep subsystem, not the GPIO interrupt controller. Because this configuration is lost during deep sleep, gpio_config() runs on every boot.

Registering the Wakeup Source

esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown() is the main function to use for deep sleep GPIO wakeup on the ESP32-C6. It takes a bitmask of LP GPIO pins to monitor and a trigger level. Passing BIT(WAKEUP_GPIO) and ESP_GPIO_WAKEUP_GPIO_LOW tells the LP domain to wake the chip when GPIO0 is pulled low. This call runs after the awake period and before esp_deep_sleep_start(). After waking, esp_sleep_get_wakeup_causes() returns a bitmask — checking the ESP_SLEEP_WAKEUP_GPIO bit confirms the source, and esp_sleep_get_gpio_wakeup_status() identifies the specific pin.

Testing the Wakeup Behavior

Before you begin, be sure to add the led_strip component to your project with the following command:

idf.py add-dependency "espressif/led_strip"

Here is an example main.c file you can use to test a full deep sleep and button wakeup cycle on your ESP32-C6 DevKitC. On every boot, it reads the wakeup cause and signals it using the onboard WS2812B RGB LED on GPIO8 — three slow blue flashes for a cold boot, three fast green flashes after a button press. The chip then holds the LED on solid for five seconds before turning it off and re-entering deep sleep.

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_sleep.h"
#include "esp_log.h"
#include "led_strip.h"

// Only GPIO0–GPIO7 are in the LP domain and can trigger deep sleep wakeup
#define WAKEUP_GPIO         GPIO_NUM_0
#define LED_GPIO            8
#define LED_COUNT           1
#define AWAKE_DURATION_MS   5000
#define FLASH_COUNT         3

static const char *TAG = "deep_sleep_gpio";

// Initializes the WS2812B over RMT and clears any residual color from a previous run
static led_strip_handle_t led_init(void)
{
    led_strip_handle_t strip;
    led_strip_config_t strip_cfg = {
        .strip_gpio_num = LED_GPIO,
        .max_leds       = LED_COUNT,
        .led_model      = LED_MODEL_WS2812,
    };
    led_strip_rmt_config_t rmt_cfg = {
        .resolution_hz  = 10 * 1000 * 1000,
        .flags.with_dma = false,
    };
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_cfg, &rmt_cfg, &strip));
    ESP_ERROR_CHECK(led_strip_clear(strip));
    return strip;
}

// refresh() is required to push the pixel buffer to hardware — set_pixel alone does nothing
static void led_set(led_strip_handle_t strip, uint8_t r, uint8_t g, uint8_t b)
{
    ESP_ERROR_CHECK(led_strip_set_pixel(strip, 0, r, g, b));
    ESP_ERROR_CHECK(led_strip_refresh(strip));
}

static void led_off(led_strip_handle_t strip)
{
    ESP_ERROR_CHECK(led_strip_clear(strip));
}

static void led_flash(led_strip_handle_t strip,
                      uint8_t r, uint8_t g, uint8_t b,
                      int count, int on_ms, int off_ms)
{
    for (int i = 0; i < count; i++) {
        led_set(strip, r, g, b);
        vTaskDelay(pdMS_TO_TICKS(on_ms));
        led_off(strip);
        if (i < count - 1) {
            vTaskDelay(pdMS_TO_TICKS(off_ms));
        }
    }
}

// GPIO config does not survive deep sleep — reconfigure on every boot
static void configure_wakeup_gpio(void)
{
    gpio_config_t cfg = {
        .pin_bit_mask   = (1ULL << WAKEUP_GPIO),
        .mode           = GPIO_MODE_INPUT,
        .pull_up_en     = GPIO_PULLUP_ENABLE,
        .pull_down_en   = GPIO_PULLDOWN_DISABLE,
        .intr_type      = GPIO_INTR_DISABLE,
    };
    ESP_ERROR_CHECK(gpio_config(&cfg));
}

void app_main(void)
{
    led_strip_handle_t strip = led_init();

    // Returns a bitmask of wakeup sources; 0 means cold boot or hard reset
    uint32_t causes = esp_sleep_get_wakeup_causes();

    if (causes == 0) {
        ESP_LOGI(TAG, "Cold boot / hard reset");
        led_flash(strip, 0, 0, 255, FLASH_COUNT, 500, 300);
    } else if (causes & BIT(ESP_SLEEP_WAKEUP_GPIO)) {
        // Returns a bitmask of which LP GPIO pins triggered the wakeup
        uint64_t pin_mask = esp_sleep_get_gpio_wakeup_status();
        int pin = (pin_mask != 0) ? __builtin_ffsll(pin_mask) - 1 : WAKEUP_GPIO;
        ESP_LOGI(TAG, "Woke from deep sleep — GPIO%d button press", pin);
        led_flash(strip, 0, 255, 0, FLASH_COUNT, 200, 150);
    } else {
        ESP_LOGI(TAG, "Woke from deep sleep — other cause (0x%lx)", causes);
        led_flash(strip, 255, 255, 255, FLASH_COUNT, 200, 150);
    }

    configure_wakeup_gpio();

    if (causes == 0) {
        led_set(strip, 0, 0, 255);
    } else if (causes & BIT(ESP_SLEEP_WAKEUP_GPIO)) {
        led_set(strip, 0, 255, 0);
    } else {
        led_set(strip, 255, 255, 255);
    }

    ESP_LOGI(TAG, "Staying awake for %d ms...", AWAKE_DURATION_MS);
    vTaskDelay(pdMS_TO_TICKS(AWAKE_DURATION_MS));

    // Tells the LP domain to monitor GPIO0 and wake when pulled LOW
    ESP_ERROR_CHECK(
        esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(
            BIT(WAKEUP_GPIO),
            ESP_GPIO_WAKEUP_GPIO_LOW
        )
    );

    ESP_LOGI(TAG, "Entering deep sleep. Press button on GPIO%d to wake.", WAKEUP_GPIO);
    led_off(strip);
    // Give the WS2812 time to latch the off state before RMT loses power
    vTaskDelay(pdMS_TO_TICKS(100));

    esp_deep_sleep_start();
}

Expected Behavior

On cold power-up or hard reset, the LED flashes blue three times slowly (500 ms on, 300 ms off) and then holds solid blue for five seconds. The slower cadence makes cold boot visually distinct from wakeup. When the five seconds expire, the LED goes dark and the chip enters deep sleep, drawing 5–20 µA.

Press the button and the LED flashes green three times rapidly (200 ms on, 150 ms off), holds solid green for five seconds, then returns to sleep. The green color and faster flash rate confirm the GPIO wakeup path ran. The response is immediate — there is no USB enumeration delay, no waiting for a terminal connection, and no ambiguity about what the chip did.

Below is a sped-up example of what you may see on your board:

ESP32-C6 deep sleep button wake demonstration

Timing and State Considerations

Two details in the code are worth examining. First, the 100 ms delay between led_off() and esp_deep_sleep_start() is not padding. WS2812B LEDs receive pixel data over the RMT peripheral. If esp_deep_sleep_start() cuts power to RMT before the LED has finished processing the off command, the LED stays lit after the chip sleeps. The delay lets the data propagate before the peripheral loses power.

Second, configure_wakeup_gpio() is called on every boot, not conditionally. Skipping it on wakeup boots (assuming that the pin was already configured before sleep) produces unreliable behavior. The main GPIO peripheral powers down completely during deep sleep, and its state does not survive the transition. A floating GPIO0 on wakeup can generate spurious low-level detections and trigger additional wakeups with no button press involved.

For more detail on the LP domain power states and the full list of wakeup sources, refer to the Espressif ESP32-C6 Technical Reference Manual. The ESP-IDF sleep modes reference covers timer wakeup, the ULP coprocessor, and other sources if your application needs more than a single GPIO trigger.

Building for Long Field Life

Active current on the ESP32-C6 runs between 20 and 30 mA. A device sitting in deep sleep draws 5–20 µA — three orders of magnitude lower. A 1000 mAh cell powering a device that stays awake for five seconds per button press and sleeps the rest of the time can sustain years of operation depending on how frequently the button is pressed. Reaching that kind of field life means keeping the wakeup path as lean as possible: the right GPIO domain, a clean pin configuration on every boot, and a wakeup registration that hands off to the LP domain with no wasted current in between. When those pieces are in place, the hardware does exactly what it was designed to do without wasting power.