How to Hold a GPIO Output State Across Deep Sleep on the ESP32-C6
Say you’re building an automated garden watering system with the ESP32-C6. The board opens a water valve through one GPIO pin, then goes into deep sleep for twenty minutes while the water runs. By default, the valve closes as soon as the chip sleeps because deep sleep powers off the circuit driving the pin.
In this guide, you’ll learn how to fix this problem by latching a GPIO pin on the ESP32-C6-DevKitC-1, so it keeps its level through sleep and wake.
Deep Sleep and GPIO Output
Deep sleep is the ESP32-C6’s lowest-power mode, in which the chip only uses about 7 µA. During deep sleep, the board powers off the CPU, the main RAM, and the peripheral domain that holds the GPIO controller.
The GPIO controller is one shared block that defines every pin’s direction, output level, pull resistors, and drive strength. Each pin has a driver circuit, also known as a pad, that connects to the controller and carries out its instructions. Thus, when deep sleep removes power from the controller, every pad loses its instructions at once and resets its pin to default, which is an input with the output driver off.
For instance, if an output GPIO pin was driving an LED, the LED would typically turn off as long as the chip sleeps. Even when the chip wakes, the LED stays off because waking from deep sleep is a full reset. The chip would run its bootloader again, and then restart your firmware from app_main(). The output pin only becomes an output again when your code reaches its pin setup, so the LED stays off through the whole boot.
Pad Hold on the ESP32-C6
Each pad has a hold latch. When you enable the latch, the pad freezes its current state and ignores later configuration changes, including the reset that follows a wake. The latch lives in the low-power (LP) domain, which stays powered through deep sleep, so the pad would keep driving its last level even when everything else is off.
The ESP32-C6 makes implementing the hold latch simpler than older iterations of the same board. For example, on the original ESP32, the ESP32-S3, and the ESP32-C3, holding a digital pin through deep sleep required a second chip-wide call named gpio_deep_sleep_hold_en(). On the ESP32-C6, one call to gpio_hold_en() holds any of the chip’s 31 GPIO pins through deep sleep and the wake that follows.
Remember that once you hold a pad, most ordinary operations can’t change it. For example, the hold stays active after wake, and changing the pin’s value with gpio_set_level() has no effect.
The only way you can change a pin is to call gpio_hold_dis() first. The function releases the pad hold, allowing the pin to take whatever configuration the controller has at that instant. To modify your pin behavior correctly, configure the pin and set its level before releasing the hold.
Modify the Firmware
Before you begin, make sure you have ESP-IDF v6.0 or later installed on your PC.
Start by creating a new project within ESP-IDF and setting the target chip to esp32c6. Then, wire two LEDs to pins 18 and 19 on your ESP32-C6 board, respectively. This will help you visually confirm that your latched GPIO pin doesn’t change even during deep sleep.
Add the following code to your project’s main.c to configure two GPIO pins 18 and 19. It sets both pins high, latches only pin 18, then puts the chip into deep sleep for five seconds. When the chip wakes, the application runs again in the same way in a quasi-loop.
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_sleep.h"
#include "esp_attr.h"
#include "esp_log.h"
static const char *TAG = "hold";
#define HELD_PIN GPIO_NUM_18 // gets gpio_hold_en() before sleep
#define FREE_PIN GPIO_NUM_19 // same setup, no hold, for comparison
// RTC_DATA_ATTR places this counter in LP memory, which stays powered
// through deep sleep. A plain static lives in main RAM and reads 0
// after every wake, because the wake is a reset.
static RTC_DATA_ATTR uint32_t sleep_cycles;
void app_main(void)
{
// The chip reports every wake source as one bit in a 32-bit mask.
// A set timer bit means this is a pass through the sleep cycle.
if (esp_sleep_get_wakeup_causes() & BIT(ESP_SLEEP_WAKEUP_TIMER)) {
sleep_cycles++;
ESP_LOGI(TAG, "woke from deep sleep, cycle %" PRIu32, sleep_cycles);
} else {
sleep_cycles = 0;
ESP_LOGI(TAG, "cold boot");
}
// Configure both pins as plain outputs. While GPIO18 is still held,
// these writes reach the controller and the pad ignores them.
gpio_config_t cfg = {
.pin_bit_mask = BIT64(HELD_PIN) | BIT64(FREE_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_ERROR_CHECK(gpio_config(&cfg));
ESP_ERROR_CHECK(gpio_set_level(HELD_PIN, 1));
ESP_ERROR_CHECK(gpio_set_level(FREE_PIN, 1));
// Release the hold last. The controller already says "output, high",
// so the pad takes that state and nothing visible changes. Releasing
// before gpio_config() drops the pin low until the config runs.
ESP_ERROR_CHECK(gpio_hold_dis(HELD_PIN));
ESP_LOGI(TAG, "both pins high for 3 s");
vTaskDelay(pdMS_TO_TICKS(3000));
// Freeze GPIO18 as it is right now. From here until gpio_hold_dis()
// runs on the next boot, nothing changes this pad, deep sleep included.
ESP_ERROR_CHECK(gpio_hold_en(HELD_PIN));
ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(5 * 1000000ULL));
ESP_LOGI(TAG, "sleeping 5 s, GPIO18 held, GPIO19 free");
esp_deep_sleep_start(); // never returns, because the wake is a reset
}Build the program, flash it, and open the serial terminal.
Results
You should see both LEDs light up within a second of the flash completing. In the image below, the LED on the left is connected to pin 18 on the ESP32-C6, while the LED on the right is connected to pin 19.

Three seconds later, the LED connected to pin 19 should turn off, while the LED connected to pin 18 remains lit. At this point, your chip should be in deep sleep, so the unlatched pad (pin 19) should turn off while the latched one (pin 18) doesn’t change.

About five seconds later, your chip should wake, and the pin 19 LED should light up again. The cycle should then repeat, with three seconds of both LEDs lit, followed by five seconds of only one LED lit.
Here’s what you should see in the first boot/sleep cycle in your serial terminal:

followed by what you should see in a subsequent sleep/wake cycle:

If your pin 18 LED never flickers, that confirms that gpio_hold_en() held the GPIO output correctly through many sleep/wake cycles.
For a comparison, comment out the gpio_hold_en() line, rebuild, and reflash. Now you should see both LEDs go dark during sleep.
Next Steps
Now you know how to hold any GPIO pin’s state, especially through deep sleep. On a production board, you can use this knowledge to save battery power by keeping a pin in the state that turns off the load or by keeping a sensor’s power switch off.
If you need to change a pin while the chip sleeps, read our other guide on Waking the ESP32-C6 from Deep Sleep Using a Button. How to Retain Application Data Through Resets and Power Loss on the ESP32-C6 covers keeping data across the same resets, and the ESP32 sleep modes guide covers everything else the chip gives up in deep sleep.
Ready to connect your devices anywhere on Earth? Get started with Hubble Network for free →