blob: 5c02f592533105477216e6c0d9fa8f60621f7ddb [file] [log] [blame]
/* Copyright (c) 2012 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.
*
* Battery charging task and state machine.
*/
#include "battery.h"
#include "battery_pack.h"
#include "charge_state.h"
#include "charger.h"
#include "chipset.h"
#include "common.h"
#include "console.h"
#include "gpio.h"
#include "host_command.h"
#include "power_button.h"
#include "power_led.h"
#include "printf.h"
#include "smart_battery.h"
#include "system.h"
#include "timer.h"
#include "util.h"
#include "x86_power.h"
/* Console output macros */
#define CPUTS(outstr) cputs(CC_CHARGER, outstr)
#define CPRINTF(format, args...) cprintf(CC_CHARGER, format, ## args)
/* Voltage debounce time */
#define DEBOUNCE_TIME (10 * SECOND)
/* Time period between setting power LED */
#define SET_LED_PERIOD (10 * SECOND)
static const char * const state_name[] = POWER_STATE_NAME_TABLE;
static int state_machine_force_idle = 0;
/* Current power state context */
static struct power_state_context task_ctx;
static inline int is_charger_expired(
struct power_state_context *ctx, timestamp_t now)
{
return now.val - ctx->charger_update_time.val > CHARGER_UPDATE_PERIOD;
}
static inline void update_charger_time(
struct power_state_context *ctx, timestamp_t now)
{
ctx->charger_update_time.val = now.val;
}
/* Battery information used to fill ACPI _BIF and/or _BIX */
static void update_battery_info(void)
{
char *batt_str;
int batt_serial;
/* Design Capacity of Full */
battery_design_capacity((int *)host_get_memmap(EC_MEMMAP_BATT_DCAP));
/* Design Voltage */
battery_design_voltage((int *)host_get_memmap(EC_MEMMAP_BATT_DVLT));
/* Last Full Charge Capacity */
battery_full_charge_capacity(
(int *)host_get_memmap(EC_MEMMAP_BATT_LFCC));
/* Cycle Count */
battery_cycle_count((int *)host_get_memmap(EC_MEMMAP_BATT_CCNT));
/* Battery Manufacturer string */
batt_str = (char *)host_get_memmap(EC_MEMMAP_BATT_MFGR);
memset(batt_str, 0, EC_MEMMAP_TEXT_MAX);
battery_manufacturer_name(batt_str, EC_MEMMAP_TEXT_MAX);
/* Battery Model string */
batt_str = (char *)host_get_memmap(EC_MEMMAP_BATT_MODEL);
memset(batt_str, 0, EC_MEMMAP_TEXT_MAX);
battery_device_name(batt_str, EC_MEMMAP_TEXT_MAX);
/* Battery Type string */
batt_str = (char *)host_get_memmap(EC_MEMMAP_BATT_TYPE);
battery_device_chemistry(batt_str, EC_MEMMAP_TEXT_MAX);
/* Smart battery serial number is 16 bits */
batt_str = (char *)host_get_memmap(EC_MEMMAP_BATT_SERIAL);
memset(batt_str, 0, EC_MEMMAP_TEXT_MAX);
if (battery_serial_number(&batt_serial) == 0)
snprintf(batt_str, EC_MEMMAP_TEXT_MAX, "%04X", batt_serial);
/* Battery data is now present */
*host_get_memmap(EC_MEMMAP_BATTERY_VERSION) = 1;
}
/* Prevent battery from going into deep discharge state */
static void poweroff_wait_ac(void)
{
/* Shutdown the main processor */
if (chipset_in_state(CHIPSET_STATE_ON)) {
/* chipset_force_state(CHIPSET_STATE_SOFT_OFF);
* TODO(rong): remove platform dependent code
*/
#ifdef CONFIG_TASK_X86POWER
CPRINTF("[%T force shutdown to avoid damaging battery]\n");
x86_power_force_shutdown();
host_set_single_event(EC_HOST_EVENT_BATTERY_SHUTDOWN);
#endif /* CONFIG_TASK_X86POWER */
}
}
/* Common handler for charging states.
* This handler gets battery charging parameters, charger state, ac state,
* and timestamp. It also fills memory map and issues power events on state
* change.
*/
static int state_common(struct power_state_context *ctx)
{
int rv, d;
struct power_state_data *curr = &ctx->curr;
struct power_state_data *prev = &ctx->prev;
struct batt_params *batt = &ctx->curr.batt;
uint8_t *batt_flags = ctx->memmap_batt_flags;
/* Copy previous state and init new state */
ctx->prev = ctx->curr;
curr->ts = get_time();
curr->error = 0;
/* Detect AC change */
curr->ac = power_ac_present();
if (curr->ac != prev->ac) {
if (curr->ac) {
/* AC on
* Initialize charger to power on reset mode
*/
rv = charger_post_init();
if (rv)
curr->error |= F_CHARGER_INIT;
host_set_single_event(EC_HOST_EVENT_AC_CONNECTED);
} else {
/* AC off */
host_set_single_event(EC_HOST_EVENT_AC_DISCONNECTED);
}
}
if (curr->ac) {
*batt_flags |= EC_BATT_FLAG_AC_PRESENT;
rv = charger_get_voltage(&curr->charging_voltage);
if (rv) {
charger_set_voltage(0);
charger_set_current(0);
curr->error |= F_CHARGER_VOLTAGE;
}
rv = charger_get_current(&curr->charging_current);
if (rv) {
charger_set_voltage(0);
charger_set_current(0);
curr->error |= F_CHARGER_CURRENT;
}
} else {
*batt_flags &= ~EC_BATT_FLAG_AC_PRESENT;
/* AC disconnected should get us out of force idle mode. */
state_machine_force_idle = 0;
}
rv = battery_temperature(&batt->temperature);
if (rv) {
/* Check low battery condition and retry */
if (curr->ac && !(curr->error & F_CHARGER_MASK) &&
(curr->charging_voltage == 0 ||
curr->charging_current == 0)) {
charger_set_voltage(ctx->battery->voltage_min);
charger_set_current(ctx->charger->current_min);
usleep(SECOND);
rv = battery_temperature(&batt->temperature);
}
}
if (rv)
curr->error |= F_BATTERY_TEMPERATURE;
rv = battery_voltage(&batt->voltage);
if (rv)
curr->error |= F_BATTERY_VOLTAGE;
*ctx->memmap_batt_volt = batt->voltage;
rv = battery_current(&batt->current);
if (rv)
curr->error |= F_BATTERY_CURRENT;
/* Memory mapped value: discharge rate */
*ctx->memmap_batt_rate = batt->current < 0 ?
-batt->current : batt->current;
rv = battery_desired_voltage(&batt->desired_voltage);
if (rv)
curr->error |= F_DESIRED_VOLTAGE;
rv = battery_desired_current(&batt->desired_current);
if (rv)
curr->error |= F_DESIRED_CURRENT;
rv = battery_state_of_charge(&batt->state_of_charge);
if (rv)
curr->error |= F_BATTERY_STATE_OF_CHARGE;
if (batt->state_of_charge != prev->batt.state_of_charge) {
rv = battery_full_charge_capacity(&d);
if (!rv && d != *(int*)host_get_memmap(EC_MEMMAP_BATT_LFCC)) {
*(int*)host_get_memmap(EC_MEMMAP_BATT_LFCC) = d;
/* Notify host to re-read battery information */
host_set_single_event(EC_HOST_EVENT_BATTERY);
}
}
/* Prevent deep discharging */
if (!curr->ac)
if ((batt->state_of_charge < BATTERY_LEVEL_SHUTDOWN &&
!(curr->error & F_BATTERY_STATE_OF_CHARGE)) ||
(batt->voltage <= ctx->battery->voltage_min &&
!(curr->error & F_BATTERY_VOLTAGE)))
poweroff_wait_ac();
/* Check battery presence */
if (curr->error & F_BATTERY_MASK) {
*ctx->memmap_batt_flags &= ~EC_BATT_FLAG_BATT_PRESENT;
return curr->error;
}
*ctx->memmap_batt_flags |= EC_BATT_FLAG_BATT_PRESENT;
/* Battery charge level low */
if (batt->state_of_charge <= BATTERY_LEVEL_LOW &&
prev->batt.state_of_charge > BATTERY_LEVEL_LOW)
host_set_single_event(EC_HOST_EVENT_BATTERY_LOW);
/* Battery charge level critical */
if (batt->state_of_charge <= BATTERY_LEVEL_CRITICAL) {
*ctx->memmap_batt_flags |= EC_BATT_FLAG_LEVEL_CRITICAL;
/* Send battery critical host event */
if (prev->batt.state_of_charge > BATTERY_LEVEL_CRITICAL)
host_set_single_event(EC_HOST_EVENT_BATTERY_CRITICAL);
} else
*ctx->memmap_batt_flags &= ~EC_BATT_FLAG_LEVEL_CRITICAL;
/* Apply battery pack vendor charging method */
battery_vendor_params(batt);
#ifdef CONFIG_CHARGING_CURRENT_LIMIT
if (batt->desired_current > CONFIG_CHARGING_CURRENT_LIMIT)
batt->desired_current = CONFIG_CHARGING_CURRENT_LIMIT;
#endif
rv = battery_get_battery_mode(&d);
if (rv) {
curr->error |= F_BATTERY_MODE;
} else {
if (d & MODE_CAPACITY) {
/* Battery capacity mode was set to mW
* reset it back to mAh
*/
d &= ~MODE_CAPACITY;
rv = battery_set_battery_mode(d);
if (rv)
ctx->curr.error |= F_BATTERY_MODE;
}
}
rv = battery_remaining_capacity(&d);
if (rv)
ctx->curr.error |= F_BATTERY_CAPACITY;
else
*ctx->memmap_batt_cap = d;
return ctx->curr.error;
}
/* Init state handler
* - check ac, charger, battery and temperature
* - initialize charger
* - new states: DISCHARGE, IDLE
*/
static enum power_state state_init(struct power_state_context *ctx)
{
/* Stop charger, unconditionally */
charger_set_current(0);
charger_set_voltage(0);
/* Update static battery info */
update_battery_info();
/* If AC is not present, switch to discharging state */
if (!ctx->curr.ac)
return PWR_STATE_DISCHARGE;
/* Check general error conditions */
if (ctx->curr.error)
return PWR_STATE_ERROR;
/* Send battery event to host */
host_set_single_event(EC_HOST_EVENT_BATTERY);
return PWR_STATE_IDLE;
}
/* Idle state handler
* - both charger and battery are online
* - detect charger and battery status change
* - new states: CHARGE, INIT
*/
static enum power_state state_idle(struct power_state_context *ctx)
{
struct batt_params *batt = &ctx->curr.batt;
const struct charger_info *c_info = ctx->charger;
/* If we are forcing idle mode, then just stay in IDLE. */
if (state_machine_force_idle)
return PWR_STATE_UNCHANGE;
if (!ctx->curr.ac)
return PWR_STATE_INIT;
if (ctx->curr.error)
return PWR_STATE_ERROR;
/* Prevent charging in idle mode */
if (ctx->curr.charging_voltage ||
ctx->curr.charging_current)
return PWR_STATE_INIT;
if (ctx->curr.batt.state_of_charge >= STOP_CHARGE_THRESHOLD)
return PWR_STATE_UNCHANGE;
/* Configure init charger state and switch to charge state */
if (ctx->curr.batt.desired_voltage &&
ctx->curr.batt.desired_current) {
/* Set charger output constraints */
if (batt->desired_current < ctx->charger->current_min) {
/* Trickle charging */
if (charger_set_current(c_info->current_min) ||
charger_set_voltage(batt->voltage))
return PWR_STATE_ERROR;
ctx->trickle_charging_time = get_time();
} else {
/* Normal charging */
if (charger_set_voltage(batt->desired_voltage) ||
charger_set_current(batt->desired_current))
return PWR_STATE_ERROR;
}
update_charger_time(ctx, get_time());
return PWR_STATE_CHARGE;
}
return PWR_STATE_UNCHANGE;
}
/* Charge state handler
* - detect battery status change
* - new state: INIT
*/
static enum power_state state_charge(struct power_state_context *ctx)
{
struct power_state_data *curr = &ctx->curr;
struct batt_params *batt = &ctx->curr.batt;
const struct charger_info *c_info = ctx->charger;
int debounce = 0;
timestamp_t now;
if (curr->error)
return PWR_STATE_ERROR;
if (batt->desired_current < c_info->current_min &&
batt->desired_current > 0)
return trickle_charge(ctx);
/* Check charger reset */
if (curr->charging_voltage == 0 ||
curr->charging_current == 0)
return PWR_STATE_INIT;
if (!curr->ac)
return PWR_STATE_INIT;
if (batt->state_of_charge >= STOP_CHARGE_THRESHOLD) {
if (charger_set_voltage(0) || charger_set_current(0))
return PWR_STATE_ERROR;
return PWR_STATE_IDLE;
}
now = get_time();
if (batt->desired_voltage != curr->charging_voltage) {
if (charger_set_voltage(batt->desired_voltage))
return PWR_STATE_ERROR;
update_charger_time(ctx, now);
}
if (batt->desired_current == curr->charging_current) {
/* Tick charger watchdog */
if (!is_charger_expired(ctx, now))
return PWR_STATE_UNCHANGE;
} else if (batt->desired_current > curr->charging_current) {
if (!timestamp_expired(ctx->voltage_debounce_time, &now))
return PWR_STATE_UNCHANGE;
} else {
/* Debounce charging current on falling edge */
debounce = 1;
}
if (charger_set_current(batt->desired_current))
return PWR_STATE_ERROR;
/* Update charger watchdog timer and debounce timer */
update_charger_time(ctx, now);
if (debounce)
ctx->voltage_debounce_time.val = now.val + DEBOUNCE_TIME;
return PWR_STATE_UNCHANGE;
}
/* Discharge state handler
* - detect ac status
* - new state: INIT
*/
static enum power_state state_discharge(struct power_state_context *ctx)
{
struct batt_params *batt = &ctx->curr.batt;
if (ctx->curr.ac)
return PWR_STATE_INIT;
if (ctx->curr.error)
return PWR_STATE_ERROR;
/* Overtemp in discharging state
* - poweroff host and ec
*/
if (batt->temperature > ctx->battery->temp_discharge_max ||
batt->temperature < ctx->battery->temp_discharge_min)
poweroff_wait_ac();
return PWR_STATE_UNCHANGE;
}
/* Error state handler
* - check charger and battery communication
* - log error
* - new state: INIT
*/
static enum power_state state_error(struct power_state_context *ctx)
{
static int logged_error;
if (!ctx->curr.error) {
logged_error = 0;
return PWR_STATE_INIT;
}
/* Debug output */
if (ctx->curr.error != logged_error) {
CPRINTF("[%T Charge error: flag[%08b -> %08b], ac %d, "
" charger %s, battery %s\n",
logged_error, ctx->curr.error, ctx->curr.ac,
(ctx->curr.error & F_CHARGER_MASK) ?
"(err)" : "ok",
(ctx->curr.error & F_BATTERY_MASK) ?
"(err)" : "ok");
logged_error = ctx->curr.error;
}
return PWR_STATE_UNCHANGE;
}
static void charging_progress(struct power_state_context *ctx)
{
int seconds, minutes;
if (ctx->curr.batt.state_of_charge !=
ctx->prev.batt.state_of_charge) {
if (ctx->curr.ac)
battery_time_to_full(&minutes);
else
battery_time_to_empty(&minutes);
CPRINTF("[%T Battery %3d%% / %dh:%d]\n",
ctx->curr.batt.state_of_charge,
minutes / 60, minutes % 60);
return;
}
if (ctx->curr.charging_voltage != ctx->prev.charging_voltage &&
ctx->trickle_charging_time.val) {
/* Calculating minutes by dividing usec by 60 million
* GNU toolchain generate architecture dependent calls
* instead of machine code when the divisor is large.
* Hence following calculation was broke into 2 lines.
*/
seconds = (int)(get_time().val -
ctx->trickle_charging_time.val) / (int)SECOND;
minutes = seconds / 60;
CPRINTF("[%T Precharge CHG(%dmV) BATT(%dmV %dmA) "
"%dh:%d]\n", ctx->curr.charging_voltage,
ctx->curr.batt.voltage, ctx->curr.batt.current,
minutes / 60, minutes % 60);
}
}
enum power_state charge_get_state(void)
{
return task_ctx.curr.state;
}
int charge_get_percent(void)
{
return task_ctx.curr.batt.state_of_charge;
}
static int enter_force_idle_mode(void)
{
if (!power_ac_present())
return EC_ERROR_UNKNOWN;
state_machine_force_idle = 1;
charger_post_init();
return EC_SUCCESS;
}
static int exit_force_idle_mode(void)
{
state_machine_force_idle = 0;
return EC_SUCCESS;
}
static enum powerled_color force_idle_led_blink(void)
{
static enum powerled_color last = POWERLED_GREEN;
if (last == POWERLED_GREEN)
last = POWERLED_OFF;
else
last = POWERLED_GREEN;
return last;
}
/* Battery charging task */
void charge_state_machine_task(void)
{
struct power_state_context *ctx = &task_ctx;
timestamp_t ts;
int sleep_usec = POLL_PERIOD_SHORT, diff_usec, sleep_next;
enum power_state new_state;
uint8_t batt_flags;
enum powerled_color led_color = POWERLED_OFF;
int rv_setled = 0;
uint64_t last_setled_time = 0;
ctx->prev.state = PWR_STATE_INIT;
ctx->curr.state = PWR_STATE_INIT;
ctx->trickle_charging_time.val = 0;
ctx->battery = battery_get_info();
ctx->charger = charger_get_info();
/* Setup LPC direct memmap */
ctx->memmap_batt_volt =
(uint32_t *)host_get_memmap(EC_MEMMAP_BATT_VOLT);
ctx->memmap_batt_rate =
(uint32_t *)host_get_memmap(EC_MEMMAP_BATT_RATE);
ctx->memmap_batt_cap =
(uint32_t *)host_get_memmap(EC_MEMMAP_BATT_CAP);
ctx->memmap_batt_flags = host_get_memmap(EC_MEMMAP_BATT_FLAG);
while (1) {
state_common(ctx);
switch (ctx->prev.state) {
case PWR_STATE_INIT:
new_state = state_init(ctx);
break;
case PWR_STATE_IDLE:
new_state = state_idle(ctx);
break;
case PWR_STATE_DISCHARGE:
new_state = state_discharge(ctx);
break;
case PWR_STATE_CHARGE:
new_state = state_charge(ctx);
break;
case PWR_STATE_ERROR:
new_state = state_error(ctx);
break;
default:
CPRINTF("[%T Charge state %d undefined]\n",
ctx->curr.state);
ctx->curr.state = PWR_STATE_ERROR;
new_state = PWR_STATE_ERROR;
}
if (state_machine_force_idle &&
ctx->prev.state != PWR_STATE_IDLE &&
ctx->prev.state != PWR_STATE_INIT)
new_state = PWR_STATE_INIT;
if (new_state) {
ctx->curr.state = new_state;
CPRINTF("[%T Charge state %s -> %s]\n",
state_name[ctx->prev.state],
state_name[new_state]);
}
switch (new_state) {
case PWR_STATE_IDLE:
batt_flags = *ctx->memmap_batt_flags;
batt_flags &= ~EC_BATT_FLAG_CHARGING;
batt_flags &= ~EC_BATT_FLAG_DISCHARGING;
*ctx->memmap_batt_flags = batt_flags;
/* Charge done */
led_color = POWERLED_GREEN;
rv_setled = powerled_set(POWERLED_GREEN);
last_setled_time = get_time().val;
sleep_usec = POLL_PERIOD_LONG;
break;
case PWR_STATE_DISCHARGE:
batt_flags = *ctx->memmap_batt_flags;
batt_flags &= ~EC_BATT_FLAG_CHARGING;
batt_flags |= EC_BATT_FLAG_DISCHARGING;
*ctx->memmap_batt_flags = batt_flags;
sleep_usec = POLL_PERIOD_LONG;
break;
case PWR_STATE_CHARGE:
batt_flags = *ctx->memmap_batt_flags;
batt_flags |= EC_BATT_FLAG_CHARGING;
batt_flags &= ~EC_BATT_FLAG_DISCHARGING;
*ctx->memmap_batt_flags = batt_flags;
/* Charging */
led_color = POWERLED_YELLOW;
rv_setled = powerled_set(POWERLED_YELLOW);
last_setled_time = get_time().val;
sleep_usec = POLL_PERIOD_CHARGE;
break;
case PWR_STATE_ERROR:
/* Error */
led_color = POWERLED_RED;
rv_setled = powerled_set(POWERLED_RED);
last_setled_time = get_time().val;
sleep_usec = POLL_PERIOD_CHARGE;
break;
case PWR_STATE_UNCHANGE:
/* Don't change sleep duration */
if (state_machine_force_idle)
powerled_set(force_idle_led_blink());
else if (rv_setled || get_time().val - last_setled_time
> SET_LED_PERIOD) {
/*
* It is possible to make power LED go off
* without disconnecting AC. Therefore we
* need to reset power LED periodically.
*/
rv_setled = powerled_set(led_color);
last_setled_time = get_time().val;
}
break;
default:
/* Other state; poll quickly and hope it goes away */
sleep_usec = POLL_PERIOD_SHORT;
}
/* Show charging progress in console */
charging_progress(ctx);
ts = get_time();
diff_usec = (int)(ts.val - ctx->curr.ts.val);
sleep_next = sleep_usec - diff_usec;
if (sleep_next < MIN_SLEEP_USEC)
sleep_next = MIN_SLEEP_USEC;
if (sleep_next > MAX_SLEEP_USEC)
sleep_next = MAX_SLEEP_USEC;
usleep(sleep_next);
}
}
static int charge_command_force_idle(struct host_cmd_handler_args *args)
{
const struct ec_params_force_idle *p = args->params;
int rv;
if (system_is_locked())
return EC_RES_ACCESS_DENIED;
if (p->enabled)
rv = enter_force_idle_mode();
else
rv = exit_force_idle_mode();
if (rv != EC_SUCCESS)
return EC_RES_ERROR;
return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_CHARGE_FORCE_IDLE, charge_command_force_idle,
EC_VER_MASK(0));