Skip to content

xtensa/espressif+riscv: fix PM_NORMAL stay leak in idle loop - #20179

Merged
xiaoxiang781216 merged 1 commit into
apache:masterfrom
FelipeMdeO:fix/pm-normal-stay-leak-in-idle-loop
Sep 18, 2026
Merged

xiaoxiang781216 merged 1 commit into
apache:masterfrom
FelipeMdeO:fix/pm-normal-stay-leak-in-idle-loop

Conversation

@FelipeMdeO

Copy link
Copy Markdown
Contributor

Summary

up_idlepm() (arch/xtensa/src/esp32s3/esp32s3_idle.c,
arch/xtensa/src/esp32/esp32_idle.c, arch/xtensa/src/esp32s2/esp32s2_idle.c
and the shared RISC-V arch/risc-v/src/common/espressif/esp_idle.c, used by
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.

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.

Impact

  • Is new feature added? No -- pure bug fix.
  • Is existing feature changed? No behavior change for configs that never
    hit the leaking branch; for configs that do, this restores the intended
    ability to reach light/deep sleep after a PM_NORMAL recovery event.
  • Impact on hardware? Affects every board using CONFIG_PM +
    CONFIG_SCHED_TICKLESS on 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:

  • Build Host: Ubuntu 24.04, x86_64, xtensa-esp-elf-gcc (crosstool-NG
    esp-14.2.0_20241119, 14.2.0).
  • Target: Xtensa, Seeed XIAO ESP32-S3, out-of-tree defconfig with
    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 up
briefly moves the domain off PM_NORMAL and this branch forces it back).
Over a 40+ minute run: state = 0 (PM_NORMAL) the entire time, zero
newstate=2 (PM_STANDBY) transitions logged, zero light-sleep-return log
lines.

$1 = {state = 0 '\000', ...,
      wakelock = {{head = 0x3fca1684 <g_wakelock+44>, tail = 0x3fca1684 <g_wakelock+44>}, ...},
      ...}
$2 = {name = "system", '\000' <repeats 25 times>, domain = 0,
      state = PM_NORMAL, count = 1, ...,
      start = {tv_sec = 12, tv_nsec = 119949125}, ...}

Console log before the fix (only two PM transitions ever logged, the
second one being the leak):

up_idlepm: newstate= 1 oldstate=0
up_idlepm: newstate= 0 oldstate=1

(nothing else for the following 40 minutes)

After the fix, same board, same config, fresh boot -- reached
PM_STANDBY (real light sleep) within seconds:

up_idlepm: newstate= 1 oldstate=0
up_idlepm: newstate= 2 oldstate=1

And reading g_pmdomains[0] live again, caught mid-sleep:

$1 = {state = 2 '\002', in_sleep = true,
      wakelock = {{head = 0x0, tail = 0x0},           /* PM_NORMAL: empty now */
                  {head = 0x0, tail = 0x0},           /* PM_IDLE: empty */
                  {head = 0x3fca1774 <g_wakelock+284>, ...}, /* PM_STANDBY: the board's own intentional floor blocking deep sleep, unrelated to this bug */
                  {head = 0x0, tail = 0x0}},
      ...}

@github-actions github-actions Bot added Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: xtensa Issues related to the Xtensa architecture Size: S The size of the change in this PR is small labels Sep 17, 2026
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
FelipeMdeO force-pushed the fix/pm-normal-stay-leak-in-idle-loop branch from 16a4ef1 to 82a78c4 Compare September 17, 2026 19:06
@github-actions github-actions Bot added Size: M The size of the change in this PR is medium and removed Size: S The size of the change in this PR is small labels Sep 17, 2026

@tmedicci tmedicci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, @FelipeMdeO

@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@xiaoxiang781216
xiaoxiang781216 merged commit 877d153 into apache:master Sep 18, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: risc-v Issues related to the RISC-V (32-bit or 64-bit) architecture Arch: xtensa Issues related to the Xtensa architecture Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants