xtensa/espressif+riscv: fix PM_NORMAL stay leak in idle loop - #20179
Merged
xiaoxiang781216 merged 1 commit intoSep 18, 2026
Merged
xiaoxiang781216 merged 1 commit into
xiaoxiang781216 merged 1 commit into
Conversation
FelipeMdeO
requested review from
eren-terzioglu,
fdcavalcanti,
jerpelea and
tmedicci
as code owners
September 17, 2026 19:02
up_idlepm() (esp32s3_idle.c/esp32_idle.c/esp32s2_idle.c and the
shared risc-v esp_idle.c for esp32c3/esp32c6) has a recovery branch
that forces the domain back to PM_NORMAL when oldstate is not
PM_NORMAL and nothing is currently staying at it:
pm_stay(PM_IDLE_DOMAIN, PM_NORMAL);
pm_changestate(PM_IDLE_DOMAIN, PM_NORMAL);
newstate = PM_NORMAL;
pm_stay() here has no matching pm_relax() anywhere in any of the
four files. The first time this branch runs, the stay count for
PM_NORMAL never returns to 0, and pm_checkstate() (called
unconditionally right after this block) can never recommend
anything deeper than PM_NORMAL again for the rest of uptime -- the
idle loop keeps running, but the governor is permanently pinned at
full power, with no further light or deep sleep.
Confirmed on real ESP32-S3 hardware (XIAO ESP32-S3,
CONFIG_ESPRESSIF_WIFI + CONFIG_PM + CONFIG_SCHED_TICKLESS): reading
g_pmdomains[0] live via JTAG/GDB showed a "system" wakelock stuck at
state=PM_NORMAL, count=1, acquired a few seconds after boot (right
when Wi-Fi coming up briefly moves the domain off PM_NORMAL and this
branch then forces it back). Reproduced 4/4 times before this fix
(never a single PM_STANDBY transition or light-sleep-return log line
across a 40+ minute run), 0/4 after.
The trigger is timing-dependent (whether anything else already
holds PM_NORMAL at the moment this branch runs), which is likely why
it does not reproduce on every single boot.
Fix: release the stay right after the one pm_changestate() call it
exists to force, matching the comment already there ("Keep working
in normal stage") -- a one-shot nudge, not a standing hold.
Touching the switch statement right below the fix in all four files
exposed a pre-existing nxstyle violation (case labels indented level
with the switch's opening brace instead of one level in from it, per
NuttX style); reindented alongside since checkpatch lints the whole
file. esp32s3_idle.c also had two unrelated stray-indented lines
("Perform IDLE mode power management" / up_idlepm()) in up_idle();
fixed those too, same reason.
Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
Assisted-by: Claude:claude-sonnet-5
FelipeMdeO
force-pushed
the
fix/pm-normal-stay-leak-in-idle-loop
branch
from
September 17, 2026 19:06
16a4ef1 to
82a78c4
Compare
tmedicci
approved these changes
Sep 17, 2026
tmedicci
left a comment
Contributor
There was a problem hiding this comment.
Nice catch, @FelipeMdeO
acassis
approved these changes
Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
up_idlepm()(arch/xtensa/src/esp32s3/esp32s3_idle.c,arch/xtensa/src/esp32/esp32_idle.c,arch/xtensa/src/esp32s2/esp32s2_idle.cand the shared RISC-V
arch/risc-v/src/common/espressif/esp_idle.c, used byesp32c3/esp32c6) has a recovery branch that forces the domain back to
PM_NORMAL when
oldstateis not PM_NORMAL and nothing is currently stayingat it:
pm_stay()here has no matchingpm_relax()anywhere in any of the fourfiles. The first time this branch runs, the stay count for PM_NORMAL never
returns to 0, and
pm_checkstate()(called unconditionally right afterthis block) can never recommend anything deeper than PM_NORMAL again for
the rest of uptime -- the idle loop keeps running, but the governor is
permanently pinned at full power, with no further light or deep sleep.
The trigger is timing-dependent (whether anything else already holds
PM_NORMAL at the moment this branch runs), which is likely why it does not
reproduce on every single boot.
Fix: release the stay right after the one
pm_changestate()call it existsto force, matching the comment already there ("Keep working in normal
stage") -- a one-shot nudge, not a standing hold.
Impact
hit the leaking branch; for configs that do, this restores the intended
ability to reach light/deep sleep after a PM_NORMAL recovery event.
CONFIG_PM+CONFIG_SCHED_TICKLESSon esp32, esp32s2, esp32s3, esp32c3 or esp32c6 --generic arch-level code, no board files touched.
Testing
I confirm that changes are verified on local setup and works as intended:
xtensa-esp-elf-gcc(crosstool-NGesp-14.2.0_20241119, 14.2.0).
CONFIG_PM=y,CONFIG_SCHED_TICKLESS=y,CONFIG_ESPRESSIF_WIFI=y.Confirmed via JTAG (OpenOCD + GDB), reading
g_pmdomains[0]live in memory:Before the fix -- a "system" wakelock stuck at
state=PM_NORMAL, count=1, acquired a few seconds after boot (right when Wi-Fi coming upbriefly moves the domain off PM_NORMAL and this branch forces it back).
Over a 40+ minute run:
state = 0(PM_NORMAL) the entire time, zeronewstate=2(PM_STANDBY) transitions logged, zero light-sleep-return loglines.
Console log before the fix (only two PM transitions ever logged, the
second one being the leak):
(nothing else for the following 40 minutes)
After the fix, same board, same config, fresh boot -- reached
PM_STANDBY (real light sleep) within seconds:
And reading
g_pmdomains[0]live again, caught mid-sleep: