Skip to content

wbec: suspend-to-off support (suspend mode 0xA4, 2.4.0 series)#92

Draft
evgeny-boger wants to merge 12 commits into
mainfrom
feature/ec-suspend-mode
Draft

wbec: suspend-to-off support (suspend mode 0xA4, 2.4.0 series)#92
evgeny-boger wants to merge 12 commits into
mainfrom
feature/ec-suspend-mode

Conversation

@evgeny-boger

Copy link
Copy Markdown
Member

EC side of WB8.5 suspend-to-off (2.4.0 series, validated baseline a655128): suspend mode armed via register 0xA4 with a deadline backstop; on RTC alarm / PWRON / deadline the EC restarts the PMIC (PWRON press → V33 poll → PWROK pulse) and the SoC resumes from self-refresh DRAM.

Includes: suspend mode 0xA4 + deadline; PWROK pulse on wake; RTC ALRAF drain; exit-arm reset (regression-tested); resume-beep gate.

The sleep window in this baseline is the 64 MHz busy-poll (0.212 W system floor). Two competing low-power window implementations are stacked on this branch as separate PRs: Stop 1 (feature/ec-stop-mode) and Standby heartbeat (feature/ec-suspend-standby) — bench A/B pending (gate G2), both target the measured 0.165 W floor.

Validated on the bench across 40+ suspend cycles (alarm wake exact, deadline recovery clean). The tip deliberately excludes the abandoned ec0bef4 Standby experiment (root cause of its failure — un-fed IWDG reset misclassified as resume — documented in notes/ec-stop-mode-brief.md §3).

🤖 Generated with Claude Code

evgeny-boger and others added 12 commits July 4, 2026 14:35
Warm reset pulses the PMIC RESET (PWROK) line for 100 ms instead of
cycling the 5V rail. On WB 8.5 the line is wired to both the AXP15060
PWROK pin and the T507 RESET pin; with PMIC restart-on-PWROK-low
disabled (AXP REG32[4]=0, the power-on default) the pulse resets only
the SoC while all PMIC rails, including DRAM power, stay up. DRAM
contents survive, so ramoops panic logs can be read back after reboot.
Validated on WB 8.5.1: 16 MB test pattern survived EC-driven PWROK
reset bit-perfect.

The feature is gated by WBEC_HAS_WARM_RESET, defined only in
config_wb85.h: the pulse behavior is hardware-validated on WB 8.5 only,
so WB74 keeps its exact previous behavior (immediate hard power cycle
on watchdog timeout, 2 s PMIC RESET hold on the reset_pmic request).

On WB85 the watchdog escalates in two stages: the first timeout
triggers a warm reset (new appended poweron reason 8,
REASON_WATCHDOG_WARM); if Linux does not feed the watchdog via regmap
before the next timeout, the EC falls back to the original hard power
cycle (REASON_WATCHDOG). Feeding the watchdog re-arms the warm stage;
a timeout-only regmap write restarts the timer but deliberately does
not re-arm it.

On WB85 the reset_pmic request from Linux (POWER_CTRL bit 2) now
performs the short-pulse warm reset instead of the 2 s hold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RiQPqJiM3ftEk3SDda31Kf
…light

A watchdog timeout racing a 3.3V loss (or a reset_pmic request) in the
same main loop pass started a second power sequence on top of the warm
reset pulse already in flight. The pulse never completed, the PMIC RESET
(PWROK) line stayed latched and the SoC was held in reset forever: 5V
rail on, no SPL/U-Boot output ever, and no further watchdog recovery
(the escalation was latched to hard resets, which did not release the
line). Matches the failure observed on a WB 8.5.1 bench unit after a
rapid kernel-hang reboot loop. The same race latched the line on WB74
via the reset_pmic request path.

- wbec: allow only one power action per pass in WORKING; once a power
  sequence is started, defer the watchdog and 3.3V checks to the next
  passes (pending watchdog flags are not lost)
- wbec: restart the escalation after a stage-2 hard reset so unfed
  timeouts alternate warm/hard; the first hang of every fresh boot gets
  a DRAM-preserving warm reset and ramoops stays readable
- linux-power-control: hard_reset/hard_off/power-on always release the
  PMIC RESET (PWROK) line so no aborted sequence can leave it latched;
  a completed warm pulse turns 5V on before the power-on sequence
- add wbec + linux-power-control + wdt integration test suite with a
  board model (PMIC/SoC/5V rail): reproduces the wedge, sweeps PMIC
  failure timing across all reset phases, asserts bounded recovery

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVPkc2g2yir6SGyrED6uNT
Linux writes the expected sleep duration in seconds to a new regmap
region SUSPEND_CTRL (0xA4) before suspending. While the mode is
active:
- the 3.3V-loss monitor is skipped (the firmware suspend path powers
  DCDC1 down deliberately, which is otherwise indistinguishable from
  the 'PMIC unexpectedly off' failure);
- watchdog timeouts are ignored, replaced by a deadline of the
  requested duration plus 10 s: a system that never wakes gets the
  usual warm-reset (WB85) / power-cycle (WB74) recovery.

The mode ends on the first watchdog feed after resume (no extra
kernel work needed), an explicit write of 0, or the deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The watchdog daemon keeps feeding for another second or two between
the SUSPEND_CTRL write (systemd-sleep pre hook) and the actual freeze,
so exiting the mode on any feed cancelled it before the sleep even
began — and the 3.3V monitor then killed the suspended board as
before. The feed-exit is now armed only once the EC has actually seen
3.3V drop during the announced window; aborted suspends are covered by
the explicit zero-write from the post hook and by the deadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SUSPEND_CTRL gains an off_mode flag (0xA5 bit0). In this mode the
firmware puts the whole PMIC to sleep (AXP853T sleep state, only the
DRAM rails stay up) after Linux freezes — nothing on the SoC can wake
it, so the EC becomes the wake authority: on its RTC alarm, the
suspend deadline, or a power-button press it restarts the power-on
sequence, whose PWRON escalation is exactly the AXP sleep wake source
(POK negative edge). The PMIC then restores the recorded rail
configuration and the SoC boots through its power-on reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sleep

