Hold the deep sleep wake pad at its idle level

Before: a board whose variant does not define BUTTON_NEED_PULLUP enters deep
sleep with the wake pad set to plain INPUT and no pull on it, while ext1 is
armed to wake on a low level on that same pin. Nothing holds the line. On the
~40 ESP32 and ESP32-S3 variants whose BUTTON_PIN is GPIO0 that pin is also the
download-mode boot strap, and the reset that ends a deep sleep makes the ROM
read it again: a floating GPIO0 can bring the chip up in ROM download mode
("boot:0x0 (DOWNLOAD(USB/UART0))", "waiting for download") instead of booting
the firmware, which clears only when power is physically removed. A pad with no
pull is also a wake source that can fire on its own, so the board need not stay
asleep at all.

After: the pin ext1 wakes on keeps an internal pull at its idle level for the
whole sleep - a pull-up for a low-level wake, a pull-down where the variant
arms ESP_EXT1_WAKEUP_ANY_HIGH. A BUTTON_PIN that is not the wake pin keeps the
old pull-less INPUT, since gpio_hold_en() latches it.

The pull is applied in both register sets on purpose. The digital pull covers
the window before the sleep; the RTC pull is the one that still holds once the
RTC domain owns the pad, which is the state the ROM's strap read sees.

How: doDeepSleep() picks INPUT_PULLUP or INPUT_PULLDOWN for the wake pad rather
than INPUT, and cpuDeepSleep() asserts the matching pull unconditionally on the
pin it is about to arm. Two existing defects go with it: that pull-up was
previously gated on BUTTON_NEED_PULLUP and applied to BUTTON_PIN even when
config.device.button_gpio or DEEP_SLEEP_WAKE_PIN had moved the wake pin
elsewhere, and gpio_hold_en() skipped BUTTON_PIN on a DEEP_SLEEP_WAKE_PIN board
although it is not the pin being woken on. The wake-pin choice moves to the top
of cpuDeepSleep(), out of the SOC_RTCIO_HOLD_SUPPORTED block that the ext1 mask
below it was already reading it from.

An internal pull is a few hundred nA, less than the leakage the pull-less INPUT
was there to avoid.

Co-Authored-By: Jonathan Bennett <jbennett@incomsystems.biz>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTiifXrVc4YdonmLRCeWhw
This commit is contained in:
Jonathan BennettandClaude Opus 5 authored and Claude committed 2026-10-12 04:49:41 +00:00
1 parent e75d85f8be
commit 5e2acd13a6
2 files changed
+67 -18

No files matched your search

