| /* Copyright (c) 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. |
| * |
| * Battery charging task and state machine. |
| */ |
| |
| #include "battery.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 "printf.h" |
| #include "sb_fw_update.h" |
| #include "system.h" |
| #include "task.h" |
| #include "timer.h" |
| #include "util.h" |
| |
| /* Console output macros */ |
| #define CPUTS(outstr) cputs(CC_CHARGER, outstr) |
| #define CPRINTS(format, args...) cprints(CC_CHARGER, format, ## args) |
| |
| /* Voltage debounce time */ |
| #define DEBOUNCE_TIME (10 * SECOND) |
| |
| #define LOW_BATTERY_SHUTDOWN_TIMEOUT_US \ |
| (CONFIG_BATTERY_CRITICAL_SHUTDOWN_TIMEOUT * SECOND) |
| |
| #ifndef BATTERY_AP_OFF_LEVEL |
| #define BATTERY_AP_OFF_LEVEL 0 |
| #endif |
| |
| static const char * const state_name[] = CHARGE_STATE_NAME_TABLE; |
| |
| static int state_machine_force_idle; |
| |
| static unsigned user_current_limit = -1U; |
| |
| static int fake_state_of_charge = -1; |
| |
| /* Current power state context */ |
| static struct charge_state_context task_ctx; |
| |
| static inline int is_charger_expired( |
| struct charge_state_context *ctx, timestamp_t now) |
| { |
| return now.val - ctx->charger_update_time.val > CHARGER_UPDATE_PERIOD; |
| } |
| |
| static inline void update_charger_time( |
| struct charge_state_context *ctx, timestamp_t now) |
| { |
| ctx->charger_update_time.val = now.val; |
| } |
| |
| /** |
| * Update memory-mapped battery information, used by 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 low_battery_shutdown(struct charge_state_context *ctx) |
| { |
| if (chipset_in_state(CHIPSET_STATE_ANY_OFF)) { |
| /* AP is off, so shut down the EC now */ |
| CPRINTS("charge force EC hibernate due to low battery"); |
| system_hibernate(0, 0); |
| } else if (!ctx->shutdown_warning_time.val) { |
| /* Warn AP battery level is so low we'll shut down */ |
| CPRINTS("charge warn shutdown due to low battery"); |
| ctx->shutdown_warning_time = get_time(); |
| host_set_single_event(EC_HOST_EVENT_BATTERY_SHUTDOWN); |
| } else if (get_time().val > ctx->shutdown_warning_time.val + |
| LOW_BATTERY_SHUTDOWN_TIMEOUT_US) { |
| /* Timeout waiting for AP to shut down, so kill it */ |
| CPRINTS("charge force shutdown due to low battery"); |
| chipset_force_shutdown(); |
| } |
| } |
| |
| int charge_keep_power_off(void) |
| { |
| int charge; |
| |
| if (BATTERY_AP_OFF_LEVEL == 0) |
| return 0; |
| |
| if (battery_remaining_capacity(&charge)) |
| return charge_get_state() != PWR_STATE_ERROR; |
| |
| return charge <= BATTERY_AP_OFF_LEVEL; |
| } |
| |
| #ifdef CONFIG_CHARGER_EN_GPIO |
| #ifdef CONFIG_CHARGER_EN_ACTIVE_LOW |
| static void charge_set_charger_en_gpio(int level) |
| { |
| gpio_set_level(GPIO_CHARGER_EN_L, !level); |
| } |
| |
| static int charge_get_charger_en_gpio(void) |
| { |
| return !gpio_get_level(GPIO_CHARGER_EN_L); |
| } |
| #else |
| static void charge_set_charger_en_gpio(int level) |
| { |
| gpio_set_level(GPIO_CHARGER_EN, level); |
| } |
| |
| static int charge_get_charger_en_gpio(void) |
| { |
| return gpio_get_level(GPIO_CHARGER_EN); |
| } |
| #endif |
| #endif |
| |
| /** |
| * Enable or disable charging, and set requested voltage and current. If either |
| * of voltage and current is set to 0, charging is disable. |
| * |
| * @param voltage Requested voltage in mV. Set -1 to preserve current value. |
| * @param current Requested current in mA. Set -1 to preserve current value. |
| */ |
| static int charge_request(int voltage, int current) |
| { |
| int rv = EC_SUCCESS; |
| |
| if (voltage == -1 && current == -1) |
| return EC_SUCCESS; |
| |
| #ifdef CONFIG_CHARGER_EN_GPIO |
| if (voltage == 0 || current == 0) { |
| charge_set_charger_en_gpio(0); |
| return EC_SUCCESS; |
| } else { |
| charge_set_charger_en_gpio(1); |
| } |
| #endif |
| |
| if (voltage != -1) |
| rv |= charger_set_voltage(voltage); |
| if (current != -1) |
| rv |= charger_set_current(current); |
| |
| return rv; |
| } |
| |
| /** |
| * 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 charge_state_context *ctx) |
| { |
| int rv, d; |
| |
| struct charge_state_data *curr = &ctx->curr; |
| struct charge_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 = charge_get_flags() & CHARGE_FLAG_EXTERNAL_POWER; |
| 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; |
| } |
| } |
| |
| if (curr->ac) { |
| *batt_flags |= EC_BATT_FLAG_AC_PRESENT; |
| if (charger_get_voltage(&curr->charging_voltage)) { |
| charge_request(0, 0); |
| curr->error |= F_CHARGER_VOLTAGE; |
| } |
| if (charger_get_current(&curr->charging_current)) { |
| charge_request(0, 0); |
| curr->error |= F_CHARGER_CURRENT; |
| } |
| #ifdef CONFIG_CHARGER_EN_GPIO |
| if (!charge_get_charger_en_gpio()) { |
| curr->charging_voltage = 0; |
| curr->charging_current = 0; |
| } |
| #endif |
| } else { |
| *batt_flags &= ~EC_BATT_FLAG_AC_PRESENT; |
| /* AC disconnected should get us out of force idle mode. */ |
| state_machine_force_idle = 0; |
| } |
| |
| #if defined(CONFIG_BATTERY_PRESENT_CUSTOM) || \ |
| defined(CONFIG_BATTERY_PRESENT_GPIO) |
| if (battery_is_present() == BP_NO) { |
| curr->error |= F_BATTERY_NOT_CONNECTED; |
| return curr->error; |
| } |
| #endif |
| |
| /* Read params and see if battery is responsive */ |
| battery_get_params(batt); |
| if (!(batt->flags & BATT_FLAG_RESPONSIVE)) { |
| /* Check low battery condition and retry */ |
| if (curr->ac && ctx->battery_responsive && |
| !(curr->error & F_CHARGER_MASK)) { |
| ctx->battery_responsive = 0; |
| /* |
| * Try to revive ultra low voltage pack. Charge |
| * battery pack with minimum current and maximum |
| * voltage for 30 seconds. |
| */ |
| charge_request(ctx->battery->voltage_max, |
| ctx->battery->precharge_current); |
| for (d = 0; d < PRECHARGE_TIMEOUT; d++) { |
| sleep(1); |
| battery_get_params(batt); |
| if (batt->flags & BATT_FLAG_RESPONSIVE) { |
| ctx->battery_responsive = 1; |
| break; |
| } |
| } |
| } |
| |
| /* Set error if battery is still unresponsive */ |
| if (!(batt->flags & BATT_FLAG_RESPONSIVE)) { |
| curr->error |= F_BATTERY_UNRESPONSIVE; |
| return curr->error; |
| } |
| } else { |
| ctx->battery_responsive = 1; |
| } |
| |
| /* Translate flags */ |
| if (batt->flags & BATT_FLAG_BAD_ANY) |
| curr->error |= F_BATTERY_GET_PARAMS; |
| if (batt->flags & BATT_FLAG_BAD_VOLTAGE) |
| curr->error |= F_BATTERY_VOLTAGE; |
| if (batt->flags & BATT_FLAG_BAD_STATE_OF_CHARGE) |
| curr->error |= F_BATTERY_STATE_OF_CHARGE; |
| |
| *ctx->memmap_batt_volt = batt->voltage; |
| |
| /* Memory mapped value: discharge rate */ |
| *ctx->memmap_batt_rate = batt->current < 0 ? |
| -batt->current : batt->current; |
| |
| /* Fake state of charge if necessary */ |
| if (fake_state_of_charge >= 0) { |
| batt->state_of_charge = fake_state_of_charge; |
| 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))) |
| low_battery_shutdown(ctx); |
| } |
| |
| /* 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; |
| } |
| |
| #ifdef CONFIG_BATTERY_OVERRIDE_PARAMS |
| /* Apply battery pack vendor charging method */ |
| battery_override_params(batt); |
| #endif |
| |
| #ifdef CONFIG_CHARGER_CURRENT_LIMIT |
| if (batt->desired_current > CONFIG_CHARGER_CURRENT_LIMIT) |
| batt->desired_current = CONFIG_CHARGER_CURRENT_LIMIT; |
| #endif |
| if (batt->desired_current > user_current_limit) |
| batt->desired_current = user_current_limit; |
| |
| if (fake_state_of_charge >= 0) |
| *ctx->memmap_batt_cap = |
| fake_state_of_charge * |
| *(int *)host_get_memmap(EC_MEMMAP_BATT_LFCC) / 100; |
| else if (battery_remaining_capacity(&d)) |
| 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 charge_state state_init(struct charge_state_context *ctx) |
| { |
| /* Stop charger, unconditionally */ |
| charge_request(0, 0); |
| |
| /* if battery was not detected initially, get battery info again */ |
| if (ctx->battery == NULL) |
| ctx->battery = battery_get_info(); |
| |
| /* Update static battery info */ |
| update_battery_info(); |
| |
| /* Clear shutdown timer */ |
| ctx->shutdown_warning_time.val = 0; |
| |
| /* 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_IDLE0; |
| } |
| |
| /** |
| * Idle state handler |
| * |
| * - both charger and battery are online |
| * - detect charger and battery status change |
| * - new states: CHARGE, INIT |
| */ |
| static enum charge_state state_idle(struct charge_state_context *ctx) |
| { |
| struct batt_params *batt = &ctx->curr.batt; |
| |
| /* 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_REINIT; |
| |
| 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_REINIT; |
| |
| if (batt->state_of_charge >= BATTERY_LEVEL_FULL) |
| return PWR_STATE_UNCHANGE; |
| |
| /* Configure init charger state and switch to charge state */ |
| if (batt->flags & BATT_FLAG_WANT_CHARGE) { |
| int want_current = |
| charger_closest_current(batt->desired_current); |
| |
| CPRINTS("Charge start %dmV %dmA", |
| batt->desired_voltage, want_current); |
| |
| if (charge_request(batt->desired_voltage, want_current)) |
| return PWR_STATE_ERROR; |
| |
| update_charger_time(ctx, get_time()); |
| |
| if (ctx->curr.batt.state_of_charge < BATTERY_LEVEL_NEAR_FULL) |
| return PWR_STATE_CHARGE; |
| else |
| return PWR_STATE_CHARGE_NEAR_FULL; |
| } |
| |
| return PWR_STATE_UNCHANGE; |
| } |
| |
| /** |
| * Charge state handler |
| * |
| * - detect battery status change |
| * - new state: INIT |
| */ |
| static enum charge_state state_charge(struct charge_state_context *ctx) |
| { |
| struct charge_state_data *curr = &ctx->curr; |
| struct batt_params *batt = &ctx->curr.batt; |
| int debounce = 0; |
| int want_current; |
| int want_voltage; |
| timestamp_t now; |
| |
| if (curr->error) |
| return PWR_STATE_ERROR; |
| |
| /* |
| * Some chargers will reset out from underneath us. If this happens, |
| * reinitialize charging. |
| */ |
| if (curr->charging_voltage == 0 || |
| curr->charging_current == 0) |
| return PWR_STATE_REINIT; |
| |
| if (!curr->ac) |
| return PWR_STATE_REINIT; |
| |
| /* Stop charging if charging is no longer allowed */ |
| if (!(batt->flags & BATT_FLAG_WANT_CHARGE)) { |
| if (charge_request(0, 0)) |
| return PWR_STATE_ERROR; |
| return PWR_STATE_IDLE; |
| } |
| |
| now = get_time(); |
| |
| /* |
| * Adjust desired voltage to one the charger can actually supply |
| * or else we'll keep asking for a voltage the charger can't actually |
| * supply. |
| */ |
| want_voltage = charger_closest_voltage(batt->desired_voltage); |
| |
| if (want_voltage != curr->charging_voltage) { |
| CPRINTS("Charge voltage %dmV", want_voltage); |
| if (charge_request(want_voltage, -1)) |
| return PWR_STATE_ERROR; |
| update_charger_time(ctx, now); |
| } |
| |
| /* |
| * Adjust desired current to one the charger can actually supply before |
| * we do debouncing, or else we'll keep asking for a current the |
| * charger can't actually supply. |
| */ |
| want_current = charger_closest_current(batt->desired_current); |
| |
| if (want_current == curr->charging_current) { |
| /* Tick charger watchdog */ |
| if (!is_charger_expired(ctx, now)) |
| return PWR_STATE_UNCHANGE; |
| } else if (want_current > curr->charging_current) { |
| if (!timestamp_expired(ctx->voltage_debounce_time, &now)) |
| return PWR_STATE_UNCHANGE; |
| } else { |
| debounce = 1; |
| } |
| |
| if (want_current != curr->charging_current) { |
| CPRINTS("Charge current %dmA @ %dmV", |
| want_current, batt->desired_voltage); |
| } |
| |
| if (charge_request(-1, want_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 charge_state state_discharge(struct charge_state_context *ctx) |
| { |
| struct batt_params *batt = &ctx->curr.batt; |
| int8_t bat_temp_c = DECI_KELVIN_TO_CELSIUS(batt->temperature); |
| if (ctx->curr.ac) |
| return PWR_STATE_REINIT; |
| |
| if (ctx->curr.error) |
| return PWR_STATE_ERROR; |
| |
| /* Handle overtemp in discharging state by powering off host */ |
| if ((bat_temp_c >= ctx->battery->discharging_max_c || |
| bat_temp_c < ctx->battery->discharging_min_c) && |
| chipset_in_state(CHIPSET_STATE_ON)) { |
| CPRINTS("charge force shutdown due to battery temp"); |
| chipset_force_shutdown(); |
| host_set_single_event(EC_HOST_EVENT_BATTERY_SHUTDOWN); |
| } |
| return PWR_STATE_UNCHANGE; |
| } |
| |
| /** |
| * Error state handler |
| * |
| * - check charger and battery communication |
| * - log error |
| * - new state: INIT |
| */ |
| static enum charge_state state_error(struct charge_state_context *ctx) |
| { |
| static int logged_error; |
| |
| if (!ctx->curr.error) { |
| logged_error = 0; |
| return PWR_STATE_REINIT; |
| } |
| |
| charge_request(0, 0); |
| |
| /* Debug output */ |
| if (ctx->curr.error != logged_error) { |
| CPRINTS("Charge error: flag[%08b -> %08b], ac %d, " |
| " charger %s, battery %s", |
| 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; |
| } |
| |
| /** |
| * Print charging progress |
| */ |
| static void charging_progress(struct charge_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); |
| |
| CPRINTS("Battery %3d%% / %dh:%d", |
| 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) { |
| /* Calculate minutes by dividing usec by 60 million. GNU |
| * toolchain generates architecture dependent calls instead of |
| * machine code when the divisor is large, so break the |
| * calculation into 2 lines. |
| */ |
| seconds = (int)(get_time().val - |
| ctx->trickle_charging_time.val) / (int)SECOND; |
| minutes = seconds / 60; |
| CPRINTS("Precharge CHG(%dmV) BATT(%dmV %dmA) " |
| "%dh:%d", ctx->curr.charging_voltage, |
| ctx->curr.batt.voltage, ctx->curr.batt.current, |
| minutes / 60, minutes % 60); |
| } |
| } |
| |
| enum charge_state charge_get_state(void) |
| { |
| return task_ctx.curr.state; |
| } |
| |
| uint32_t charge_get_flags(void) |
| { |
| uint32_t flags = 0; |
| |
| if (state_machine_force_idle) |
| flags |= CHARGE_FLAG_FORCE_IDLE; |
| if (extpower_is_present()) |
| flags |= CHARGE_FLAG_EXTERNAL_POWER; |
| |
| return flags; |
| } |
| |
| int charge_get_percent(void) |
| { |
| return task_ctx.curr.batt.state_of_charge; |
| } |
| |
| int charge_get_battery_temp(int idx, int *temp_ptr) |
| { |
| const struct batt_params *batt = &task_ctx.curr.batt; |
| |
| if (!(batt->flags & BATT_FLAG_RESPONSIVE)) |
| return EC_ERROR_UNKNOWN; |
| |
| *temp_ptr = C_TO_K(DECI_KELVIN_TO_CELSIUS(batt->temperature)); |
| return EC_SUCCESS; |
| } |
| |
| int charge_want_shutdown(void) |
| { |
| return (charge_get_state() == PWR_STATE_DISCHARGE) && |
| charge_get_percent() < BATTERY_LEVEL_SHUTDOWN; |
| } |
| |
| int charge_prevent_power_on(int power_button_pressed) |
| { |
| int prevent_power_on = 0; |
| #ifdef CONFIG_CHARGER_MIN_BAT_PCT_FOR_POWER_ON |
| /* Require a minimum battery level to power on */ |
| if (battery_is_present() == BP_NO || |
| charge_get_percent() < CONFIG_CHARGER_MIN_BAT_PCT_FOR_POWER_ON) |
| prevent_power_on = 1; |
| #endif |
| /* Factory override: Always allow power on if WP is disabled */ |
| return prevent_power_on && system_is_locked(); |
| } |
| |
| static int charge_force_idle(int enable) |
| { |
| if (enable) { |
| /* |
| * Force-idle state is only meaningful if external power is |
| * present. If it's not present we can't charge anyway... |
| */ |
| if (!(charge_get_flags() & CHARGE_FLAG_EXTERNAL_POWER)) |
| return EC_ERROR_UNKNOWN; |
| charger_post_init(); |
| } |
| state_machine_force_idle = enable; |
| return EC_SUCCESS; |
| } |
| |
| const struct batt_params *charger_current_battery_params(void) |
| { |
| return &task_ctx.curr.batt; |
| } |
| |
| /** |
| * Battery charging task |
| */ |
| void charger_task(void) |
| { |
| struct charge_state_context *ctx = &task_ctx; |
| timestamp_t ts; |
| int sleep_usec = CHARGE_POLL_PERIOD_SHORT, diff_usec, sleep_next; |
| enum charge_state new_state; |
| uint8_t batt_flags; |
| |
| while (1) { |
| #ifdef CONFIG_SB_FIRMWARE_UPDATE |
| if (sb_fw_update_in_progress()) { |
| task_wait_event(CHARGE_MAX_SLEEP_USEC); |
| continue; |
| } |
| #endif |
| state_common(ctx); |
| |
| #ifdef CONFIG_CHARGER_TIMEOUT_HOURS |
| if (ctx->curr.state == PWR_STATE_CHARGE && |
| ctx->charge_state_updated_time.val + |
| CONFIG_CHARGER_TIMEOUT_HOURS * HOUR < ctx->curr.ts.val) { |
| CPRINTS("Charge timed out after %d hours", |
| CONFIG_CHARGER_TIMEOUT_HOURS); |
| charge_force_idle(1); |
| } |
| #endif /* CONFIG_CHARGER_TIMEOUT_HOURS */ |
| |
| switch (ctx->prev.state) { |
| case PWR_STATE_INIT: |
| case PWR_STATE_REINIT: |
| new_state = state_init(ctx); |
| break; |
| case PWR_STATE_IDLE0: |
| new_state = state_idle(ctx); |
| /* If still idling, move from IDLE0 to IDLE */ |
| if (new_state == PWR_STATE_UNCHANGE) |
| new_state = PWR_STATE_IDLE; |
| 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); |
| if (new_state == PWR_STATE_UNCHANGE && |
| (ctx->curr.batt.state_of_charge >= |
| BATTERY_LEVEL_NEAR_FULL)) { |
| /* Almost done charging */ |
| new_state = PWR_STATE_CHARGE_NEAR_FULL; |
| } |
| break; |
| |
| case PWR_STATE_CHARGE_NEAR_FULL: |
| new_state = state_charge(ctx); |
| if (new_state == PWR_STATE_UNCHANGE && |
| (ctx->curr.batt.state_of_charge < |
| BATTERY_LEVEL_NEAR_FULL)) { |
| /* Battery below almost-full threshold. */ |
| new_state = PWR_STATE_CHARGE; |
| } |
| break; |
| case PWR_STATE_ERROR: |
| new_state = state_error(ctx); |
| break; |
| default: |
| CPRINTS("Charge state %d undefined", |
| ctx->curr.state); |
| ctx->curr.state = PWR_STATE_ERROR; |
| new_state = PWR_STATE_ERROR; |
| } |
| |
| if (state_machine_force_idle && |
| ctx->prev.state != PWR_STATE_IDLE0 && |
| ctx->prev.state != PWR_STATE_IDLE && |
| ctx->prev.state != PWR_STATE_INIT && |
| ctx->prev.state != PWR_STATE_REINIT) |
| new_state = PWR_STATE_REINIT; |
| |
| if (new_state) { |
| ctx->curr.state = new_state; |
| CPRINTS("Charge state %s -> %s after %.6ld sec", |
| state_name[ctx->prev.state], |
| state_name[new_state], |
| ctx->curr.ts.val - |
| ctx->charge_state_updated_time.val); |
| ctx->charge_state_updated_time = ctx->curr.ts; |
| hook_notify(HOOK_CHARGE_STATE_CHANGE); |
| } |
| |
| switch (new_state) { |
| case PWR_STATE_IDLE0: |
| /* |
| * First time transitioning from init -> idle. Don't |
| * set the flags or LED yet because we may transition |
| * to charging on the next call and we don't want to |
| * blink the LED green. |
| */ |
| sleep_usec = CHARGE_POLL_PERIOD_SHORT; |
| break; |
| case PWR_STATE_CHARGE_NEAR_FULL: |
| /* |
| * Battery is almost charged. The last few percent |
| * take a loooong time, so fall through and look like |
| * we're charged. This mirrors similar hacks at the |
| * ACPI/kernel/UI level. |
| */ |
| 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 */ |
| sleep_usec = (new_state == PWR_STATE_IDLE |
| ? CHARGE_POLL_PERIOD_LONG |
| : CHARGE_POLL_PERIOD_CHARGE); |
| 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 = CHARGE_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 */ |
| sleep_usec = CHARGE_POLL_PERIOD_CHARGE; |
| break; |
| case PWR_STATE_ERROR: |
| /* Error */ |
| sleep_usec = CHARGE_POLL_PERIOD_CHARGE; |
| break; |
| case PWR_STATE_UNCHANGE: |
| /* Don't change sleep duration */ |
| break; |
| default: |
| /* Other state; poll quickly and hope it goes away */ |
| sleep_usec = CHARGE_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 (ctx->curr.state == PWR_STATE_DISCHARGE && |
| chipset_in_state(CHIPSET_STATE_ANY_OFF | |
| CHIPSET_STATE_SUSPEND)) { |
| /* |
| * Discharging and system is off or suspended, so no |
| * need to poll frequently. charge_hook() will wake us |
| * up if anything important changes. |
| */ |
| sleep_next = CHARGE_POLL_PERIOD_VERY_LONG - diff_usec; |
| } else if (sleep_next < CHARGE_MIN_SLEEP_USEC) { |
| sleep_next = CHARGE_MIN_SLEEP_USEC; |
| } else if (sleep_next > CHARGE_MAX_SLEEP_USEC) { |
| sleep_next = CHARGE_MAX_SLEEP_USEC; |
| } |
| |
| task_wait_event(sleep_next); |
| } |
| } |
| |
| /*****************************************************************************/ |
| /* Hooks */ |
| |
| /** |
| * Chipset notification hook. |
| * |
| * This is triggered when the system boots or resumes, so that we can update |
| * our charging state. |
| */ |
| static void chipset_hook(void) |
| { |
| /* Wake up the task now */ |
| task_wake(TASK_ID_CHARGER); |
| } |
| DECLARE_HOOK(HOOK_CHIPSET_RESUME, chipset_hook, HOOK_PRIO_DEFAULT); |
| |
| /** |
| * AC change notification hook. |
| * |
| * This is triggered when the AC state changes, so that we can update the |
| * memory-mapped AC status and our charging state. |
| */ |
| static void ac_change_hook(void) |
| { |
| /** |
| * Update the memory-mapped AC_PRESENT flag immediately so the |
| * state is correct prior to the host being notified of the AC |
| * change event. |
| */ |
| if (extpower_is_present()) |
| *task_ctx.memmap_batt_flags |= EC_BATT_FLAG_AC_PRESENT; |
| else |
| *task_ctx.memmap_batt_flags &= ~EC_BATT_FLAG_AC_PRESENT; |
| |
| /* Wake up the task now */ |
| task_wake(TASK_ID_CHARGER); |
| } |
| DECLARE_HOOK(HOOK_AC_CHANGE, ac_change_hook, HOOK_PRIO_DEFAULT); |
| |
| static void charge_init(void) |
| { |
| struct charge_state_context *ctx = &task_ctx; |
| |
| 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(); |
| /* Assume the battery is responsive until proven otherwise */ |
| ctx->battery_responsive = 1; |
| |
| /* Set up 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); |
| } |
| DECLARE_HOOK(HOOK_INIT, charge_init, HOOK_PRIO_DEFAULT); |
| |
| |
| static void charge_shutdown(void) |
| { |
| /* Hibernate immediately if battery level is too low */ |
| if (charge_want_shutdown()) { |
| CPRINTS("charge force EC hibernate after " |
| "shutdown due to low battery"); |
| system_hibernate(0, 0); |
| } |
| } |
| /* |
| * Run the charge shutdown hook last, since when it hibernates no subsequent |
| * hooks would be run. |
| */ |
| DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, charge_shutdown, HOOK_PRIO_LAST); |
| |
| /*****************************************************************************/ |
| /* Host commands */ |
| |
| static int charge_command_charge_control(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_charge_control *p = args->params; |
| int rv; |
| |
| if (system_is_locked()) |
| return EC_RES_ACCESS_DENIED; |
| |
| rv = charge_force_idle(p->mode != CHARGE_CONTROL_NORMAL); |
| if (rv != EC_SUCCESS) |
| return rv; |
| |
| #ifdef CONFIG_CHARGER_DISCHARGE_ON_AC |
| #ifdef CONFIG_CHARGER_DISCHARGE_ON_AC_CUSTOM |
| rv = board_discharge_on_ac(p->mode == CHARGE_CONTROL_DISCHARGE); |
| #else |
| rv = charger_discharge_on_ac(p->mode == CHARGE_CONTROL_DISCHARGE); |
| #endif |
| if (rv != EC_SUCCESS) |
| return rv; |
| #endif |
| |
| return EC_RES_SUCCESS; |
| } |
| /* |
| * TODO(crbug.com/239197) : Adding both versions to the version mask is a |
| * temporary workaround for a problem in the cros_ec driver. Drop |
| * EC_VER_MASK(0) once cros_ec driver can send the correct version. |
| */ |
| DECLARE_HOST_COMMAND(EC_CMD_CHARGE_CONTROL, charge_command_charge_control, |
| EC_VER_MASK(0) | EC_VER_MASK(1)); |
| |
| static void reset_current_limit(void) |
| { |
| user_current_limit = -1; |
| } |
| DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, reset_current_limit, HOOK_PRIO_DEFAULT); |
| DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, reset_current_limit, HOOK_PRIO_DEFAULT); |
| |
| static int charge_command_current_limit(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_current_limit *p = args->params; |
| |
| user_current_limit = p->limit; |
| |
| return EC_RES_SUCCESS; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_CHARGE_CURRENT_LIMIT, charge_command_current_limit, |
| EC_VER_MASK(0)); |
| |
| /*****************************************************************************/ |
| /* Console commands */ |
| |
| static int command_battfake(int argc, char **argv) |
| { |
| char *e; |
| int v; |
| |
| if (argc == 2) { |
| v = strtoi(argv[1], &e, 0); |
| if (*e || v < -1 || v > 100) |
| return EC_ERROR_PARAM1; |
| |
| fake_state_of_charge = v; |
| } |
| |
| if (fake_state_of_charge < 0) |
| ccprintf("Reporting real battery level\n"); |
| else |
| ccprintf("Reporting fake battery level %d%%\n", |
| fake_state_of_charge); |
| |
| /* Wake charger task immediately to see new level */ |
| task_wake(TASK_ID_CHARGER); |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_CONSOLE_COMMAND(battfake, command_battfake, |
| "percent (-1 = use real level)", |
| "Set fake battery level"); |