diff options
-rw-r--r-- | board/poppy/board.c | 4 | ||||
-rw-r--r-- | common/charge_state_v2.c | 485 | ||||
-rw-r--r-- | include/charge_state_v2.h | 15 | ||||
-rw-r--r-- | include/ec_commands.h | 2 | ||||
-rw-r--r-- | util/ectool.c | 5 |
5 files changed, 499 insertions, 12 deletions
diff --git a/board/poppy/board.c b/board/poppy/board.c index 8103ed5d67..9ed130fc03 100644 --- a/board/poppy/board.c +++ b/board/poppy/board.c @@ -632,6 +632,10 @@ int board_set_active_charge_port(int charge_port) gpio_set_level(GPIO_USB_C0_CHARGE_L, 1); gpio_set_level(GPIO_USB_C1_CHARGE_L, 1); } else { + /* + * TODO(b:67029560): Here, we should make sure that base/lid + * power is diconnected. + */ /* Make sure non-charging port is disabled */ gpio_set_level(charge_port ? GPIO_USB_C0_CHARGE_L : GPIO_USB_C1_CHARGE_L, 1); diff --git a/common/charge_state_v2.c b/common/charge_state_v2.c index 7774978cd0..3e704b8b6b 100644 --- a/common/charge_state_v2.c +++ b/common/charge_state_v2.c @@ -14,6 +14,7 @@ #include "chipset.h" #include "common.h" #include "console.h" +#include "ec_ec_comm_master.h" #include "ec_ec_comm_slave.h" #include "extpower.h" #include "gpio.h" @@ -30,6 +31,10 @@ /* Console output macros */ #define CPUTS(outstr) cputs(CC_CHARGER, outstr) #define CPRINTS(format, args...) cprints(CC_CHARGER, format, ## args) +#define CPRINTF(format, args...) cprintf(CC_CHARGER, format, ## args) + +/* Extra debugging prints when allocating power between lid and base. */ +#undef CHARGE_ALLOCATE_EXTRA_DEBUG #define CRITICAL_BATTERY_SHUTDOWN_TIMEOUT_US \ (CONFIG_BATTERY_CRITICAL_SHUTDOWN_TIMEOUT * SECOND) @@ -39,6 +44,8 @@ /* Prior to negotiating PD, most PD chargers advertise 15W */ #define LIKELY_PD_USBC_POWER_MW 15000 +static int charge_request(int voltage, int current); + /* * State for charger_task(). Here so we can reset it on a HOOK_INIT, and * because stack space is more limited than .bss @@ -54,6 +61,14 @@ static unsigned int user_current_limit = -1U; test_export_static timestamp_t shutdown_warning_time; static timestamp_t precharge_start_time; +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER +static int base_connected; +static int prev_base_connected; +static int charge_base; +static int prev_charge_base; +static int prev_current_base; +#endif + /* Is battery connected but unresponsive after precharge? */ static int battery_seems_to_be_dead; @@ -119,6 +134,386 @@ static void problem(enum problem_type p, int v) problems_exist = 1; } +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER +/* + * Parameters for dual-battery policy. + * TODO(b:71881017): This should be made configurable by AP in the future. + */ +struct dual_battery_policy { + /*** Policies when AC is not connected. ***/ + /* Voltage to use when using OTG mode between lid and base (mV) */ + uint16_t otg_voltage; + /* Maximum current to apply from base to lid (mA) */ + uint16_t max_base_to_lid_current; + /* When base battery is low, current to provide from lid to base (mA) */ + uint16_t lid_to_base_current_charge_base_low; + /* + * Margin to apply between provided OTG output current and input current + * limit, to make sure that input charger does not overcurrent output + * charger. input_current = (1-margin) * output_current. (/128) + */ + uint8_t margin_otg_current; + + /* Only do base to lid OTG when base battery above this value (%) */ + uint8_t min_charge_base_otg; + + /* + * When base/lid battery percentage is below this value, do + * battery-to-battery charging. (%) + */ + uint8_t max_charge_base_batt_to_batt; + uint8_t max_charge_lid_batt_to_batt; + + /*** Policies when AC is connected. ***/ + /* Minimum power to allocate to base (mW) */ + uint16_t min_base_system_power; + + /* Smoothing factor for lid power (/128) */ + uint8_t lid_system_power_smooth; + /* + * Smoothing factor for base/lid battery power, when the battery power + * is decreasing only: we try to estimate the maximum power that the + * battery is willing to take and always reset it when it draws more + * than the estimate. (/128) + */ + uint8_t battery_power_smooth; + + /* + * Margin to add to requested base/lid battery power, to figure out how + * much current to allocate. allocation = (1+margin) * request. (/128) + */ + uint8_t margin_base_battery_power; + uint8_t margin_lid_battery_power; + + /* Maximum current to apply from lid to base (mA) */ + uint16_t max_lid_to_base_current; +}; + +static const struct dual_battery_policy db_policy = { + .otg_voltage = 15000, /* mV */ + .max_base_to_lid_current = 1800, /* mA, about 2000mA with margin. */ + .lid_to_base_current_charge_base_low = 200, /* mA, so about 3W. */ + .margin_otg_current = 13, /* /128 = 10.1% */ + .min_charge_base_otg = 5, /* % */ + .max_charge_base_batt_to_batt = 4, /* % */ + .max_charge_lid_batt_to_batt = 10, /* % */ + .min_base_system_power = 1100, /* mW */ + .lid_system_power_smooth = 32, /* 32/128 = 0.25 */ + .battery_power_smooth = 1, /* 1/128 = 0.008 */ + .margin_base_battery_power = 32, /* 32/128 = 0.25 */ + .margin_lid_battery_power = 32, /* 32/128 = 0.25 */ + .max_lid_to_base_current = 2000, /* mA */ +}; + +/* Add at most "value" to power_var, subtracting from total_power budget. */ +#define CHG_ALLOCATE(power_var, total_power, value) do { \ + int val_capped = MIN(value, total_power); \ + (power_var) += val_capped; \ + (total_power) -= val_capped; \ +} while (0) + +static int charge_get_base_percent(void) +{ + if (base_battery_dynamic.flags & (BATT_FLAG_BAD_FULL_CAPACITY | + BATT_FLAG_BAD_REMAINING_CAPACITY)) + return -1; + + if (base_battery_dynamic.full_capacity > 0) + return 100 * base_battery_dynamic.remaining_capacity + / base_battery_dynamic.full_capacity; + + return 0; +} + +/** + * Setup current settings for lid and base, in a safe way. + * + * @param current_base Current to be drawn by base (negative to provide power) + * @param allow_charge_base Whether base battery should be charged (only makes + * sense with positive current) + * @param current_lid Current to be drawn by lid (negative to provide power) + * @param allow_charge_lid Whether lid battery should be charged + */ +static void set_base_lid_current(int current_base, int allow_charge_base, + int current_lid, int allow_charge_lid) +{ + /* "OTG" voltage from base to lid or from lid to base. */ + const int otg_voltage = db_policy.otg_voltage; + + static int prev_allow_charge_base; + static int prev_current_lid; + + int lid_first; + int ret; + + /* TODO(b:71881017): This is still quite verbose during charging. */ + if (prev_current_base != current_base || + prev_allow_charge_base != allow_charge_base || + prev_current_lid != current_lid) { + CPRINTS("Base/Lid: %d%s/%d%s mA", + current_base, allow_charge_base ? "+" : "", + current_lid, allow_charge_lid ? "+" : ""); + } + + /* + * To decide whether to first control the lid or the base, we first + * control the side that _reduces_ current that would be drawn, then + * setup one that would start providing power, then increase current. + */ + if (current_lid >= 0 && current_lid < prev_current_lid) + lid_first = 1; /* Lid decreases current */ + else if (current_base >= 0 && current_base < prev_current_base) + lid_first = 0; /* Base decreases current */ + else if (current_lid < 0) + lid_first = 1; /* Lid provide power */ + else + lid_first = 0; /* All other cases: control the base first */ + + if (!lid_first && base_connected) { + ret = ec_ec_master_base_charge_control(current_base, + otg_voltage, allow_charge_base); + if (ret) + return; + } + + if (current_lid >= 0) { + ret = charge_set_output_current_limit(0, 0); + if (ret) + return; + ret = charger_set_input_current(current_lid); + if (ret) + return; + if (allow_charge_lid) + ret = charge_request(curr.requested_voltage, + curr.requested_current); + else + ret = charge_request(0, 0); + } else { + ret = charge_set_output_current_limit( + -current_lid, otg_voltage); + } + + if (ret) + return; + + if (lid_first && base_connected) { + ret = ec_ec_master_base_charge_control(current_base, + otg_voltage, allow_charge_base); + if (ret) + return; + } + + prev_current_base = current_base; + prev_allow_charge_base = allow_charge_base; + prev_current_lid = current_lid; +} + +/** + * Smooth power value, covering some edge cases. + * Compute s*curr+(1-s)*prev, where s is in 1/128 unit. + */ +static int smooth_value(int prev, int curr, int s) +{ + if (curr < 0) + curr = 0; + if (prev < 0) + return curr; + + return prev + s * (curr - prev) / 128; +} + +/** + * Add margin m to value. Compute (1+m)*value, where m is in 1/128 unit. + */ +static int add_margin(int value, int m) +{ + return value + m * value / 128; +} + +static void charge_allocate_input_current_limit(void) +{ + /* + * All the power numbers are in mW. + * + * Since we work with current and voltage in mA and mV, multiplying them + * gives numbers in uW, which are dangerously close to overflowing when + * doing intermediate computations (60W * 100 overflows a 32-bit int, + * for example). We therefore divide the product by 1000 and re-multiply + * the power numbers by 1000 when converting them back to current. + */ + int total_power = 0; + + static int prev_base_battery_power = -1; + int base_battery_power = 0; + int base_battery_power_max = 0; + + static int prev_lid_system_power = -1; + int lid_system_power; + + static int prev_lid_battery_power = -1; + int lid_battery_power = 0; + int lid_battery_power_max = 0; + + int power_base = 0; + int power_lid = 0; + + int current_base = 0; + int current_lid = 0; + + int charge_lid = charge_get_percent(); + + if (!base_connected) { + set_base_lid_current(0, 0, curr.desired_input_current, 1); + prev_base_battery_power = -1; + return; + } + + /* Charging */ + if (curr.desired_input_current > 0 && curr.input_voltage > 0) + total_power = + curr.desired_input_current * curr.input_voltage / 1000; + + /* + * TODO(b:71723024): We should be able to replace this test by curr.ac, + * but the value is currently wrong, especially during transitions. + */ + if (total_power <= 0) { + /* Discharging */ + prev_base_battery_power = -1; + prev_lid_system_power = -1; + prev_lid_battery_power = -1; + + if (charge_base > db_policy.min_charge_base_otg) { + int lid_current = db_policy.max_base_to_lid_current; + int base_current = add_margin(lid_current, + db_policy.margin_otg_current); + /* Draw current from base to lid */ + set_base_lid_current(-base_current, 0, lid_current, + charge_lid < db_policy.max_charge_lid_batt_to_batt); + } else { + /* + * Base battery is too low, apply power to it, and allow + * it to charge if it connected, and it is critically + * low. + * + * TODO(b:71881017): This will make the battery charge + * oscillate between 3 and 4 percent, which might not be + * great for battery life. We need some hysteresis. + */ + int base_current = db_policy.min_base_system_power; + int lid_current = add_margin(base_current, + db_policy.margin_otg_current); + + int allow_charge = charge_base >= 0 && + charge_base < db_policy.max_charge_base_batt_to_batt; + + set_base_lid_current(base_current, allow_charge, + -lid_current, 0); + } + + return; + } + + /* Estimate system power. */ + lid_system_power = charger_get_system_power() / 1000; + + /* Smooth system power, as it is very spiky */ + lid_system_power = smooth_value(prev_lid_system_power, + lid_system_power, db_policy.lid_system_power_smooth); + prev_lid_system_power = lid_system_power; + + /* + * TODO(b:71881017): Smoothing the battery power isn't necessarily a + * good idea: if the system takes up too much power, we may reduce the + * estimate power too quickly, leading to oscillations when the system + * power goes down. Instead, we should probably estimate the current + * based on remaining capacity. + */ + /* Estimate lid battery power. */ + if (!(curr.batt.flags & + (BATT_FLAG_BAD_VOLTAGE | BATT_FLAG_BAD_CURRENT))) + lid_battery_power = curr.batt.current * + curr.batt.voltage / 1000; + if (lid_battery_power < prev_lid_battery_power) + lid_battery_power = smooth_value(prev_lid_battery_power, + lid_battery_power, db_policy.battery_power_smooth); + if (!(curr.batt.flags & + (BATT_FLAG_BAD_DESIRED_VOLTAGE | + BATT_FLAG_BAD_DESIRED_CURRENT))) + lid_battery_power_max = curr.batt.desired_current * + curr.batt.desired_voltage / 1000; + + lid_battery_power = MIN(lid_battery_power, lid_battery_power_max); + + /* Estimate base battery power. */ + if (!(base_battery_dynamic.flags & EC_BATT_FLAG_INVALID_DATA)) { + base_battery_power = base_battery_dynamic.actual_current * + base_battery_dynamic.actual_voltage / 1000; + base_battery_power_max = base_battery_dynamic.desired_current * + base_battery_dynamic.desired_voltage / 1000; + } + if (base_battery_power < prev_base_battery_power) + base_battery_power = smooth_value(prev_base_battery_power, + base_battery_power, db_policy.battery_power_smooth); + base_battery_power = MIN(base_battery_power, base_battery_power_max); + + if (debugging) { + CPRINTF("%s:\n", __func__); + CPRINTF("total power: %d\n", total_power); + CPRINTF("base battery power: %d (%d)\n", + base_battery_power, base_battery_power_max); + CPRINTF("lid system power: %d\n", lid_system_power); + CPRINTF("lid battery power: %d\n", lid_battery_power); + CPRINTF("percent base/lid: %d%% %d%%\n", + charge_base, charge_lid); + } + + prev_lid_battery_power = lid_battery_power; + prev_base_battery_power = base_battery_power; + + if (total_power > 0) { /* Charging */ + /* Allocate system power */ + CHG_ALLOCATE(power_base, total_power, + db_policy.min_base_system_power); + CHG_ALLOCATE(power_lid, total_power, lid_system_power); + + /* Allocate lid, then base battery power */ + lid_battery_power = add_margin(lid_battery_power, + db_policy.margin_lid_battery_power); + CHG_ALLOCATE(power_lid, total_power, lid_battery_power); + + base_battery_power = add_margin(base_battery_power, + db_policy.margin_base_battery_power); + CHG_ALLOCATE(power_base, total_power, base_battery_power); + + /* Give everything else to the lid. */ + CHG_ALLOCATE(power_lid, total_power, total_power); + if (debugging) + CPRINTF("power: base %d mW / lid %d mW\n", + power_base, power_lid); + + current_base = 1000 * power_base / curr.input_voltage; + current_lid = 1000 * power_lid / curr.input_voltage; + + if (current_base > db_policy.max_lid_to_base_current) { + current_lid += (current_base + - db_policy.max_lid_to_base_current); + current_base = db_policy.max_lid_to_base_current; + } + + if (debugging) + CPRINTF("current: base %d mA / lid %d mA\n", + current_base, current_lid); + + set_base_lid_current(current_base, 1, current_lid, 1); + } else { /* Discharging */ + } + + if (debugging) + CPRINTF("====\n"); +} +#endif /* CONFIG_EC_EC_COMM_BATTERY_MASTER */ + #ifdef HAS_TASK_HOSTCMD /* Returns zero if every item was updated. */ static int update_static_battery_info(void) @@ -451,6 +846,9 @@ static void dump_charge_state(void) #ifdef CONFIG_CHARGER_OTG DUMP(output_current, "%dmA"); #endif +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + DUMP(input_voltage, "%dmV"); +#endif ccprintf("chg_ctl_mode = %d\n", chg_ctl_mode); ccprintf("manual_mode = %d\n", manual_mode); ccprintf("user_current_limit = %dmA\n", user_current_limit); @@ -510,6 +908,10 @@ static void show_charging_progress(void) to_full ? "to full" : "to empty", is_full ? ", not accepting current" : ""); +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + CPRINTS("Base battery %d%%", charge_base); +#endif + if (debugging) { ccprintf("battery:\n"); print_battery_debug(); @@ -744,6 +1146,8 @@ const struct batt_params *charger_current_battery_params(void) return &curr.batt; } +/*****************************************************************************/ +/* Hooks */ void charger_init(void) { /* Initialize current state */ @@ -752,6 +1156,15 @@ void charger_init(void) } DECLARE_HOOK(HOOK_INIT, charger_init, HOOK_PRIO_DEFAULT); +/* Wake up the task when something important happens */ +static void charge_wakeup(void) +{ + task_wake(TASK_ID_CHARGER); +} +DECLARE_HOOK(HOOK_CHIPSET_RESUME, charge_wakeup, HOOK_PRIO_DEFAULT); +DECLARE_HOOK(HOOK_AC_CHANGE, charge_wakeup, HOOK_PRIO_DEFAULT); + + static int get_desired_input_current(enum battery_present batt_present, const struct charger_info * const info) { @@ -788,6 +1201,10 @@ void charger_task(void *u) chg_ctl_mode = CHARGE_CONTROL_NORMAL; shutdown_warning_time.val = 0UL; battery_seems_to_be_dead = 0; +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + prev_base_connected = -1; + curr.input_voltage = CHARGE_VOLTAGE_UNINITIALIZED; +#endif /* * If system is not locked and we don't have a battery to live on, @@ -806,6 +1223,15 @@ void charger_task(void *u) problems_exist = 0; battery_critical = 0; curr.ac = extpower_is_present(); +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + /* + * When base is powering the system, make sure curr.ac stays 0. + * TODO(b:71723024): Fix extpower_is_present() in hardware + * instead. + */ + if (base_connected && prev_current_base < 0) + curr.ac = 0; +#endif if (curr.ac != prev_ac) { if (curr.ac) { /* @@ -834,6 +1260,36 @@ void charger_task(void *u) prev_ac = curr.ac; } } + +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + base_connected = board_is_base_connected(); + + if (prev_base_connected != base_connected) { + /* Invalidate static/dynamic information */ + base_battery_dynamic.flags = EC_BATT_FLAG_INVALID_DATA; + charge_base = -1; + + if (base_connected) { + /* Redo power allocation (e.g. enable OTG). */ + charge_allocate_input_current_limit(); + /* Apply power to the base */ + board_enable_base_power(1); + } + + prev_base_connected = base_connected; + } + + if (base_connected) { + /* + * TODO(b:71881017): Be smart about static info and do + * not fetch it over and over again. + */ + ec_ec_master_base_get_static_info(); + ec_ec_master_base_get_dynamic_info(); + charge_base = charge_get_base_percent(); + } +#endif + charger_get_params(&curr.chg); battery_get_params(&curr.batt); @@ -1033,9 +1489,15 @@ wait_for_it: is_full = calc_is_full(); if ((!(curr.batt.flags & BATT_FLAG_BAD_STATE_OF_CHARGE) && curr.batt.state_of_charge != prev_charge) || +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + (charge_base != prev_charge_base) || +#endif (is_full != prev_full)) { show_charging_progress(); prev_charge = curr.batt.state_of_charge; +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + prev_charge_base = charge_base; +#endif hook_notify(HOOK_BATTERY_SOC_CHANGE); } prev_full = is_full; @@ -1094,7 +1556,12 @@ wait_for_it: curr.requested_current = 0; #endif } + +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + charge_allocate_input_current_limit(); +#else charge_request(curr.requested_voltage, curr.requested_current); +#endif /* How long to sleep? */ if (problems_exist) @@ -1364,21 +1831,17 @@ int charge_set_input_current_limit(int ma, int mv) ma = MIN(ma, CONFIG_CHARGER_MAX_INPUT_CURRENT); #endif curr.desired_input_current = ma; +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + curr.input_voltage = mv; + /* Wake up charger task to allocate current between lid and base. */ + charge_wakeup(); + return EC_SUCCESS; +#else return charger_set_input_current(ma); +#endif } /*****************************************************************************/ -/* Hooks */ - -/* Wake up the task when something important happens */ -static void charge_wakeup(void) -{ - task_wake(TASK_ID_CHARGER); -} -DECLARE_HOOK(HOOK_CHIPSET_RESUME, charge_wakeup, HOOK_PRIO_DEFAULT); -DECLARE_HOOK(HOOK_AC_CHANGE, charge_wakeup, HOOK_PRIO_DEFAULT); - -/*****************************************************************************/ /* Host commands */ static int charge_command_charge_control(struct host_cmd_handler_args *args) diff --git a/include/charge_state_v2.h b/include/charge_state_v2.h index f9f5d39edf..3fc9a94181 100644 --- a/include/charge_state_v2.h +++ b/include/charge_state_v2.h @@ -6,6 +6,7 @@ #include "battery.h" #include "battery_smart.h" #include "charger.h" +#include "ec_ec_comm_master.h" #include "timer.h" #ifndef __CROS_EC_CHARGE_STATE_V2_H @@ -41,6 +42,9 @@ struct charge_state_data { #ifdef CONFIG_CHARGER_OTG int output_current; #endif +#ifdef CONFIG_EC_EC_COMM_BATTERY_MASTER + int input_voltage; +#endif }; /** @@ -74,5 +78,16 @@ int charge_set_input_current_limit(int ma, int mv); int charge_get_charge_state_debug(int param, uint32_t *value); #endif /* CONFIG_CHARGE_STATE_DEBUG */ +/** + * Board-specific routine to indicate if the base is connected. + */ +int board_is_base_connected(void); + +/** + * Board-specific routine to enable power distribution between lid and base + * (current can flow both ways). + */ +void board_enable_base_power(int enable); + #endif /* __CROS_EC_CHARGE_STATE_V2_H */ diff --git a/include/ec_commands.h b/include/ec_commands.h index 10ac439215..00c6dad089 100644 --- a/include/ec_commands.h +++ b/include/ec_commands.h @@ -163,7 +163,7 @@ #define EC_BATT_FLAG_DISCHARGING 0x04 #define EC_BATT_FLAG_CHARGING 0x08 #define EC_BATT_FLAG_LEVEL_CRITICAL 0x10 -/* Set if some of the dynamic data is invalid (or outdated). */ +/* Set if some of the static/dynamic data is invalid (or outdated). */ #define EC_BATT_FLAG_INVALID_DATA 0x20 /* Switch flags at EC_MEMMAP_SWITCHES */ diff --git a/util/ectool.c b/util/ectool.c index c0aa05e06c..155947d4a2 100644 --- a/util/ectool.c +++ b/util/ectool.c @@ -6076,6 +6076,11 @@ int get_battery_command(int index) if (rv < 0) return -1; + if (dynamic_r.flags & EC_BATT_FLAG_INVALID_DATA) { + printf(" Invalid data (not present?)\n"); + return -1; + } + if (!is_string_printable(static_r.manufacturer)) goto cmd_error; printf(" OEM name: %s\n", static_r.manufacturer); |