+50 -16
View File
@@ -359,6 +359,26 @@ void cpuDeepSleep(uint32_t msecToWake)
Note: we don't isolate pins that are used for the LORA, LED, i2c, or ST7735 Display for the Chatter2, spi or the wake
button(s), maybe we should not include any other GPIOs...
*/
// The pad deep sleep wakes on. A variant whose button is not an RTC IO can nominate a different one
// with DEEP_SLEEP_WAKE_PIN; that replaces the button rather than joining it, because
// esp_sleep_enable_ext1_wakeup() validates the whole mask and arms nothing if any pin in it lacks
// RTC function, so one bad pin would veto the good one.
#if defined(DEEP_SLEEP_WAKE_PIN)
const int wakeButton = DEEP_SLEEP_WAKE_PIN;
#elif defined(BUTTON_PIN)
const int wakeButton = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#else
const int wakeButton = -1;
#endif
#if defined(BUTTON_PIN) || defined(DEEP_SLEEP_WAKE_PIN)
// Which level ext1 is armed for below, so the pull we hold the pad at is the idle one.
#if defined(ESP32S3_WAKE_TYPE)
const bool wakeOnHigh = (ESP32S3_WAKE_TYPE == ESP_EXT1_WAKEUP_ANY_HIGH);
#else
const bool wakeOnHigh = false;
#endif
#endif
#if SOC_RTCIO_HOLD_SUPPORTED
static const uint8_t rtcGpios[] = {
#ifndef HELTEC_VISION_MASTER_E213
@@ -371,16 +391,6 @@ void cpuDeepSleep(uint32_t msecToWake)
#endif
34, 35, 37};
// A variant whose button is not an RTC IO can nominate a different pad here. This replaces the
// button rather than joining it: esp_sleep_enable_ext1_wakeup() validates the whole mask and
// arms nothing if any pin in it lacks RTC function, so one bad pin would veto the good one.
#if defined(DEEP_SLEEP_WAKE_PIN)
const int wakeButton = DEEP_SLEEP_WAKE_PIN;
#elif defined(BUTTON_PIN)
const int wakeButton = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#else
const int wakeButton = -1;
#endif
// Isolating a pad holds it, and ext1 cannot re-arm a held pad - skip the pin we wake on.
for (int i = 0; i < sizeof(rtcGpios); i++) {
if (rtcGpios[i] != wakeButton)
@@ -404,13 +414,37 @@ void cpuDeepSleep(uint32_t msecToWake)
#ifdef ALT_BUTTON_WAKE
gpioMask |= (1ULL << BUTTON_PIN_ALT);
#endif
#ifdef BUTTON_NEED_PULLUP
gpio_pullup_en((gpio_num_t)BUTTON_PIN);
// Hold the wake pad at its idle level for the whole sleep. Two reasons, and the second is the one
// that bites: ext1 samples the line, and on a strapping pin the ROM reads it again at the reset that
// ends the sleep - GPIO0 is the ESP32 download-mode strap, so a pad left floating can bring the chip
// up in ROM download mode instead of booting the firmware, which needs power physically removed to
// clear. ~40 variants use GPIO0 as BUTTON_PIN. This was previously gated on BUTTON_NEED_PULLUP and
// applied to BUTTON_PIN rather than to the pin actually armed; an internal pull is a few hundred nA,
// less than the leakage the gate was there to avoid.
//
// Both register sets are written on purpose: the digital pull covers the window before the sleep,
// the RTC pull is the one that holds once the RTC domain owns the pad.
if (wakeButton >= 0) {
const gpio_num_t wakePad = (gpio_num_t)wakeButton;
if (wakeOnHigh) {
gpio_pullup_dis(wakePad);
gpio_pulldown_en(wakePad);
} else {
gpio_pulldown_dis(wakePad);
gpio_pullup_en(wakePad);
}
#if SOC_RTCIO_HOLD_SUPPORTED
if (rtc_gpio_is_valid_gpio(wakePad)) {
if (wakeOnHigh) {
rtc_gpio_pullup_dis(wakePad);
rtc_gpio_pulldown_en(wakePad);
} else {
rtc_gpio_pulldown_dis(wakePad);
rtc_gpio_pullup_en(wakePad);
}
}
#endif
// Not needed because both of the current boards have external pullups
// FIXME change polarity in hw so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead
// of just the first) gpio_pullup_en((gpio_num_t)BUTTON_PIN);
}
#if SOC_PM_SUPPORT_EXT1_WAKEUP
gpioMask = filterExt1WakeMask(gpioMask);
+17 -2
View File
@@ -311,13 +311,28 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
#ifdef BUTTON_PIN
// Avoid leakage through button pin
if (GPIO_IS_VALID_OUTPUT_GPIO(BUTTON_PIN)) {
#if defined(DEEP_SLEEP_WAKE_PIN)
const int deepSleepWakePin = DEEP_SLEEP_WAKE_PIN;
#else
const int deepSleepWakePin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN;
#endif
#ifdef BUTTON_NEED_PULLUP
pinMode(BUTTON_PIN, INPUT_PULLUP);
#else
pinMode(BUTTON_PIN, INPUT);
#if defined(ESP32S3_WAKE_TYPE)
const bool wakeOnHigh = (ESP32S3_WAKE_TYPE == ESP_EXT1_WAKEUP_ANY_HIGH);
#else
const bool wakeOnHigh = false;
#endif
// Leaving the pad with no pull is only safe for a pin we are not waking on, which gpio_hold_en()
// latches below. The pin ext1 does wake on has to hold its idle level instead: the ROM reads a
// strapping pin again at the reset that ends the sleep, and GPIO0 - BUTTON_PIN on ~40 variants -
// is the download-mode strap, so a floating pad can come back up in ROM download mode.
// cpuDeepSleep() asserts the same pull in the RTC registers, which are what hold through the sleep.
pinMode(BUTTON_PIN, BUTTON_PIN != deepSleepWakePin ? INPUT : (wakeOnHigh ? INPUT_PULLDOWN : INPUT_PULLUP));
#endif
// A held pad ignores ext1_wakeup_prepare()'s re-route to RTC, so never hold the pin we wake on.
if (config.device.button_gpio && config.device.button_gpio != BUTTON_PIN)
if (BUTTON_PIN != deepSleepWakePin)
gpio_hold_en((gpio_num_t)BUTTON_PIN);
}
#endif