blob: 5319f9dd4118217b5706091d58147c83ee0ec773 [file] [log] [blame]
/* Copyright 2020 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*
* Dual Role (Source & Sink) USB-PD module.
*/
#include "charge_manager.h"
#include "charge_state.h"
#include "dps.h"
#include "system.h"
#include "usb_common.h"
#include "usb_pd.h"
#include "util.h"
#define CPRINTS(format, args...) cprints(CC_USBPD, format, ##args)
/* The macro is used to prevent a DBZ exception while decoding PDOs. */
#define PROCESS_ZERO_DIVISOR(x) ((x) == 0 ? 1 : (x))
#if defined(PD_MAX_VOLTAGE_MV) && defined(PD_OPERATING_POWER_MW)
/*
* As a sink, this is the max voltage (in millivolts) we can request
* before getting source caps
*/
static unsigned int max_request_mv = PD_MAX_VOLTAGE_MV;
void pd_set_max_voltage(unsigned int mv)
{
max_request_mv = mv;
}
unsigned int pd_get_max_voltage(void)
{
return max_request_mv;
}
/**
* Return true if port is capable of communication over USB data lines.
*
* @param port USB-C port number
*/
static bool pd_get_usb_comm_capable(int port)
{
uint32_t fixed_pdo;
/* the fixed PDO is always the first entry */
if (pd_get_power_role(port) == PD_ROLE_SINK) {
fixed_pdo = pd_snk_pdo[0];
} else {
const uint32_t *pdo;
pd_get_source_pdo(&pdo, port);
fixed_pdo = pdo[0];
}
return !!(fixed_pdo & PDO_FIXED_COMM_CAP);
}
/*
* Zinger implements a board specific usb policy that does not define
* PD_MAX_VOLTAGE_MV and PD_OPERATING_POWER_MW. And in turn, does not
* use the following functions.
*/
int pd_find_pdo_index(uint32_t src_cap_cnt, const uint32_t *const src_caps,
int max_mv, uint32_t *selected_pdo)
{
int i, uw, mv;
int ret = 0;
int cur_uw = 0;
int has_preferred_pdo;
int prefer_cur;
int __attribute__((unused)) cur_mv = 0;
/* max voltage is always limited by this boards max request */
max_mv = MIN(max_mv, PD_MAX_VOLTAGE_MV);
/* Get max power that is under our max voltage input */
for (i = 0; i < src_cap_cnt; i++) {
if (IS_ENABLED(CONFIG_USB_PD_ONLY_FIXED_PDOS) &&
(src_caps[i] & PDO_TYPE_MASK) != PDO_TYPE_FIXED)
continue;
/* its an unsupported Augmented PDO (PD3.0) */
if ((src_caps[i] & PDO_TYPE_MASK) == PDO_TYPE_AUGMENTED)
continue;
mv = PDO_FIXED_GET_VOLT(src_caps[i]);
/* Skip invalid voltage */
if (!mv)
continue;
/*
* It's illegal to have EPR PDO in 1...7.
* TODO: This is supposed to be a hard reset (8.3.3.3.8)
*/
if (i < 7 && mv > PD_MAX_SPR_VOLTAGE)
continue;
/* Skip any voltage not supported by this board */
if (!pd_is_valid_input_voltage(mv))
continue;
if ((src_caps[i] & PDO_TYPE_MASK) == PDO_TYPE_BATTERY) {
uw = 250000 * (src_caps[i] & 0x3FF);
} else {
int ma = PDO_FIXED_GET_CURR(src_caps[i]);
ma = MIN(ma, PD_MAX_CURRENT_MA);
uw = ma * mv;
}
if (mv > max_mv)
continue;
uw = MIN(uw, PD_MAX_POWER_MW * 1000);
prefer_cur = 0;
/* Apply special rules in favor of voltage */
if (IS_ENABLED(PD_PREFER_LOW_VOLTAGE)) {
if (uw == cur_uw && mv < cur_mv)
prefer_cur = 1;
} else if (IS_ENABLED(PD_PREFER_HIGH_VOLTAGE)) {
if (uw == cur_uw && mv > cur_mv)
prefer_cur = 1;
}
/* Prefer higher power, except for tiebreaker */
has_preferred_pdo = prefer_cur || (uw > cur_uw);
if (has_preferred_pdo) {
ret = i;
cur_uw = uw;
cur_mv = mv;
}
}
if (selected_pdo)
*selected_pdo = src_caps[ret];
return ret;
}
void pd_extract_pdo_power(uint32_t pdo, uint32_t *ma, uint32_t *max_mv,
uint32_t *min_mv)
{
int max_ma, mw;
if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_FIXED) {
*max_mv = PDO_FIXED_VOLTAGE(pdo);
*min_mv = *max_mv;
} else if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_AUGMENTED) {
*max_mv = PDO_AUG_MAX_VOLTAGE(pdo);
*min_mv = PDO_AUG_MIN_VOLTAGE(pdo);
} else if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_VARIABLE) {
*max_mv = PDO_VAR_MAX_VOLTAGE(pdo);
*min_mv = PDO_VAR_MIN_VOLTAGE(pdo);
} else {
*max_mv = PDO_BATT_MAX_VOLTAGE(pdo);
*min_mv = PDO_BATT_MIN_VOLTAGE(pdo);
}
if (*max_mv == 0) {
*ma = 0;
*min_mv = 0;
return;
}
if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_FIXED) {
max_ma = PDO_FIXED_CURRENT(pdo);
} else if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_AUGMENTED) {
max_ma = PDO_AUG_MAX_CURRENT(pdo);
} else if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_VARIABLE) {
max_ma = PDO_VAR_MAX_CURRENT(pdo);
} else {
mw = PDO_BATT_MAX_POWER(pdo);
max_ma = 1000 * mw / PROCESS_ZERO_DIVISOR(*min_mv);
}
max_ma = MIN(max_ma,
PD_MAX_POWER_MW * 1000 / PROCESS_ZERO_DIVISOR(*min_mv));
*ma = MIN(max_ma, PD_MAX_CURRENT_MA);
}
void pd_build_request(int32_t vpd_vdo, uint32_t *rdo, uint32_t *ma,
uint32_t *mv, int port)
{
uint32_t pdo;
int pdo_index, flags = 0;
int uw;
int max_or_min_ma;
int max_or_min_mw;
int max_vbus;
int vpd_vbus_dcr;
int vpd_gnd_dcr;
uint32_t src_cap_cnt = pd_get_src_cap_cnt(port);
const uint32_t *const src_caps = pd_get_src_caps(port);
int charging_allowed;
int max_request_allowed;
uint32_t max_request_mv = pd_get_max_voltage();
uint32_t unused;
/*
* If this port is the current charge port, or if there isn't an active
* charge port, set this value to true. If CHARGE_PORT_NONE isn't
* considered, then there can be a race condition in PD negotiation and
* the charge manager which forces an incorrect request for
* vSafe5V. This can then lead to a brownout condition when the input
* current limit gets incorrectly set to 0.5A.
*/
if (IS_ENABLED(CONFIG_CHARGE_MANAGER)) {
int chg_port = charge_manager_get_selected_charge_port();
charging_allowed =
(chg_port == port || chg_port == CHARGE_PORT_NONE);
} else {
charging_allowed = 1;
}
if (IS_ENABLED(CONFIG_USB_PD_CHECK_MAX_REQUEST_ALLOWED))
max_request_allowed = pd_is_max_request_allowed();
else
max_request_allowed = 1;
if (IS_ENABLED(CONFIG_USB_PD_DPS) && dps_is_enabled())
max_request_mv = MIN(max_request_mv, dps_get_dynamic_voltage());
/*
* If currently charging on a different port, or we are not allowed to
* request the max voltage, then select vSafe5V
*/
if (charging_allowed && max_request_allowed) {
/* find pdo index for max voltage we can request */
pdo_index = pd_find_pdo_index(src_cap_cnt, src_caps,
max_request_mv, &pdo);
} else {
/* src cap 0 should be vSafe5V */
pdo_index = 0;
pdo = src_caps[0];
}
pd_extract_pdo_power(pdo, ma, mv, &unused);
/*
* Adjust VBUS current if CTVPD device was detected.
*/
if (vpd_vdo > 0) {
max_vbus = VPD_VDO_MAX_VBUS(vpd_vdo);
vpd_vbus_dcr = VPD_VDO_VBUS_IMP(vpd_vdo) << 1;
vpd_gnd_dcr = VPD_VDO_GND_IMP(vpd_vdo);
/*
* Valid max_vbus values:
* 00b - 20000 mV
* 01b - 30000 mV
* 10b - 40000 mV
* 11b - 50000 mV
*/
max_vbus = 20000 + max_vbus * 10000;
if (*mv > max_vbus)
*mv = max_vbus;
/*
* 5000 mA cable: 150 = 750000 / 50000
* 3000 mA cable: 250 = 750000 / 30000
*/
if (*ma > 3000)
*ma = 750000 / (150 + vpd_vbus_dcr + vpd_gnd_dcr);
else
*ma = 750000 / (250 + vpd_vbus_dcr + vpd_gnd_dcr);
}
uw = *ma * *mv;
/* Mismatch bit set if less power offered than the operating power */
if (uw < (1000 * PD_OPERATING_POWER_MW))
flags |= RDO_CAP_MISMATCH;
/* b:271612382S has more details. */
if (IS_ENABLED(CONFIG_USB_PD_EPR))
flags |= RDO_EPR_MODE_CAPABLE;
#ifdef CONFIG_USB_PD_GIVE_BACK
/* Tell source we are give back capable. */
flags |= RDO_GIVE_BACK;
/*
* BATTERY PDO: Inform the source that the sink will reduce
* power to this minimum level on receipt of a GotoMin Request.
*/
max_or_min_mw = PD_MIN_POWER_MW;
/*
* FIXED or VARIABLE PDO: Inform the source that the sink will
* reduce current to this minimum level on receipt of a GotoMin
* Request.
*/
max_or_min_ma = PD_MIN_CURRENT_MA;
#else
/*
* Can't give back, so set maximum current and power to
* operating level.
*/
max_or_min_ma = *ma;
max_or_min_mw = uw / 1000;
#endif
if ((pdo & PDO_TYPE_MASK) == PDO_TYPE_BATTERY) {
int mw = uw / 1000;
*rdo = RDO_BATT(pdo_index + 1, mw, max_or_min_mw, flags);
} else {
*rdo = RDO_FIXED(pdo_index + 1, *ma, max_or_min_ma, flags);
}
/*
* Ref: USB Power Delivery Specification
* (Revision 3.0, Version 2.0 / Revision 2.0, Version 1.3)
* 6.4.2.4 USB Communications Capable
* 6.4.2.5 No USB Suspend
*
* If the port is capable of USB communication, set the USB
* Communications Capable flag.
* If the port partner is sink device do not suspend USB as the
* power can be used for charging.
*/
if (pd_get_usb_comm_capable(port)) {
*rdo |= RDO_COMM_CAP;
if (pd_get_power_role(port) == PD_ROLE_SINK)
*rdo |= RDO_NO_SUSPEND;
}
}
void pd_process_source_cap(int port, int cnt, uint32_t *src_caps)
{
pd_set_src_caps(port, cnt, src_caps);
if (IS_ENABLED(CONFIG_CHARGE_MANAGER)) {
uint32_t ma, mv, pdo, unused;
uint32_t max_mv = pd_get_max_voltage();
if (IS_ENABLED(CONFIG_USB_PD_DPS) && dps_is_enabled())
max_mv = MIN(max_mv, dps_get_dynamic_voltage());
/* Get max power info that we could request */
pd_find_pdo_index(pd_get_src_cap_cnt(port),
pd_get_src_caps(port), max_mv, &pdo);
pd_extract_pdo_power(pdo, &ma, &mv, &unused);
/* Set max. limit, but 2.5 W ceiling will be applied later. */
pd_set_input_current_limit(port, ma, mv);
}
}
#endif /* defined(PD_MAX_VOLTAGE_MV) && defined(PD_OPERATING_POWER_MW) */
bool pd_is_battery_capable(void)
{
bool capable;
/* Battery is present and at some minimum percentage. */
capable = (usb_get_battery_soc() >= CONFIG_USB_PD_TRY_SRC_MIN_BATT_SOC);
#ifdef CONFIG_BATTERY_REVIVE_DISCONNECT
/*
* Not capable if the battery is in the disconnect state. The discharge
* FET may not be enabled and so attempting being a SRC may cut off
* our only power source at the time.
*/
capable &= (battery_get_disconnect_state() == BATTERY_NOT_DISCONNECTED);
#elif defined(CONFIG_BATTERY_PRESENT_CUSTOM) || \
defined(CONFIG_BATTERY_PRESENT_GPIO)
/*
* When battery is cutoff in ship mode it may not be reliable to
* check if battery is present with its state of charge.
* Also check if battery is initialized and ready to provide power.
*/
capable &= (battery_is_present() == BP_YES);
#endif /* CONFIG_BATTERY_PRESENT_[CUSTOM|GPIO] */
return capable;
}
#ifdef CONFIG_USB_PD_TRY_SRC
bool pd_is_try_source_capable(void)
{
int i;
uint8_t try_src = 0;
bool new_try_src;
for (i = 0; i < board_get_usb_pd_port_count(); i++)
try_src |= (pd_get_dual_role(i) == PD_DRP_TOGGLE_ON);
/*
* Enable try source when dual-role toggling AND battery is capable
* of powering the whole system.
*/
new_try_src = (try_src && pd_is_battery_capable());
#if CONFIG_DEDICATED_CHARGE_PORT_COUNT > 0
/*
* If a dedicated supplier is present, power is not a concern and
* therefore allow Try.Src if we're toggling.
*/
new_try_src = try_src && (charge_manager_get_supplier() ==
CHARGE_SUPPLIER_DEDICATED);
#endif /* CONFIG_DEDICATED_CHARGE_PORT_COUNT */
return new_try_src;
}
#endif /* CONFIG_USB_PD_TRY_SRC */
static int get_bbram_idx(uint8_t port)
{
if (port < MAX_SYSTEM_BBRAM_IDX_PD_PORTS)
return (port + SYSTEM_BBRAM_IDX_PD0);
return -1;
}
int pd_get_saved_port_flags(int port, uint8_t *flags)
{
if (system_get_bbram(get_bbram_idx(port), flags) != EC_SUCCESS) {
#ifndef CHIP_HOST
ccprintf("PD NVRAM FAIL");
#endif
return EC_ERROR_UNKNOWN;
}
return EC_SUCCESS;
}
static void pd_set_saved_port_flags(int port, uint8_t flags)
{
if (system_set_bbram(get_bbram_idx(port), flags) != EC_SUCCESS) {
#ifndef CHIP_HOST
ccprintf("PD NVRAM FAIL");
#endif
}
}
void pd_update_saved_port_flags(int port, uint8_t flag, uint8_t do_set)
{
uint8_t saved_flags;
if (pd_get_saved_port_flags(port, &saved_flags) != EC_SUCCESS)
return;
if (do_set)
saved_flags |= flag;
else
saved_flags &= ~flag;
pd_set_saved_port_flags(port, saved_flags);
}