chrome-ec/board/samus/extpower.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");