diff options
Diffstat (limited to 'common/dps.c')
-rw-r--r-- | common/dps.c | 639 |
1 files changed, 639 insertions, 0 deletions
diff --git a/common/dps.c b/common/dps.c new file mode 100644 index 0000000000..235f4d4e08 --- /dev/null +++ b/common/dps.c @@ -0,0 +1,639 @@ +/* Copyright 2021 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Dynamic PDO Selection. + */ + +#include <stdint.h> + +#include "adc.h" +#include "dps.h" +#include "atomic.h" +#include "battery.h" +#include "console.h" +#include "charger.h" +#include "charge_manager.h" +#include "charge_state.h" +#include "charge_state_v2.h" +#include "math_util.h" +#include "task.h" +#include "timer.h" +#include "usb_common.h" +#include "usb_pd.h" +#include "util.h" +#include "usb_pe_sm.h" + + +#define K_MORE_PWR 96 +#define K_LESS_PWR 93 +#define K_SAMPLE 1 +#define K_WINDOW 3 +#define T_REQUEST_STABLE_TIME (10 * SECOND) +#define T_NEXT_CHECK_TIME (5 * SECOND) + +#define DPS_FLAG_DISABLED BIT(0) +#define DPS_FLAG_NO_SRCCAP BIT(1) +#define DPS_FLAG_WAITING BIT(2) +#define DPS_FLAG_SAMPLED BIT(3) +#define DPS_FLAG_NEED_MORE_PWR BIT(4) + +#define DPS_FLAG_STOP_EVENTS (DPS_FLAG_DISABLED | \ + DPS_FLAG_NO_SRCCAP) +#define DPS_FLAG_ALL GENMASK(31, 0) + +#define MAX_MOVING_AVG_WINDOW 5 + +BUILD_ASSERT(K_MORE_PWR > K_LESS_PWR && 100 >= K_MORE_PWR && 100 >= K_LESS_PWR); + +/* lock for updating timeout value */ +static mutex_t dps_lock; +static timestamp_t timeout; +static bool is_enabled = true; +static int debug_level; +static bool fake_enabled; +static int fake_mv, fake_ma; +static int dynamic_mv; +static int dps_port = CHARGE_PORT_NONE; +static uint32_t flag; + +#define CPRINTF(format, args...) cprintf(CC_USBPD, "DPS " format, ##args) +#define CPRINTS(format, args...) cprints(CC_USBPD, "DPS " format, ##args) + +__overridable struct dps_config_t dps_config = { + .k_less_pwr = K_LESS_PWR, + .k_more_pwr = K_MORE_PWR, + .k_sample = K_SAMPLE, + .k_window = K_WINDOW, + .t_stable = T_REQUEST_STABLE_TIME, + .t_check = T_NEXT_CHECK_TIME, + .is_more_efficient = NULL, +}; + +int dps_get_dynamic_voltage(void) +{ + return dynamic_mv; +} + +int dps_get_charge_port(void) +{ + return dps_port; +} + +bool dps_is_enabled(void) +{ + return is_enabled; +} + +static void dps_enable(bool en) +{ + bool prev_en = is_enabled; + + is_enabled = en; + + if (is_enabled && !prev_en) + task_wake(TASK_ID_DPS); +} + +static void update_timeout(int us) +{ + timestamp_t new_timeout; + + new_timeout.val = get_time().val + us; + + mutex_lock(&dps_lock); + if (new_timeout.val > timeout.val) + timeout = new_timeout; + mutex_unlock(&dps_lock); +} + +/* + * DPS reset. + */ +static void dps_reset(void) +{ + dynamic_mv = PD_MAX_VOLTAGE_MV; + dps_port = CHARGE_PORT_NONE; +} + +/* + * DPS initialization. + */ +static void dps_init(void) +{ + dps_reset(); + + if (dps_config.k_window > MAX_MOVING_AVG_WINDOW) { + dps_config.k_window = MAX_MOVING_AVG_WINDOW; + CPRINTS("ERR:WIN"); + } + + if (dps_config.k_less_pwr > 100 || + dps_config.k_more_pwr > 100 || + dps_config.k_more_pwr <= dps_config.k_less_pwr) { + dps_config.k_less_pwr = K_LESS_PWR; + dps_config.k_more_pwr = K_MORE_PWR; + CPRINTS("ERR:COEF"); + } +} + +static bool is_near_limit(int val, int limit) +{ + return val >= (limit * dps_config.k_more_pwr / 100); +} + +bool is_more_efficient(int curr_mv, int prev_mv, int batt_mv, int batt_mw, + int input_mw) +{ + if (dps_config.is_more_efficient) + return dps_config.is_more_efficient(curr_mv, prev_mv, batt_mv, + batt_mw, input_mw); + + return ABS(curr_mv - batt_mv) < ABS(prev_mv - batt_mv); +} + +/* + * Get the input power of the active port. + * + * input_power = vbus * input_current + * + * @param vbus: VBUS in mV + * @param input_curr: input current in mA + * + * @return input_power of the result of vbus * input_curr in mW + */ +static int get_desired_input_power(int *vbus, int *input_current) +{ + int active_port; + int charger_id; + enum ec_error_list rv; + + active_port = charge_manager_get_active_charge_port(); + + if (active_port == CHARGE_PORT_NONE) + return 0; + + charger_id = charge_get_active_chg_chip(); + + if (fake_enabled) { + *vbus = fake_mv; + *input_current = fake_ma; + return fake_mv * fake_ma / 1000; + } + + rv = charger_get_input_current(charger_id, input_current); + if (rv) + return 0; + + *vbus = charge_manager_get_vbus_voltage(active_port); + + return (*vbus) * (*input_current) / 1000; +} + +/* + * Get the most efficient PDO voltage for the battery of the charging port + * + * | W\Batt | 1S(3.7V) | 2S(7.4V) | 3S(11.1V) | 4S(14.8V) | + * -------------------------------------------------------- + * | 0-15W | 5V | 9V | 12V | 15V | + * | 15-27W | 9V | 9V | 12V | 15V | + * | 27-36W | 12V | 12V | 12V | 15V | + * | 36-45W | 15V | 15V | 15V | 15V | + * | 45-60W | 20V | 20V | 20V | 20V | + * + * + * @return 0 if error occurs, else battery efficient voltage in mV + */ +int get_efficient_voltage(void) +{ + int eff_mv = 0; + int batt_mv; + int batt_pwr; + int input_pwr, vbus, input_curr; + const struct batt_params *batt = charger_current_battery_params(); + + input_pwr = get_desired_input_power(&vbus, &input_curr); + + if (!input_pwr) + return 0; + + if (battery_design_voltage(&batt_mv)) + return 0; + + batt_pwr = batt->current * batt->voltage / 1000; + + for (int i = 0; i < board_get_usb_pd_port_count(); ++i) { + const int cnt = pd_get_src_cap_cnt(i); + const uint32_t *src_caps = pd_get_src_caps(i); + + for (int j = 0; j < cnt; ++j) { + int ma, mv, unused; + + pd_extract_pdo_power(src_caps[j], &ma, &mv, &unused); + /* + * If the eff_mv is not picked, or we have more + * efficient voltage (less voltage diff) + */ + if (eff_mv == 0 || + is_more_efficient(mv, eff_mv, batt_mv, batt_pwr, + input_pwr)) + eff_mv = mv; + } + } + + return eff_mv; +} + +struct pdo_candidate { + int port; + int mv; + int mw; +}; + +#define UPDATE_CANDIDATE(new_port, new_mv, new_mw) \ + do { \ + cand->port = new_port; \ + cand->mv = new_mv; \ + cand->mw = new_mw; \ + } while (0) + +#define CLEAR_AND_RETURN() \ + do { \ + moving_avg_count = 0; \ + return false; \ + } while (0) + +/* + * Evaluate the system power if a new PD power request is needed. + * + * @param struct pdo_candidate: The candidate PDO. (Return value) + * @return true if a new power request, or false otherwise. + */ +static bool has_new_power_request(struct pdo_candidate *cand) +{ + int vbus, input_curr, input_pwr; + int input_pwr_avg = 0, input_curr_avg = 0; + int batt_pwr, batt_mv; + int max_mv = pd_get_max_voltage(); + int req_pwr, req_ma, req_mv; + int input_curr_limit; + int active_port = charge_manager_get_active_charge_port(); + int charger_id; + static int input_pwrs[MAX_MOVING_AVG_WINDOW]; + static int input_currs[MAX_MOVING_AVG_WINDOW]; + static int prev_active_port = CHARGE_PORT_NONE; + static int prev_req_mv; + static int moving_avg_count; + const struct batt_params *batt = charger_current_battery_params(); + + /* set a default value in case it early returns. */ + UPDATE_CANDIDATE(CHARGE_PORT_NONE, INT32_MAX, 0); + + if (active_port == CHARGE_PORT_NONE) + CLEAR_AND_RETURN(); + + req_mv = pd_get_requested_voltage(active_port); + req_ma = pd_get_requested_current(active_port); + + if (!req_mv) + CLEAR_AND_RETURN(); + + if (battery_design_voltage(&batt_mv)) + CLEAR_AND_RETURN(); + + /* if last sample is not the same as the current one, reset counting. */ + if (prev_req_mv != req_mv || prev_active_port != active_port) + moving_avg_count = 0; + prev_active_port = active_port; + prev_req_mv = req_mv; + + req_pwr = req_mv * req_ma / 1000; + batt_pwr = batt->current * batt->voltage / 1000; + input_pwr = get_desired_input_power(&vbus, &input_curr); + + if (!input_pwr) + CLEAR_AND_RETURN(); + + /* record moving average */ + input_pwrs[moving_avg_count % dps_config.k_window] = input_pwr; + input_currs[moving_avg_count % dps_config.k_window] = input_curr; + if (++moving_avg_count < dps_config.k_window) + return false; + + for (int i = 0; i < dps_config.k_window; i++) { + input_curr_avg += input_currs[i]; + input_pwr_avg += input_pwrs[i]; + } + input_curr_avg /= dps_config.k_window; + input_pwr_avg /= dps_config.k_window; + + charger_id = charge_get_active_chg_chip(); + + if (!charger_get_input_current_limit(charger_id, &input_curr_limit)) + /* set as last requested mA if we're unable to get the limit. */ + input_curr_limit = req_ma; + + /* + * input power might be insufficient, force it to negotiate a more + * powerful PDO. + */ + if (is_near_limit(input_pwr_avg, req_pwr) || + is_near_limit(input_curr_avg, MIN(req_ma, input_curr_limit))) { + flag |= DPS_FLAG_NEED_MORE_PWR; + if (!fake_enabled) + input_pwr_avg = req_pwr + 1; + } else { + flag &= ~DPS_FLAG_NEED_MORE_PWR; + } + + if (debug_level) + CPRINTS("C%d 0x%x last (%dmW %dmV) input (%dmW %dmV %dmA) " + "avg (%dmW, %dmA)", + active_port, flag, req_pwr, req_mv, input_pwr, vbus, + input_curr, input_pwr_avg, input_curr_avg); + + for (int i = 0; i < board_get_usb_pd_port_count(); ++i) { + const uint32_t * const src_caps = pd_get_src_caps(i); + + for (int j = 0; j < pd_get_src_cap_cnt(i); ++j) { + int ma, mv, unused; + int mw; + bool efficient; + + /* TODO(b:169532537): support augmented PDO. */ + if ((src_caps[j] & PDO_TYPE_MASK) != PDO_TYPE_FIXED) + continue; + + pd_extract_pdo_power(src_caps[j], &ma, &mv, &unused); + + if (mv > max_mv) + continue; + + mw = ma * mv / 1000; + efficient = is_more_efficient(mv, cand->mv, batt_mv, + batt_pwr, input_pwr_avg); + + if (flag & DPS_FLAG_NEED_MORE_PWR) { + /* the insufficient case.*/ + if (input_pwr_avg > cand->mw && + (mw > cand->mw || + (mw == cand->mw && efficient))) { + UPDATE_CANDIDATE(i, mv, mw); + } else if (input_pwr_avg <= mw && efficient) { + UPDATE_CANDIDATE(i, mv, mw); + } + } else { + int adjust_pwr = + mw * dps_config.k_less_pwr / 100; + int adjust_cand_mw = + cand->mw * dps_config.k_less_pwr / 100; + + /* Pick if we don't have a candidate yet. */ + if (!cand->mw) { + UPDATE_CANDIDATE(i, mv, mw); + /* + * if the candidate is insufficient, and + * we get one provides more. + */ + } else if ((adjust_cand_mw < input_pwr_avg && + cand->mw < mw) || + /* + * if the candidate is sufficient, + * and we pick a more efficient one. + */ + (adjust_cand_mw >= input_pwr_avg && + adjust_pwr >= input_pwr_avg && + efficient)) { + UPDATE_CANDIDATE(i, mv, mw); + } + } + + + /* + * if the candidate is the same as the current one, pick + * the one at active charge port. + */ + if (mw == cand->mw && mv == cand->mv && + i == active_port) + UPDATE_CANDIDATE(i, mv, mw); + } + } + + if (!cand->mv) + CPRINTS("ERR:CNDMV"); + + return (cand->mv != req_mv); +} + +static bool has_srccap(void) +{ + for (int i = 0; i < board_get_usb_pd_port_count(); ++i) { + if (pd_is_connected(i) && + pd_get_power_role(i) == PD_ROLE_SINK && + pd_get_src_cap_cnt(i) > 0) + return true; + } + return false; +} + +void dps_update_stabilized_time(int port) +{ + update_timeout(dps_config.t_stable); +} + +void dps_task(void *u) +{ + struct pdo_candidate last_cand = {CHARGE_PORT_NONE, 0, 0}; + int sample_count = 0; + + dps_init(); + update_timeout(dps_config.t_check); + + while (1) { + struct pdo_candidate curr_cand = {CHARGE_PORT_NONE, 0, 0}; + timestamp_t now; + + now = get_time(); + if (flag & DPS_FLAG_STOP_EVENTS) { + dps_reset(); + task_wait_event(-1); + /* clear flags after wake up. */ + flag = 0; + update_timeout(dps_config.t_check); + continue; + } else if (now.val < timeout.val) { + flag |= DPS_FLAG_WAITING; + task_wait_event(timeout.val - now.val); + flag &= ~DPS_FLAG_WAITING; + } + + if (!is_enabled) { + flag |= DPS_FLAG_DISABLED; + continue; + } + + if (!has_srccap()) { + flag |= DPS_FLAG_NO_SRCCAP; + continue; + } + + if (!has_new_power_request(&curr_cand)) { + sample_count = 0; + flag &= ~DPS_FLAG_SAMPLED; + } else { + if (last_cand.port == curr_cand.port && + last_cand.mv == curr_cand.mv && + last_cand.mw == curr_cand.mw) + sample_count++; + else + sample_count = 1; + flag |= DPS_FLAG_SAMPLED; + } + + if (sample_count == dps_config.k_sample) { + dynamic_mv = curr_cand.mv; + dps_port = curr_cand.port; + pd_dpm_request(dps_port, + DPM_REQUEST_NEW_POWER_LEVEL); + sample_count = 0; + flag &= ~(DPS_FLAG_SAMPLED | DPS_FLAG_NEED_MORE_PWR); + } + + last_cand.port = curr_cand.port; + last_cand.mv = curr_cand.mv; + last_cand.mw = curr_cand.mw; + + update_timeout(dps_config.t_check); + } +} + +static int command_dps(int argc, char **argv) +{ + int port = charge_manager_get_active_charge_port(); + int input_pwr, vbus, input_curr; + int holder; + + if (argc == 1) { + uint32_t last_ma = 0, last_mv = 0; + int batt_mv; + + ccprintf("flag=0x%x k_more=%d k_less=%d k_sample=%d k_win=%d\n", + flag, dps_config.k_more_pwr, dps_config.k_less_pwr, + dps_config.k_sample, dps_config.k_window); + ccprintf("t_stable=%d t_check=%d\n", + dps_config.t_stable / SECOND, + dps_config.t_check / SECOND); + if (!is_enabled) { + ccprintf("DPS Disabled\n"); + return EC_SUCCESS; + } + + if (port == CHARGE_PORT_NONE) { + ccprintf("No charger attached\n"); + return EC_SUCCESS; + } + + battery_design_voltage(&batt_mv); + input_pwr = get_desired_input_power(&vbus, &input_curr); + if (!(flag & DPS_FLAG_NO_SRCCAP)) { + last_mv = pd_get_requested_voltage(port); + last_ma = pd_get_requested_current(port); + } + ccprintf("C%d DPS Enabled\n" + "Requested: %dmV/%dmA\n" + "Measured: %dmV/%dmA/%dmW\n" + "Efficient: %dmV\n" + "Batt: %dmv\n" + "PDMaxMV: %dmV\n", + port, last_mv, last_ma, + vbus, input_curr, input_pwr, + get_efficient_voltage(), + batt_mv, + pd_get_max_voltage()); + return EC_SUCCESS; + } + + if (!strcasecmp(argv[1], "en")) { + dps_enable(true); + return EC_SUCCESS; + } else if (!strcasecmp(argv[1], "dis")) { + dps_enable(false); + return EC_SUCCESS; + } else if (!strcasecmp(argv[1], "fakepwr")) { + if (argc == 2) { + ccprintf("%sabled %dmV/%dmA\n", + fake_enabled ? "en" : "dis", fake_mv, fake_ma); + return EC_SUCCESS; + } + + if (!strcasecmp(argv[2], "dis")) { + fake_enabled = false; + return EC_SUCCESS; + } + + if (argc < 4) + return EC_ERROR_PARAM_COUNT; + + holder = atoi(argv[2]); + if (holder <= 0) + return EC_ERROR_PARAM2; + fake_mv = holder; + + holder = atoi(argv[3]); + if (holder <= 0) + return EC_ERROR_PARAM3; + fake_ma = holder; + + fake_enabled = true; + return EC_SUCCESS; + } + + if (argc != 3) + return EC_ERROR_PARAM2; + + if (!strcasecmp(argv[1], "debug")) { + debug_level = atoi(argv[2]); + } else if (!strcasecmp(argv[1], "setkmore")) { + holder = atoi(argv[2]); + if (holder > 100 || holder <= 0 || + holder < dps_config.k_less_pwr) + return EC_ERROR_PARAM2; + dps_config.k_more_pwr = holder; + } else if (!strcasecmp(argv[1], "setkless")) { + holder = atoi(argv[2]); + if (holder > 100 || holder <= 0 || + holder > dps_config.k_more_pwr) + return EC_ERROR_PARAM2; + dps_config.k_less_pwr = holder; + } else if (!strcasecmp(argv[1], "setksample")) { + holder = atoi(argv[2]); + if (holder <= 0) + return EC_ERROR_PARAM2; + dps_config.k_sample = holder; + } else if (!strcasecmp(argv[1], "setkwin")) { + holder = atoi(argv[2]); + if (holder <= 0 || holder > MAX_MOVING_AVG_WINDOW) + return EC_ERROR_PARAM2; + dps_config.k_window = holder; + } else if (!strcasecmp(argv[1], "settcheck")) { + holder = atoi(argv[2]); + if (holder <= 0) + return EC_ERROR_PARAM2; + dps_config.t_check = holder * SECOND; + } else if (!strcasecmp(argv[1], "settstable")) { + holder = atoi(argv[2]); + if (holder <= 0) + return EC_ERROR_PARAM2; + dps_config.t_stable = holder * SECOND; + } else { + return EC_ERROR_PARAM1; + } + + return EC_SUCCESS; +} +DECLARE_CONSOLE_COMMAND(dps, command_dps, + "en|dis|debug <int>\n" + "\t\t set(kmore|kless|ksample|kwindow) <int>\n" + "\t\t set(tstable|tcheck) <int>\n" + "\t\t fakepwr [dis|<mV> <mA>]", + "Print/set Dynamic PDO Selection state."); |