442 lines
12 KiB
C
442 lines
12 KiB
C
/* Copyright 2013 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.
|
|
*/
|
|
|
|
/*
|
|
* Pure GPIO-based external power detection, buffered to PCH.
|
|
* Drive high in S5-S0 when AC_PRESENT is high, otherwise drive low.
|
|
*/
|
|
|
|
#include "bq24773.h"
|
|
#include "charge_state.h"
|
|
#include "charger.h"
|
|
#include "chipset.h"
|
|
#include "common.h"
|
|
#include "console.h"
|
|
#include "extpower.h"
|
|
#include "gpio.h"
|
|
#include "hooks.h"
|
|
#include "host_command.h"
|
|
#include "i2c.h"
|
|
#include "system.h"
|
|
#include "task.h"
|
|
#include "util.h"
|
|
|
|
/* Console output macros */
|
|
#define CPRINTS(format, args...) cprints(CC_USBCHARGE, format, ## args)
|
|
|
|
/* Max number of attempts to enable/disable NVDC charger */
|
|
#define CHARGER_MODE_ATTEMPTS 3
|
|
|
|
/* Backboost has been detected */
|
|
static int bkboost_detected;
|
|
|
|
/* Charging is disabled */
|
|
static int charge_is_disabled;
|
|
|
|
/* Extpower task has been initialized */
|
|
static int extpower_task_initialized;
|
|
|
|
/*
|
|
* Charge circuit occasionally gets wedged and doesn't charge.
|
|
* This variable keeps track of the state of the circuit.
|
|
*/
|
|
static enum {
|
|
CHARGE_CIRCUIT_OK,
|
|
CHARGE_CIRCUIT_WEDGED,
|
|
} charge_circuit_state = CHARGE_CIRCUIT_OK;
|
|
|
|
int extpower_is_present(void)
|
|
{
|
|
return gpio_get_level(GPIO_AC_PRESENT);
|
|
}
|
|
|
|
static void extpower_buffer_to_pch(void)
|
|
{
|
|
if (chipset_in_state(CHIPSET_STATE_HARD_OFF)) {
|
|
/* Drive low in G3 state */
|
|
gpio_set_level(GPIO_PCH_ACOK, 0);
|
|
} else {
|
|
/* Buffer from extpower in S5+ (where 3.3DSW enabled) */
|
|
gpio_set_level(GPIO_PCH_ACOK, extpower_is_present());
|
|
}
|
|
}
|
|
DECLARE_HOOK(HOOK_CHIPSET_PRE_INIT, extpower_buffer_to_pch, HOOK_PRIO_DEFAULT);
|
|
|
|
static void extpower_shutdown(void)
|
|
{
|
|
/* Drive ACOK buffer to PCH low when shutting down */
|
|
gpio_set_level(GPIO_PCH_ACOK, 0);
|
|
}
|
|
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, extpower_shutdown, HOOK_PRIO_DEFAULT);
|
|
|
|
void extpower_interrupt(enum gpio_signal signal)
|
|
{
|
|
/* Trigger notification of external power change */
|
|
extpower_buffer_to_pch();
|
|
|
|
/* Wake extpower task only if task has been initialized */
|
|
if (extpower_task_initialized)
|
|
task_wake(TASK_ID_EXTPOWER);
|
|
}
|
|
|
|
static void extpower_init(void)
|
|
{
|
|
extpower_buffer_to_pch();
|
|
|
|
/* Enable interrupts, now that we've initialized */
|
|
gpio_enable_interrupt(GPIO_AC_PRESENT);
|
|
}
|
|
DECLARE_HOOK(HOOK_INIT, extpower_init, HOOK_PRIO_DEFAULT);
|
|
|
|
/*
|
|
* Save power in S3/S5/G3 by disabling charging when the battery is
|
|
* full. Restore charging when battery is not full anymore. This saves
|
|
* power because our input AC path is inefficient.
|
|
*/
|
|
|
|
static void check_charging_cutoff(void)
|
|
{
|
|
/* If battery is full disable charging */
|
|
if (charge_get_percent() == 100) {
|
|
charge_is_disabled = 1;
|
|
host_command_pd_send_status(PD_CHARGE_NONE);
|
|
}
|
|
}
|
|
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, check_charging_cutoff, HOOK_PRIO_DEFAULT);
|
|
|
|
static void cancel_charging_cutoff(void)
|
|
{
|
|
/* If charging is disabled, enable it */
|
|
if (charge_is_disabled) {
|
|
charge_is_disabled = 0;
|
|
host_command_pd_send_status(PD_CHARGE_5V);
|
|
}
|
|
}
|
|
DECLARE_HOOK(HOOK_CHIPSET_RESUME, cancel_charging_cutoff, HOOK_PRIO_DEFAULT);
|
|
|
|
static void batt_soc_change(void)
|
|
{
|
|
/* If in S0, leave charging alone */
|
|
if (chipset_in_state(CHIPSET_STATE_ON)) {
|
|
host_command_pd_send_status(PD_CHARGE_NO_CHANGE);
|
|
return;
|
|
}
|
|
|
|
/* Check to disable or enable charging based on batt state of charge */
|
|
if (!charge_is_disabled && charge_get_percent() == 100) {
|
|
host_command_pd_send_status(PD_CHARGE_NONE);
|
|
charge_is_disabled = 1;
|
|
} else if (charge_is_disabled && charge_get_percent() < 100) {
|
|
charge_is_disabled = 0;
|
|
host_command_pd_send_status(PD_CHARGE_5V);
|
|
} else {
|
|
/* Leave charging alone, but update battery SOC */
|
|
host_command_pd_send_status(PD_CHARGE_NO_CHANGE);
|
|
}
|
|
}
|
|
DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE, batt_soc_change, HOOK_PRIO_DEFAULT);
|
|
|
|
/**
|
|
* Enable/disable NVDC charger to control AC to system and battery.
|
|
*/
|
|
static void charger_disable(int disable)
|
|
{
|
|
int i, rv;
|
|
|
|
for (i = 0; i < CHARGER_MODE_ATTEMPTS; i++) {
|
|
rv = charger_discharge_on_ac(disable);
|
|
if (rv == EC_SUCCESS)
|
|
return;
|
|
}
|
|
|
|
CPRINTS("Setting learn mode %d failed!", disable);
|
|
}
|
|
|
|
static void allow_max_request(void)
|
|
{
|
|
int prochot_status;
|
|
if (charge_circuit_state == CHARGE_CIRCUIT_WEDGED) {
|
|
/* Read PROCHOT status register to clear it */
|
|
i2c_read8(I2C_PORT_CHARGER, BQ24773_ADDR_FLAGS,
|
|
BQ24773_PROCHOT_STATUS, &prochot_status);
|
|
charge_circuit_state = CHARGE_CIRCUIT_OK;
|
|
}
|
|
host_command_pd_send_status(PD_CHARGE_MAX);
|
|
}
|
|
DECLARE_DEFERRED(allow_max_request);
|
|
|
|
static void allow_min_charging(void)
|
|
{
|
|
if (!charge_is_disabled && charge_circuit_state == CHARGE_CIRCUIT_OK)
|
|
host_command_pd_send_status(PD_CHARGE_5V);
|
|
}
|
|
DECLARE_DEFERRED(allow_min_charging);
|
|
|
|
static void extpower_board_hacks(int extpower, int extpower_prev)
|
|
{
|
|
/* Cancel deferred attempt to enable max charge request */
|
|
hook_call_deferred(&allow_max_request_data, -1);
|
|
|
|
/*
|
|
* When AC is detected, delay briefly before allowing PD
|
|
* to negotiate up to the max voltage to give charge circuit
|
|
* time to settle down. When AC goes away, disable charging
|
|
* for a brief time, allowing charge state machine time to
|
|
* see AC has gone away, and then set PD to only allow
|
|
* 5V charging for the next time AC is connected.
|
|
*
|
|
* Use NVDC charger learn mode (charger_disable()) when AC
|
|
* is not present to avoid backboosting when AC is plugged in.
|
|
*
|
|
* When in G3, PP5000 needs to be enabled to accurately sense
|
|
* CC voltage when AC is attached. When AC is disconnceted
|
|
* it needs to be off to save power.
|
|
*/
|
|
if (extpower && !extpower_prev) {
|
|
/* AC connected */
|
|
charger_disable(0);
|
|
hook_call_deferred(&allow_max_request_data, 500*MSEC);
|
|
set_pp5000_in_g3(PP5000_IN_G3_AC, 1);
|
|
} else if (extpower && extpower_prev) {
|
|
/*
|
|
* Glitch on AC_PRESENT, attempt to recover from
|
|
* backboost
|
|
*/
|
|
host_command_pd_send_status(PD_CHARGE_NONE);
|
|
} else {
|
|
/* AC disconnected */
|
|
if (!charge_is_disabled &&
|
|
charge_circuit_state == CHARGE_CIRCUIT_OK)
|
|
host_command_pd_send_status(PD_CHARGE_NONE);
|
|
|
|
charger_disable(1);
|
|
|
|
hook_call_deferred(&allow_min_charging_data, 100*MSEC);
|
|
set_pp5000_in_g3(PP5000_IN_G3_AC, 0);
|
|
}
|
|
extpower_prev = extpower;
|
|
}
|
|
|
|
/* Return boostin_voltage or negative if error */
|
|
static int get_boostin_voltage(void)
|
|
{
|
|
/* Static structs to save stack space */
|
|
static struct ec_response_usb_pd_power_info pd_power_ret;
|
|
static struct ec_params_usb_pd_power_info pd_power_args;
|
|
int ret;
|
|
int err;
|
|
|
|
/* Boost-in voltage is maximum of voltage now on each port */
|
|
pd_power_args.port = 0;
|
|
err = pd_host_command(EC_CMD_USB_PD_POWER_INFO, 0,
|
|
&pd_power_args,
|
|
sizeof(struct ec_params_usb_pd_power_info),
|
|
&pd_power_ret,
|
|
sizeof(struct ec_response_usb_pd_power_info));
|
|
if (err < 0)
|
|
return err;
|
|
ret = pd_power_ret.meas.voltage_now;
|
|
|
|
pd_power_args.port = 1;
|
|
err = pd_host_command(EC_CMD_USB_PD_POWER_INFO, 0,
|
|
&pd_power_args,
|
|
sizeof(struct ec_params_usb_pd_power_info),
|
|
&pd_power_ret,
|
|
sizeof(struct ec_response_usb_pd_power_info));
|
|
if (err < 0)
|
|
return err;
|
|
|
|
/* Get max of two measuremente */
|
|
if (pd_power_ret.meas.voltage_now > ret)
|
|
ret = pd_power_ret.meas.voltage_now;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Send command to PD to write a custom persistent log entry indicating that
|
|
* charging was wedged. Returns pd_host_command success status.
|
|
*/
|
|
static int log_charge_wedged(void)
|
|
{
|
|
static struct ec_params_pd_write_log_entry log_args;
|
|
|
|
log_args.type = PD_EVENT_MCU_BOARD_CUSTOM;
|
|
log_args.port = 0;
|
|
|
|
return pd_host_command(EC_CMD_PD_WRITE_LOG_ENTRY, 0,
|
|
&log_args,
|
|
sizeof(struct ec_params_pd_write_log_entry),
|
|
NULL, 0);
|
|
}
|
|
|
|
/* Time interval between checking if charge circuit is wedged */
|
|
#define CHARGE_WEDGE_CHECK_INTERVAL (2*SECOND)
|
|
|
|
/*
|
|
* Number of iterations through check_charge_wedged() with charging stalled
|
|
* before attempting unwedge.
|
|
*/
|
|
#define CHARGE_STALLED_COUNT 5
|
|
/*
|
|
* Number of iterations through check_charge_wedged() with charging stalled
|
|
* after we already just tried unwedging the circuit, before we try again.
|
|
*/
|
|
#define CHARGE_STALLED_REPEATEDLY_COUNT 60
|
|
|
|
/*
|
|
* Minimum number of iterations through check_charge_wedged() between
|
|
* unwedge attempts.
|
|
*/
|
|
#define MIN_COUNTS_BETWEEN_UNWEDGES 3
|
|
|
|
static void check_charge_wedged(void)
|
|
{
|
|
int rv, prochot_status, batt_discharging_on_ac, boostin_voltage = 0;
|
|
static int counts_since_wedged;
|
|
static int charge_stalled_count = CHARGE_STALLED_COUNT;
|
|
uint8_t *batt_flags = host_get_memmap(EC_MEMMAP_BATT_FLAG);
|
|
|
|
if (charge_circuit_state == CHARGE_CIRCUIT_OK) {
|
|
/* Check PROCHOT warning */
|
|
rv = i2c_read8(I2C_PORT_CHARGER, BQ24773_ADDR_FLAGS,
|
|
BQ24773_PROCHOT_STATUS, &prochot_status);
|
|
if (rv)
|
|
prochot_status = 0;
|
|
|
|
batt_discharging_on_ac =
|
|
(*batt_flags & EC_BATT_FLAG_AC_PRESENT) &&
|
|
(*batt_flags & EC_BATT_FLAG_DISCHARGING);
|
|
|
|
/*
|
|
* If PROCHOT is set or we are discharging on AC, then we
|
|
* need to know boostin_voltage.
|
|
*/
|
|
if (prochot_status || batt_discharging_on_ac)
|
|
boostin_voltage = get_boostin_voltage();
|
|
|
|
/*
|
|
* If AC is present, and battery is discharging, and
|
|
* boostin voltage is above 5V, then we might be wedged.
|
|
*/
|
|
if (batt_discharging_on_ac) {
|
|
if (boostin_voltage > 6000)
|
|
charge_stalled_count--;
|
|
else if (boostin_voltage >= 0)
|
|
charge_stalled_count = CHARGE_STALLED_COUNT;
|
|
/* If boostin_voltage < 0, don't change stalled count */
|
|
} else {
|
|
charge_stalled_count = CHARGE_STALLED_COUNT;
|
|
}
|
|
|
|
/*
|
|
* If we were recently wedged, then give ourselves a free pass
|
|
* here. This gives an opportunity for reading the PROCHOT
|
|
* status to clear it if the error has gone away.
|
|
*/
|
|
if (counts_since_wedged < MIN_COUNTS_BETWEEN_UNWEDGES)
|
|
counts_since_wedged++;
|
|
|
|
/*
|
|
* If PROCHOT is asserted AND boost_in voltage is above 5V,
|
|
* then charge circuit is wedged. If charging has been stalled
|
|
* long enough, then also consider the circuit wedged.
|
|
*
|
|
* To unwedge the charge circuit turn on learn mode and notify
|
|
* PD to disable charging on all ports.
|
|
* Note: learn mode is critical here because when in this state
|
|
* backboosting causes >20V on boostin even after PD disables
|
|
* CHARGE_EN lines.
|
|
*/
|
|
if ((prochot_status && boostin_voltage > 6000 &&
|
|
counts_since_wedged >= MIN_COUNTS_BETWEEN_UNWEDGES) ||
|
|
charge_stalled_count <= 0) {
|
|
counts_since_wedged = 0;
|
|
host_command_pd_send_status(PD_CHARGE_NONE);
|
|
charger_disable(1);
|
|
charge_circuit_state = CHARGE_CIRCUIT_WEDGED;
|
|
log_charge_wedged();
|
|
CPRINTS("Charge wedged! PROCHOT %02x, Stalled: %d",
|
|
prochot_status, charge_stalled_count);
|
|
|
|
/*
|
|
* If this doesn't clear the problem, then start
|
|
* the stall counter higher so that we don't retry
|
|
* unwedging for a while. Note, if we do start charging
|
|
* properly, then stall counter will be set to
|
|
* default, so that we will trigger faster the first
|
|
* time it stalls out.
|
|
*/
|
|
charge_stalled_count = CHARGE_STALLED_REPEATEDLY_COUNT;
|
|
}
|
|
} else {
|
|
/*
|
|
* Charge circuit is wedged and we already disabled charging,
|
|
* Now start to recover from wedged state by allowing 5V.
|
|
*/
|
|
host_command_pd_send_status(PD_CHARGE_5V);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Task to handle external power change
|
|
*/
|
|
void extpower_task(void)
|
|
{
|
|
int extpower = extpower_is_present();
|
|
int extpower_prev = 0;
|
|
|
|
extpower_board_hacks(extpower, extpower_prev);
|
|
extpower_prev = extpower;
|
|
extpower_task_initialized = 1;
|
|
|
|
/* Enable backboost detection interrupt */
|
|
gpio_enable_interrupt(GPIO_BKBOOST_DET);
|
|
|
|
while (1) {
|
|
if (task_wait_event(CHARGE_WEDGE_CHECK_INTERVAL) ==
|
|
TASK_EVENT_TIMER) {
|
|
/*
|
|
* If we are NOT purposely discharging on AC, then
|
|
* periodically check if charge circuit is wedged.
|
|
*/
|
|
if (!board_is_discharging_on_ac())
|
|
check_charge_wedged();
|
|
} else {
|
|
/* Must have received power change interrupt */
|
|
extpower = extpower_is_present();
|
|
|
|
/* Various board hacks to run on extpower change */
|
|
extpower_board_hacks(extpower, extpower_prev);
|
|
extpower_prev = extpower;
|
|
|
|
hook_notify(HOOK_AC_CHANGE);
|
|
|
|
/* Forward notification to host */
|
|
if (extpower)
|
|
host_set_single_event(
|
|
EC_HOST_EVENT_AC_CONNECTED);
|
|
else
|
|
host_set_single_event(
|
|
EC_HOST_EVENT_AC_DISCONNECTED);
|
|
}
|
|
}
|
|
}
|
|
|
|
void bkboost_det_interrupt(enum gpio_signal signal)
|
|
{
|
|
/* Backboost has been detected, save it, and disable interrupt */
|
|
bkboost_detected = 1;
|
|
gpio_disable_interrupt(GPIO_BKBOOST_DET);
|
|
}
|
|
|
|
static int command_backboost_det(int argc, char **argv)
|
|
{
|
|
ccprintf("Backboost detected: %d\n", bkboost_detected);
|
|
return EC_SUCCESS;
|
|
}
|
|
DECLARE_CONSOLE_COMMAND(bkboost, command_backboost_det, NULL,
|
|
"Read backboost detection");
|