Framework Universal Smart Power Controller
“Elastic V-Curve” Implementation Guide (FW13 & FW16)
Target Hardware:
- Framework Laptop 13 (AMD 7040 / AI 300)
- Framework Laptop 16 (AMD 7040 / AI 300)
Target Controller: ISL9241 Buck-Boost Charger
Firmware Base: Chromium EC (cros-ec)
1. Executive Summary
This firmware modification addresses the “battery flipping,” “input throttling,” and “400MHz lock” issues observed on AMD-based Framework laptops during high-load scenarios.
It replaces the stock reactive logic with a Proactive Hybrid Controller that implements an Universal Elastic V-Curve. This allows the battery to act as a high-performance energy buffer without triggering false-positive safety trips.
Key Findings from Community Engineering
- The “Crying Wolf” Prochot: The stock firmware enables “Input Current PROCHOT” (Bit 9 of Control2). This triggers CPU throttling immediately when an input spike occurs, even if the battery is healthy and capable of assisting. Fix: We disable this specific bit and manage thermal safety manually.
- The BGATE Danger: Warning from Framework Staff: The BGATE MOSFETs must never be manually disabled under load. Their body diodes are rated for only ~2A. Fix: This implementation manages power exclusively via current limits, ensuring the ISL9241 hardware state machine keeps BGATE safely engaged in NVDC mode.
- FW16 Power Topology: The FW16 uses a pre-buck converter that steps 36V/48V inputs down to a fixed 20V before reaching the charger. Fix: FW16 input limits must be calculated relative to this 20V rail, not the external PD voltage.
2. Power Source Logic Categories
The controller automatically classifies the power source into one of four modes to optimize behavior. Crucially, all modes allow for eventual recovery to the target charge limit (e.g., 100%), though the timeframe varies significantly.
| Source Class |
Wattage |
Logic Strategy |
Goal |
Recovery Profile |
| Desktop |
> 85W |
Passthrough |
Max Performance. No Boost needed. |
Instant. Charges during load. |
| Hybrid |
20W - 85W |
Elastic V-Curve |
Bridge the deficit. Maintain system stability. |
Managed. Throttles to force charge when low. |
| Range Extender |
8W - 20W |
Input Saturation |
Slow the drain. Input < Load. |
Opportunistic. Charges when user Idles/Types. |
| Scavenger |
< 8W |
Sleep Only |
Ignore active usage. Charge when sleeping. |
Overnight. Charges only in s2idle or Off. |
Eventual Recovery Assurance
Even on the weakest power sources (e.g., a 15W phone charger), the logic ensures the battery will eventually reach the user-defined limit (e.g., 90% or 100%) provided the laptop remains plugged in.
- Mechanism: When the system is idle or sleeping, power consumption drops to ~0.8W - 5W. The controller directs the remaining 10W+ from the phone charger entirely into the battery. Over a long duration (e.g., leaving it plugged in overnight), this accumulates to a full charge, resetting the “Boost Budget” for the next day’s high-performance tasks.
Adaptive Floor (“Snooze” Functionality)
The system enforces a Safety Floor (Default: 20%) where “Boost” is disabled to protect the battery and ensure user runtime if unplugged. However, power users may need to finish a critical task (e.g., a ranked match or a render) and are willing to sacrifice that safety buffer.
- The Trigger: Unplugging and Replugging the USB-C charger while at the floor.
- The Logic:
- System detects AC insertion.
- Checks if Battery SoC is near the current floor (e.g., 20%).
- Action: Lowers the floor by 5% (e.g., new floor = 15%).
- Result: Turbo Boost is immediately unlocked again.
- Safety Stop: This “Snooze” can be repeated until the Absolute Floor (5%) is reached, at which point the system enforces hard throttling to prevent brownouts.
3. Comprehensive Behavior Matrices
Battery Assumptions:
- FW16: 85Wh Capacity (Recovery to 90% requires ~61Wh).
- FW13: 61Wh Capacity (Recovery to 90% requires ~43Wh).
Scenario A: The “Desktop” (FW16 on 240W)
Load: 220W (Gaming) | Adapter: 240W (Effective ~215W) | Deficit: ~5W
| Time |
Battery |
State |
Performance |
Behavior |
| 0h |
100% |
Turbo |
100% |
Battery covers 200W+ transients. |
| 2h |
50% |
Turbo |
100% |
High voltage maintains efficiency. |
| 3h |
20% |
Hit Floor |
95% |
Throttled to 215W. Battery protected. |
| Action |
Replug |
Reset |
$\downarrow$ |
Floor → 15%. Full boost restored. |
| Recovery |
Active |
Charging |
~34 Hours |
Time to Full while Gaming (2W trickle). |
| Recovery |
Idle |
Fast Charge |
~50 Mins |
Time to Full if Gaming stops. |
Scenario B: The “Standard” (FW13 on 60W)
Load: 70W | Adapter: 60W (Effective 57W) | Deficit: 12W
| Time |
Battery |
State |
Performance |
Behavior |
| 0h |
100% |
Turbo |
100% |
Battery bridges 12W deficit. |
| 4h |
22% |
Gliding |
94% |
Boost limit gently reduces. |
| 4.5h |
20% |
Hit Floor |
81% (57W) |
Throttled. System stable at wall limit. |
| Recovery |
Active |
Charging |
~24.5 Hours |
Time to Full while Gaming (2W trickle). |
| Recovery |
Idle |
Fast Charge |
~1 Hour |
Time to Full if Gaming stops. |
Scenario C: The “Traveler” (FW13 on 45W Charger)
Load: 70W | Adapter: 45W (Effective 42W) | Deficit: 28W
| Time |
Battery |
State |
Performance |
Behavior |
| 0h |
100% |
Turbo |
100% |
Massive battery assist (-28W). |
| 1h |
54% |
Gliding |
98% |
Curve begins to limit peak power. |
| 1.5h |
20% |
Floor Hit |
57% (40W) |
Hard Throttle. Performance crash. |
| Recovery |
Active |
Charging |
~24.5 Hours |
Time to Full while Gaming (2W trickle). |
| Recovery |
Idle |
Charge |
~1.5 Hours |
Time to Full if Gaming stops. |
Scenario D: The “Emergency” (FW13 on 15W Phone Charger)
Load: Variable | Adapter: 15W (5V/3A) | Behavior: Survival
| Usage |
Battery |
Performance |
Behavior |
| Gaming |
Draining |
Unplayable |
System cannot sustain high load. |
| Office |
Steady |
100% |
Range Extender. Battery covers spikes. |
| Recovery |
Active |
N/A |
Infinite |
| Recovery |
Office |
Slow |
~16 Hours |
| Recovery |
Sleep |
Scavenge |
~3.5 Hours |
Scenario E: The “Trickle” (FW13 on Legacy USB-A / < 5W)
Load: Variable | Adapter: 2.5W - 4.5W
| Usage |
State |
Behavior |
| Active |
Draining |
Slow Death. Source cannot power screen + CPU. Discharges slowly. |
| Recovery |
Active |
Impossible |
| Recovery |
Sleep |
~15 Hours |
4. Implementation Guide
A. Configuration Header (smart_power_config.h)
Create board/yourboard/smart_power_config.h. Uncomment the correct definition for your target board.
#ifndef __SMART_POWER_CONFIG_H
#define __SMART_POWER_CONFIG_H
// ==========================================
// SELECT TARGET BOARD (Uncomment ONE)
// ==========================================
// #define BOARD_FRAMEWORK_13_AMD
// #define BOARD_FRAMEWORK_16_AMD
// ==========================================
// TUNING PARAMETERS
// ==========================================
#ifdef BOARD_FRAMEWORK_16_AMD
/* Framework 16 Specifics */
// FW16 uses a pre-buck converter to 20V.
// Max Transient Load ~450W observed in lab (James3).
// Battery supports massive discharge bursts.
#define MAX_BOOST_MW 180000 // Extreme Boost allowed
#define HIGH_POWER_SOURCE_MW 241000 // Even 240W needs logic
#define HAS_INPUT_BUCK_CONV 1 // 20V Step-down exists
#define FIXED_INPUT_VOLTAGE 20000 // Charger sees 20V
#else
/* Framework 13 Specifics */
// FW13 is Direct VBUS.
// Battery limited to ~1C discharge recommended.
#define MAX_BOOST_MW 25000 // 25W Max Boost
#define HIGH_POWER_SOURCE_MW 85000 // 85W+ is Desktop Mode
#define HAS_INPUT_BUCK_CONV 0 // Direct VBUS
#define FIXED_INPUT_VOLTAGE 0 // Dynamic
#endif
// Universal Constants
// Safety Margin: Stock FW uses 90%. We use 95% for better performance.
// Warning: 100% may trip OCP on older chargers due to measurement tolerance.
#define INPUT_SAFETY_MARGIN_PERCENT 95
#define LOW_POWER_SOURCE_MW 15000 // Threshold for Survival Mode
#define DEFAULT_FLOOR_SOC 20
#define ABSOLUTE_MIN_SOC 5
#define CEILING_BUFFER 2
#define CHARGE_PENALTY_MW 2000 // 2W forced charge
#define S2IDLE_DRAIN_MW 1500
#define BROWNOUT_VOLTAGE_MV 4600
#endif
B. Core Logic Module (smart_power.c)
This logic implements the survival hierarchy for weak sources and the critical architecture handling for FW16.
/*
* Framework Universal Smart Power Controller
* Implements Elastic V-Curve with Universal Source Handling
*/
#include "smart_power_config.h"
#include "driver/charger/isl9241.h"
#include "charge_state.h"
#include "usb_pd.h"
#include "math_util.h"
#include "chipset.h"
#include "thermal.h"
#include "console.h"
// INTERNAL STATE
enum power_mode {
MODE_TURBO_DESCENT,
MODE_FORCED_RECOVERY
};
static enum power_mode current_mode = MODE_TURBO_DESCENT;
static int adaptive_floor = DEFAULT_FLOOR_SOC;
/*
* HELPER: Calculate Battery Boost
*/
static int32_t calculate_boost_limit(int soc) {
if (soc <= adaptive_floor) return 0;
int range = 100 - adaptive_floor;
int val = soc - adaptive_floor;
// Normalized Sqrt Curve
uint32_t ratio_scaled = (val * 10000) / range;
uint32_t curve_factor = integer_sqrt(ratio_scaled);
// Returns mW allowed from battery
return (MAX_BOOST_MW * curve_factor) / 100;
}
/*
* HELPER: Get User Charge Limit
*/
static int get_effective_ceiling(void) {
#ifdef CONFIG_CHARGER_PROFILE_OVERRIDE
int user_limit = charge_get_display_charge();
if (user_limit > 0 && user_limit <= 100) return user_limit;
#endif
return 100;
}
/*
* EVENT: Replug ("Snooze")
*/
void smart_power_on_ac_change(void) {
if (extpower_is_present()) {
int soc = charge_get_percent();
// Drop floor if user replugs near the limit
if (soc <= (DEFAULT_FLOOR_SOC + 5) && soc > ABSOLUTE_MIN_SOC) {
adaptive_floor = (soc > 5) ? (soc - 5) : ABSOLUTE_MIN_SOC;
current_state = MODE_TURBO_DESCENT;
cprintf(CC_CHARGER, "SmartPower: Turbo Reset! Floor: %d%%\n", adaptive_floor);
} else if (soc > DEFAULT_FLOOR_SOC) {
adaptive_floor = DEFAULT_FLOOR_SOC;
current_state = MODE_TURBO_DESCENT;
}
}
}
/*
* MAIN LOOP (1Hz)
*/
void smart_power_update(void) {
if (!extpower_is_present()) return;
int soc = charge_get_percent();
int active_port = charge_manager_get_active_charge_port();
if (active_port < 0) return;
// 1. ANALYZE SOURCE & EFFECTIVE POWER
int pd_volts_mv = pd_get_negotiated_voltage(active_port);
int pd_amps_ma = pd_get_negotiated_current(active_port);
int32_t wall_mw = (pd_volts_mv * pd_amps_ma) / 1000;
// Safety Derating: Configurable margin
wall_mw = (wall_mw * INPUT_SAFETY_MARGIN_PERCENT) / 100;
int32_t input_limit_mw = wall_mw;
int32_t discharge_limit_mw = 0;
// 2. STATE MACHINE
int exit_threshold = get_effective_ceiling() - CEILING_BUFFER;
if (exit_threshold < DEFAULT_FLOOR_SOC + 5) exit_threshold = DEFAULT_FLOOR_SOC + 5;
switch (current_state) {
case MODE_TURBO_DESCENT:
if (soc <= adaptive_floor) current_mode = MODE_FORCED_RECOVERY;
break;
case MODE_FORCED_RECOVERY:
if (soc >= exit_threshold) {
current_state = MODE_TURBO_DESCENT;
adaptive_floor = DEFAULT_FLOOR_SOC;
}
break;
}
// 3. LOGIC BRANCHING
// S2IDLE (Scavenger)
if (chipset_in_state(CHIPSET_STATE_STANDBY)) {
if (wall_mw > S2IDLE_DRAIN_MW + 500) {
input_limit_mw = wall_mw;
discharge_limit_mw = 0;
// Ensure Charge Path Open
i2c_write16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE,
ISL9241_REG_CHARGE_CURRENT_LIMIT, 4000);
} else {
// Protect weak source from brownout loop
i2c_write16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE,
ISL9241_REG_CHARGE_CURRENT_LIMIT, 0);
return;
}
goto apply_limits;
}
// A. DESKTOP MODE
if (wall_mw > HIGH_POWER_SOURCE_MW) {
current_mode = MODE_TURBO_DESCENT;
input_limit_mw = wall_mw;
discharge_limit_mw = 0;
}
// B. SURVIVAL MODE (Weak Charger)
else if (wall_mw < LOW_POWER_SOURCE_MW) {
// Source is too weak to throttle (Throttle target would be < Idle).
// Strategy: Max Input, Max Discharge.
// We let the battery drain naturally to prevent voltage collapse.
input_limit_mw = wall_mw;
discharge_limit_mw = 0xFFFF; // Max value (No throttling)
}
// C. HYBRID MODE (Normal)
else {
if (current_mode == MODE_FORCED_RECOVERY) {
// Force charge reservation
if (wall_mw > CHARGE_PENALTY_MW + 8000) {
input_limit_mw = wall_mw - CHARGE_PENALTY_MW;
} else {
input_limit_mw = wall_mw;
}
discharge_limit_mw = 0;
} else {
// Turbo
discharge_limit_mw = calculate_boost_limit(soc);
}
}
apply_limits:
// 4. HARDWARE TRANSLATION (CRITICAL FW13 vs FW16 LOGIC)
uint16_t reg_input_ma = 0;
#if HAS_INPUT_BUCK_CONV
// FW16 Architecture:
// Adapter(36/48V) -> Buck(20V) -> ISL9241 -> Vsys.
// The ISL9241 measures input current at its own input pin (20V rail).
// We want to limit Power. P = I * V.
// I_reg = Power_Limit / 20V.
reg_input_ma = input_limit_mw / 20;
#else
// FW13 Architecture:
// Adapter(Vbus) -> ISL9241 -> Vsys.
// The ISL9241 measures input current at Vbus.
// I_reg = Power_Limit / Vbus.
int effective_v = (pd_volts_mv > 0) ? pd_volts_mv : 5000;
reg_input_ma = input_limit_mw / (effective_v / 1000);
#endif
// Discharge is usually relative to Battery Voltage (~15-16V)
uint16_t reg_discharge_ma = discharge_limit_mw / 15;
// 5. WRITE REGISTERS
i2c_write16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE,
ISL9241_REG_ADAPTER_CURRENT_LIMIT1, reg_input_ma);
i2c_write16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE,
ISL9241_REG_DISCHARGE_CURRENT_LIMIT, reg_discharge_ma);
// 6. PROCHOT CONFIG
// CRITICAL: Disable "Input Current Prochot" (Bit 9)
// This bit is the cause of the "Crying Wolf" throttling issue.
// We replace it with our calculated "Discharge Limit Prochot" (Bit 11).
uint16_t c2;
i2c_read16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE, ISL9241_REG_CONTROL2, &c2);
c2 &= ~ISL9241_CONTROL2_INPUT_CURRENT_PROCHOT; // DISABLE panic throttling
if (discharge_limit_mw > 0 && discharge_limit_mw < 0xFFFF) {
c2 |= ISL9241_CONTROL2_BAT_DISCHARGE_CURRENT_PROCHOT; // ENABLE smart throttling
} else {
c2 &= ~ISL9241_CONTROL2_BAT_DISCHARGE_CURRENT_PROCHOT;
}
i2c_write16(I2C_PORT_CHARGER, ISL9241_ADDR_CHARGE, ISL9241_REG_CONTROL2, c2);
}
C. Integration Hooks
Modify board/yourboard/board.c.
#include "smart_power.h"
#include "hooks.h"
// Register 1Hz loop
DECLARE_HOOK(HOOK_SECOND, smart_power_update, HOOK_PRIO_DEFAULT);
// Register AC Change for Replug/Snooze Logic
DECLARE_HOOK(HOOK_AC_CHANGE, smart_power_on_ac_change, HOOK_PRIO_DEFAULT);
static void board_init(void) {
smart_power_init();
}
DECLARE_HOOK(HOOK_INIT, board_init, HOOK_PRIO_DEFAULT);
5. References
-
Renesas ISL9241 Datasheet:
https://www.renesas.com/us/en/document/dst/isl9241-datasheet
Defines registers 0x39 (Discharge Limit), 0x3F (Adapter Limit), and 0x3D (Control 2).
-
Chromium EC Source Code (Framework EC):
https://github.com/FrameworkComputer/EmbeddedController
-
Framework Community Thread (FW13 Investigation):
https://community.frame.work/t/tracking-battery-flipping-between-charging-and-discharging-draws-from-battery-even-on-ac/22484
Primary source for battery flipping behavior and input throttling analysis.
-
Framework 16 Power Architecture:
Forum thread analysis confirms 3-stage power train (Adapter → Buck → Charger → VRM), necessitating the fixed 20V input calculation logic.