Skip to content

[RFC] fwk: epr: make a failed EPR transition recoverable without AC removal - #90

Open
tyeth wants to merge 1 commit into
FrameworkComputer:fwk-tulip-29169from
tyeth:fix/epr-fail-recovery
Open

tyeth wants to merge 1 commit into
FrameworkComputer:fwk-tulip-29169from
tyeth:fix/epr-fail-recovery

Conversation

@tyeth

@tyeth tyeth commented Oct 20, 2026

Copy link
Copy Markdown

Instrumented investigation and full telemetry: tyeth/framework-system#1 and https://github.com/tyeth/fw16-gpu-watchdog (public). Fork-side review copies: tyeth#1, issue tyeth#3.

Problem

On FW16 (Ryzen AI 300 "tulip", RTX 5070 module, 240W EPR charging), a failed EPR entry/exit leaves the system in the mid-transition state instead of the plain-SPR state, and that state persists until physical AC removal:

  • charger input current stays collapsed (~0/500–800mA),
  • the hard AP throttle asserted for the transition can stay asserted (slows even BIOS, survives OS reboot),
  • the port is latched epr_active = 0xff ("EPR fail, do not retry") and the latch survives PD controller resets — verified live: resets of controller 0, 1, or both do not clear it; the EC re-imposes the state from EC RAM. Only a physical AC disconnect recovers, because only the PD_PROGRESS_DISCONNECTED path clears the progress/event state.

Full bug description with instrumented evidence (incident captures, healthy 1.1s reference trace of the same transition, stuck-port diagnostics): tyeth/framework-system#1. Raw telemetry/data repo: https://github.com/tyeth/fw16-gpu-watchdog. Related upstream reports: FrameworkComputer/SoftwareFirmwareIssueTracker#160, #193, #241, #243.

Why exactly these lines

