/* 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));