An announced window whose suspend never actually happened (canceled or
failed before the power cut) used to warm-reset a perfectly alive
system at the deadline. If 3.3V never dropped, just leave the mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AXP853T sleep-exit restores the recorded rail configuration but
produces no reset edge, and the SoC does not start on its own when its
rails return — the board sat with all rails up and the SoC dead until
the watchdog recovery pulsed PWROK two minutes later. Do that pulse as
part of the wake: once 3.3V is back, route the power-on sequence
through the existing warm-reset pulse state instead of completing
directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment out the power-on buzzer_beep in the WBEC_STATE_WORKING entry so
the suspend-to-off bench soak (many recovery/reboot cycles) is not
accompanied by a beep on every boot. Temporary, requested for the soak
only: revert this commit and re-flash the EC to restore the beep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rtcwake -m mem -s 60 woke the board ~220 ms after it entered off-mode
instead of after 60 s, even on the first cycle after boot. The off-mode
wake evaluates rtc_alarm_take_fired(), which reads the software
alarm_fired_latch (set from the STM32G0 RTC ALRAF flag). Both live in the
backup-power domain / a firmware static and survive resets and power
loss, and nothing consumes them outside off-mode. The alarm that last
woke the board therefore leaves them latched, so the very first off-mode
poll reads a stale match as a fresh wake and restarts the PMIC at once.

The kernel-side ack of IRQ_FLAGS.RTC_ALARM (wbec.c commit 0d510d76b)
clears a different latch (the host-visible irq-subsystem flags word), not
alarm_fired_latch, so it never prevented this.

Drain both representations when off-mode is entered: consume the software
fired latch and clear the hardware ALRAF. After this the off-mode wake
starts from a clean event state, so only a freshly armed alarm (or the
deadline / power button) wakes the board -- rtcwake -m mem -s 60 now
sleeps the full 60 s and resumes the same kernel. The wake path itself
(linux_cpu_pwr_seq_wakeup) is unchanged.

Add two wbec integration regression tests: a stale alarm latch must not
wake the board immediately, and a fresh off-mode alarm must still wake
it. Both fail without this change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "sleep started" flag (suspend_started) is armed once the EC sees 3.3V
drop during an announced window, and from then on a watchdog feed exits
suspend mode (that gate stops the daemon's 1-2 s post-announce feed from
cancelling the sleep before it begins). But the flag was only ever reset
on the *entry* of the next announce, never on exit -- and the off-mode
wake is a same-kernel resume that does not restart the EC, so the flag
survived from one cycle into the next. On the bench the first rtcwake
-m mem slept, but every later one exited immediately (RF2+:
[EC] Suspend mode: off (request)): entering cycle 2 with the arm already
set, the watchdog daemon's feed ended suspend mode during entry.

Reset suspend_started in every suspend-mode exit path -- off-mode alarm/
deadline/button wake, the watchdog-fed exit, the deadline backstop and
the host de-announce (both the WARM_RESET and legacy builds) -- so the
arm returns to its power-on (disarmed) state after every exit and each
cycle starts clean like a fresh boot. The entry reset is kept.

Add wbec integration regression tests: a fresh boot arms the flag on the
3.3V drop and a suspend-mode exit must leave it disarmed (fails without
this change), and a second off-mode cycle with the watchdog feeding must
still sleep and wake on its alarm rather than exit on the feed. A new
__unittest_env__ accessor exposes the flag. Keeps the ALRAF-drain
(89947fd) and everything else intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts commit 6fdf799.

The working-state power-on beep was commented out only to keep the
suspend-to-off bench soak quiet across its many recovery/reboot cycles;
that commit was marked revert-before-merge. The soak work is done, so
restore the stock beep on WBEC_STATE_WORKING entry for production. The
buzzer_beep() line is byte-identical to its pre-6fdf799 form; the
suspend-to-off fixes (89947fd ALRAF drain, 2ac7e28 exit-arm reset) are
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The working-state beep (buzzer_beep in new_state(WBEC_STATE_WORKING))
fired on every entry to WORKING, including the suspend-to-off resume.
On resume the SoC re-initializes DRAM over self-refresh (content-
preserving) — a path sensitive to power/timing perturbation. The
buzzer firing at that moment corrupts the resume and the board cold-
boots instead of waking (hw-proven 2026-07-07: beep-on-resume -> cold
boot; gated -> clean resume).

Gate the beep with a suspend_resume_no_beep flag set on the suspend-to-
off wake path. The board still beeps on a genuine power-up (cold start
/ reboot — which init DRAM from scratch and are insensitive) but stays
silent on resume.

This supersedes the naive f3face8 "restore beep" which reintroduced the
cold-boot; the earlier "TEMP silence" was masking this timing bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qj3VnuyyfUAWs7qFKvVZX9
@evgeny-boger

Copy link
Copy Markdown
Member Author

Heads-up: the button double-delivery bug fixed in #93 (4421ec8) is LATENT IN THIS BASELINE too (the wbec.c running-state IRQ_PWR_OFF_REQ level-check predates the Stop branch; the D4 button matrix had simply never run). If this baseline ever ships standalone as the fallback ladder, cherry-pick that fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant