Skip to content

fwk: lotus: clamp typec_safety_level in update_safety_power_limit() (CPU stuck at ~600 MHz) - #95

Open
mkornreich wants to merge 1 commit into
FrameworkComputer:fwk-tulip-29169from
mkornreich:fwk-lotus-clamp-typec-safety-level
Open

mkornreich wants to merge 1 commit into
FrameworkComputer:fwk-tulip-29169from
mkornreich:fwk-lotus-clamp-typec-safety-level

Conversation

@mkornreich

Copy link
Copy Markdown

Problem

On the Laptop 16, heavy load that drains the battery can leave every CPU core hard-throttled at ~600 MHz until the EC loses power. Both boards are affected, since tulip and lotus both build lotus/src/cpu_power.c.

Unplugging or replugging the charger, OS reboots and power-profile changes don't clear it. While it's stuck, the EC console logs this for every PD port about once a second:

cypd_write_reg8_wait_ack C:0 0x1004 response 0x9
SET CCG_SELECT_REG failed

Reports of this:

  • SoftwareFirmwareIssueTracker #243
  • a comment on #202 that shows the same console loop
  • forum thread "CPU Frequency Stuck at 600 MHz" (community.frame.work/t/79713)

Cause

In update_safety_power_limit(), LEVEL_TYPEC_1_5A increments and decrements typec_safety_level without bounds, but only steps down when the counter equals exactly TYPEC_SAFETY_LEVEL_0:

  1. Average discharge over 5.49 A climbs the ladder to LEVEL_PROCHOT. That asserts THROTTLE_SRC_BAT_DISCHG_CURRENT and moves to LEVEL_TYPEC_1_5A, with the counter at 0.
  2. The throttle cuts the discharge, so the next tick is usually a decrease. The counter goes to −1, then −2, −3, …, and never equals 0 again.
  3. The ladder never returns to LEVEL_PROCHOT, so THROTTLE_OFF for that source is never sent.
  4. cypd_update_safety_table() indexes the 4-entry safety_table[] with the negative value and passes the result to cypd_select_pdo(). The PD controllers reject it, which produces the console loop above.

Where the code is:

  • The counter was added in 9ff6a96.
  • The same code is in the shipped v4.0.1 EC (9b2a0b1).
  • It's also on fwk-sunflower-dahlia-2026-09-30; this patch applies there with a 7-line offset.

It's unrelated to the PROCHOT pin fix in c6575ef.

Fix

Clamp typec_safety_level to [TYPEC_SAFETY_LEVEL_0, TYPEC_SAFETY_LEVEL_3]:

  • A decrease at 0 now steps back to LEVEL_PROCHOT, which releases the throttle on the next decrease.
  • An increase at 3 can no longer run past the end of the table.

Testing

Host simulation of the LEVEL_PROCHOT..LEVEL_COUNT cases, with the switch block copied verbatim:

Scenario Before After
PROCHOT, then 20 decrease ticks stuck at level 5: counter −20, throttle on, 20 out-of-bounds reads back to LEVEL_NORMAL, throttle released
1,000,000 random ticks counter ranged −1026 to 29, 999,796 out-of-bounds reads counter stayed within 0–3, no out-of-bounds reads

Not yet built or run on hardware. It should be reproducible on a unit with the existing console hooks:

cpupower logging 1
cpupower test_current -6000   # wait for "level = 5"
cpupower test_current -1000   # before: stays at level 5; after: steps back to 4 and releases PROCHOT

Seen in the field on a Laptop 16 AI 300 (HX 370, BIOS 04.01): the CPU locked at 603–610 MHz after heavy CPU+GPU load on battery, with the console loop above. Only an EC power cycle cleared it.

🤖 Generated with Claude Code

In LEVEL_TYPEC_1_5A the Type-C safety sub-level was incremented and
decremented without bounds, while the ladder only stepped down when the
counter was exactly TYPEC_SAFETY_LEVEL_0 (and only stepped up when it was
exactly TYPEC_SAFETY_LEVEL_3).

The hard throttle applied in LEVEL_PROCHOT cuts the battery discharge
current, so the first tick after entering LEVEL_TYPEC_1_5A is usually a
decrease tick: the counter goes 0 -> -1 -> -2 ... and never equals
TYPEC_SAFETY_LEVEL_0 again. safety_level then never returns to
LEVEL_PROCHOT, so throttle_ap(THROTTLE_OFF, THROTTLE_HARD,
THROTTLE_SRC_BAT_DISCHG_CURRENT) is never called and the APU stays hard
throttled (~600 MHz) until the EC resets, on AC as well as battery.

Every tick, cypd_update_safety_table() also indexes the 4-entry
safety_table[] with the out-of-range level and passes the result to
cypd_select_pdo(); the PD controllers reject it and the EC logs
"SET CCG_SELECT_REG failed" about once a second.

Clamp the counter to [TYPEC_SAFETY_LEVEL_0, TYPEC_SAFETY_LEVEL_3].

Fixes: 9ff6a96 ("fwk: 16-inch: Reduce the Type-C current once the battery exceed discharge current limit")

BRANCH=fwk-tulip-29169
BUG=FrameworkComputer/SoftwareFirmwareIssueTracker#243
TEST=Host simulation of the LEVEL_PROCHOT..LEVEL_COUNT cases. Without
     this change, PROCHOT followed by 20 decrease ticks leaves the ladder
     at LEVEL_TYPEC_1_5A with the counter at -20 and the throttle on; with
     it, the ladder returns to LEVEL_NORMAL and releases the throttle.
     1,000,000 random ticks keep the counter within [0, 3]. Not yet built
     or run on hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mkornreich

Copy link
Copy Markdown
Author

I did use AI for this but it does solve THE CLAMP. It resolves https://community.frame.work/t/cpu-frequency-stuck-at-600-mhz/79713

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