The transitional clamp is intended behavior (healthy trace: set AP throttling type 1 to on → input forced to 500mA → Entered EPR → restore → throttling off, ~1.1s). The bug is only that the failure paths do not undo the transitional state:

  1. cypd_update_epr_state() — EPR_MODE_ENTER_FAILED/default branch (cypd_ccg8.c): the handler latches the port but never releases the PMF transition event. The normal release path, clear_prochot() in lotus/src/cpu_power.c, only clears the ENTER event when cypd_get_ac_power() > 100W — which can never be true after a failed EPR entry, since the standing SPR contract is at most 100W. So the hard throttle outlives the transition and is left to the 3s force_clear_pmf_prochot backstop at best. The patch releases the ENTER/EXIT transition events (update_pmf_events(..., 0), the exact calls clear_prochot() makes) at the point the flow is declared failed. The charger input limit needs no new code here: the cypd_update_port_state() call that already follows the EPR event restores the limit from the standing SPR contract via the existing EPR_PROCESS_MASK restore block.

  2. epr_flow_pending_deferred() — retry-exhausted branch (cypd_ccg8.c): this is the path taken when the EPR event from the PD controller never arrives (the handler's own comment: "Sometimes, EC does not receive the EPR event... from PD chip"). It already restores the input limit via cypd_update_port_state() on the active charge port, but it too leaves the PMF transition event set. Same two-line release.

  3. cypd_port_int() — CCG_RESPONSE_PORT_CONNECT (cypress_pd_common.c): the epr_active = 0xff latch is cleared only by clear_port_state() on a PORT_DISCONNECT/hard-reset response; cypd_update_port_state() deliberately preserves it, and a fresh attach does not reset it. If the disconnect response is missed (the same event-loss class item 2 exists for), or EC RAM state outlives a PD controller reset, the latch outlives the connection that caused it and the port can never enter EPR again. The patch clears the latch when a new connection is reported, before cypd_update_port_state() re-derives epr_active from the status register.

What this deliberately does not touch

  • The do-not-retry latch itself stays. The vendor presumably latched for a reason (avoid an enter-fail loop against a charger that NAKs EPR). After this patch the latch still prevents any EPR retry on the contract/connection that failed; it just cannot outlive that connection.
  • The retry counter, retry timing, and the 4-attempt give-up are unchanged.
  • The success-path release ordering is unchanged: on a successful entry the throttle is still released only via clear_prochot() after the SB-RMI limit write, preserving the "wait for the charger to enter bypass mode" sequencing.
  • The 3-level-buck (RAA489300) transition state machine is unchanged, including its own revert-to-SPR handling.
  • No refactoring, renaming, or restructuring: 11 added lines, 0 removed.

Risk analysis

  • update_pmf_events(bit, 0) is idempotent (events &= ~pd_event; no action unless the mask actually changes) and is exactly what clear_prochot() calls on the healthy path, including the GPU GPIO_FUNC_ACDC restore tied to the ENTER bit. If other PMF events (e.g. PD_PROGRESS_DISCONNECTED) are still set, the throttle correctly stays asserted.
  • Theoretical corner: two ports transitioning simultaneously and one failing would release the shared throttle while the other is still mid-flow. The existing code already treats concurrent flows loosely (see the "low risk situation" comment in epr_flow_pending_deferred), only one active charge port exists, and the failing-first ordering is benign: the surviving flow re-raises nothing because the event was per-flow anyway, and the 3s backstop still covers it.
  • The PORT_CONNECT latch clear is guarded by CONFIG_PD_CCG8_EPR and conditioned on the 0xff sentinel, so non-EPR boards and normal attaches are untouched.
  • Compile-untested (no Zephyr build environment); the change uses only symbols already used in the same files and was verified by read-through. RFC: please build before flashing.

How a FW16 owner would test

  1. Build and flash this EC on tulip (FW16 Ryzen AI 300) with a 240W EPR charger.
  2. Healthy path regression: replug the charger on the rear port, confirm the normal sequence in the EC console (set AP throttling type 1 to on → CL: ... i500 ... → Entered EPR → CL: ... i5000 v48000 → throttling off, ~1.1s) and that 240W negotiates.
  3. Failure path: provoke an EPR entry failure (e.g. a charger/cable that advertises EPR but rejects entry, or detach timing during entry). Confirm the console shows EPR failed/enter EPR stuck, retry! give-up, and then: input current returns to the SPR contract value (not 0/500mA), h_prochot_l/0x3E22 hard mask deasserts without waiting on pmf update timeout, and the system is not throttled.
  4. Latch recovery: after a failure, unplug and replug the charger into the same port and confirm Entered EPR on the new connection (previously required a full AC-removal event).
  5. Long-run: resume the telemetry logger from the data repo and confirm no recurrence of the latched slow state across EPR transitions.

🤖 Generated with Claude Code

During EPR entry/exit the EC intentionally puts the system into a
transitional clamp state: charger input current is collapsed to 0mA
(learn mode on) and a hard AP throttle is asserted via
update_pmf_events(BIT(PD_PROGRESS_ENTER/EXIT_EPR_MODE), 1). On a
healthy transition this lasts ~1.1s and both are released after the
new contract lands. When the transition fails, two pieces of that
transitional state currently persist beyond the transition:

1. Neither failure handler releases the PMF transition event. The
   normal release path, clear_prochot(), only clears the ENTER event
   when cypd_get_ac_power() > 100W, which is never true after a failed
   EPR entry (the standing SPR contract is at most 100W), so the hard
   throttle outlives the transition it was meant to cover and is left
   to the 3s force_clear_pmf_prochot backstop at best. Release the
   ENTER/EXIT transition events at both points where the flow is
   abandoned: the EPR_MODE_ENTER_FAILED/default branch of
   cypd_update_epr_state(), and the retry-exhausted branch of
   epr_flow_pending_deferred() (the path taken when the EPR event from
   the PD controller never arrives). The failure handler now leaves
   the system in the plain-SPR state (input limit is already restored
   by the existing cypd_update_port_state() call on both paths)
   instead of the mid-transition state.

2. The "EPR fail, do not retry" latch (epr_active = 0xff) is only
   cleared by clear_port_state() on a PORT_DISCONNECT/hard-reset
   response. cypd_update_port_state() deliberately preserves it, and a
   CCG_RESPONSE_PORT_CONNECT does not reset it, so if the disconnect
   response is missed (the same event-loss class the deferred retry
   exists for) or EC RAM outlives a PD controller reset, the latch
   outlives the connection that caused it and the port can never enter
   EPR again. Clear the latch on PORT_CONNECT: a fresh attach is a new
   connection, so the do-not-retry intent (no EPR retry on the
   contract that failed) is preserved while the latch can no longer
   outlive that connection.

Field evidence (FW16 Ryzen AI 300 "tulip", RTX 5070, 240W EPR, EC
tulip-4.0.1): instrumented captures show the system latching into the
transitional state - charger input stuck at ~0/500-800mA, hard
throttle asserted (BIOS runs slow, survives OS reboot), and a port
stuck attach-without-contract where EC_CMD_GET_PD_PORT_STATE returns
zeros and the EC reports "AC not connected". Resets of either or both
PD controllers do not clear it (EC RAM state re-imposes it after every
reset); only a physical AC removal recovers, because only the
PD_PROGRESS_DISCONNECTED path clears the progress/event state. A
healthy reference trace of the same transition (throttle on, input
500mA, ~1.1s, then release) was captured for comparison.

Issue: tyeth/framework-system#1
Telemetry/data: https://github.com/tyeth/fw16-gpu-watchdog
Related: FrameworkComputer/SoftwareFirmwareIssueTracker #160 #193 #241 #243

Compile-untested: no Zephyr build environment was available; changes
were limited to call sites and symbols already used in these files and
verified by read-through.

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

tyeth commented Oct 20, 2026 •

Copy link
Copy Markdown
Author

Apologies for using AI to assist me with generating this patch and details. My intentions were good, even if not ideal behaviour 😬

And it needs #91 really.

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