/* Copyright 2017 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. * * Power and battery LED control for Nami and its variants * * This is an event-driven LED control library. It does not use tasks or * periodical hooks (HOOK_TICK, HOOK_SECOND), thus, it's more resource * efficient. * * The library defines LED states and assigns an LED behavior to each state. * The state space consists of tuple of (charge state, power state). * In each LED state, a color and a pulse interval can be defined. * * Charging states are queried each time there is a state transition, thus, not * stored. We hook power state transitions (e.g. s0->s3) and save the * destination states (e.g. s3) in power_state. * * When system is suspending and AC is unplugged, there will be race condition * between a power state hook and a charge state hook but whichever is called * first or last the result will be the same. * * Currently, it supports two LEDs, called 'battery LED' and 'power LED'. * It assumes the battery LED is connected to a PWM pin and the power LED is * connected to a regular GPIO pin. */ #include "cros_board_info.h" #include "charge_state.h" #include "chipset.h" #include "console.h" #include "ec_commands.h" #include "gpio.h" #include "hooks.h" #include "led_common.h" #include "power.h" #include "pwm.h" #include "timer.h" #include "util.h" const enum ec_led_id supported_led_ids[] = { EC_LED_ID_BATTERY_LED, EC_LED_ID_POWER_LED}; const int supported_led_ids_count = ARRAY_SIZE(supported_led_ids); enum led_color { LED_OFF = 0, LED_RED, LED_GREEN, LED_AMBER, LED_WHITE, LED_WARM_WHITE, LED_FACTORY, /* Number of colors, not a color itself */ LED_COLOR_COUNT }; /* Charging states of LED's interests */ enum led_charge_state { LED_STATE_DISCHARGE = 0, LED_STATE_CHARGE, LED_STATE_FULL, LED_CHARGE_STATE_COUNT, }; /* Power states of LED's interests */ enum led_power_state { LED_STATE_S0 = 0, LED_STATE_S3, LED_STATE_S5, LED_POWER_STATE_COUNT, }; /* Defines a LED pattern for a single state */ struct led_pattern { uint8_t color; /* Bit 0-5: Interval in 100 msec. 0=solid. Max is 3.2 sec. * Bit 6: 1=alternate (on-off-off-off), 0=regular (on-off-on-off) * Bit 7: 1=pulse, 0=blink */ uint8_t pulse; }; #define PULSE_NO 0 #define PULSE(interval) (BIT(7) | (interval)) #define BLINK(interval) (interval) #define ALTERNATE(interval) (BIT(6) | (interval)) #define IS_PULSING(pulse) ((pulse) & 0x80) #define IS_ALTERNATE(pulse) ((pulse) & 0x40) #define PULSE_INTERVAL(pulse) (((pulse) & 0x3f) * 100 * MSEC) /* 40 msec for nice and smooth transition. */ #define LED_PULSE_TICK_US (40 * MSEC) typedef struct led_pattern led_patterns[LED_CHARGE_STATE_COUNT] [LED_POWER_STATE_COUNT]; /* * Nami/Vayne - One dual color LED: * Charging Amber on (S0/S3/S5) * Charging (full) White on (S0/S3/S5) * Discharge in S0 White on * Discharge in S3/S0ix Pulsing (rising for 2 sec , falling for 2 sec) * Discharge in S5 Off * Battery Error Amber on 1sec off 1sec * Factory mode White on 2sec, Amber on 2sec */ const static led_patterns battery_pattern_0 = { /* discharging: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE(10)}, {LED_OFF, PULSE_NO}}, /* charging: s0, s3, s5 */ {{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}}, }; /* * Sona - Battery LED (dual color) */ const static led_patterns battery_pattern_1 = { /* discharging: s0, s3, s5 */ {{LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}}, /* charging: s0, s3, s5 */ {{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}}, }; /* * Pantheon - AC In/Battery LED(dual color): * Connected to AC power / Charged (100%) White (solid on) * Connected to AC power / Charging(1% -99%) Amber (solid on) * Not connected to AC power Off */ const static led_patterns battery_pattern_2 = { /* discharging: s0, s3, s5 */ {{LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}}, /* charging: s0, s3, s5 */ {{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}}, }; /* * Sona - Power LED (single color) */ const static led_patterns power_pattern_1 = { /* discharging: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}}, /* charging: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}}, }; /* * Pantheon - Power LED * S0: White on * S3/S0ix: White 1 second on, 3 second off * S5: Off */ const static led_patterns power_pattern_2 = { /* discharging: s0, s3, s5 */ {{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}}, /* charging: s0, s3, s5 */ {{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}}, /* full: s0, s3, s5 */ {{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}}, }; /* * Akali - battery LED * Charge: Amber on (s0/s3/s5) * Full: Blue on (s0/s3/s5) * Discharge in S0: Blue on * Discharge in S3: Amber on 1 sec off 3 sec * Discharge in S5: Off * Battery Error: Amber on 1sec off 1sec * Factory mode : Blue on 2sec, Amber on 2sec */ const static led_patterns battery_pattern_3 = { /* discharging: s0, s3, s5 */ {{LED_WHITE, 0}, {LED_AMBER, ALTERNATE(BLINK(10))}, {LED_OFF, 0}}, /* charging: s0, s3, s5 */ {{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}}, }; const static led_patterns battery_pattern_4 = { /* discharging: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}}, /* charging: s0, s3, s5 */ {{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}}, /* full: s0, s3, s5 */ {{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}}, }; /* Patterns for battery LED and power LED. Initialized at run-time. */ static led_patterns const *patterns[2]; /* Pattern for battery error. Only blinking battery LED is supported. */ static struct led_pattern battery_error = {LED_AMBER, BLINK(10)}; /* Pattern for low state of charge. Only battery LED is supported. */ static struct led_pattern low_battery = {LED_WHITE, BLINK(10)}; /* Pattern for factory mode. Blinking 2-color battery LED. */ static struct led_pattern battery_factory = {LED_FACTORY, BLINK(20)}; static int low_battery_soc; static void led_charge_hook(void); static enum led_power_state power_state; static void led_init(void) { switch (oem) { case PROJECT_NAMI: case PROJECT_VAYNE: patterns[0] = &battery_pattern_0; break; case PROJECT_SONA: if (model == MODEL_SYNDRA) { /* Syndra doesn't have power LED */ patterns[0] = &battery_pattern_4; } else { patterns[0] = &battery_pattern_1; patterns[1] = &power_pattern_1; } battery_error.pulse = BLINK(5); low_battery_soc = 100; /* 10.0% */ break; case PROJECT_PANTHEON: patterns[0] = &battery_pattern_2; patterns[1] = &power_pattern_2; battery_error.color = LED_OFF; battery_error.pulse = 0; break; case PROJECT_AKALI: patterns[0] = &battery_pattern_3; break; default: break; } pwm_enable(PWM_CH_LED1, 1); pwm_enable(PWM_CH_LED2, 1); /* After sysjump, power_state is cleared. Thus, we need to actively * retrieve it. */ if (chipset_in_state(CHIPSET_STATE_ANY_OFF)) power_state = LED_STATE_S5; else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND)) power_state = LED_STATE_S3; else power_state = LED_STATE_S0; } DECLARE_HOOK(HOOK_INIT, led_init, HOOK_PRIO_DEFAULT); static int set_color_battery(enum led_color color, int duty) { int led1 = 0; int led2 = 0; if (duty < 0 || 100 < duty) return EC_ERROR_UNKNOWN; switch (color) { case LED_OFF: break; case LED_AMBER: led2 = 1; break; case LED_WHITE: led1 = 1; break; case LED_WARM_WHITE: led1 = 1; led2 = 1; break; case LED_FACTORY: break; default: return EC_ERROR_UNKNOWN; } if (color != LED_FACTORY) { pwm_set_duty(PWM_CH_LED1, led1 ? duty : 0); pwm_set_duty(PWM_CH_LED2, led2 ? duty : 0); } else { pwm_set_duty(PWM_CH_LED1, duty ? 100 : 0); pwm_set_duty(PWM_CH_LED2, duty ? 0 : 100); } return EC_SUCCESS; } static int set_color_power(enum led_color color, int duty) { if (color == LED_OFF) duty = 0; gpio_set_level(GPIO_LED1, !duty /* Reversed logic */); return EC_SUCCESS; } static int set_color(enum ec_led_id id, enum led_color color, int duty) { switch (id) { case EC_LED_ID_BATTERY_LED: return set_color_battery(color, duty); case EC_LED_ID_POWER_LED: return set_color_power(color, duty); default: return EC_ERROR_UNKNOWN; } } static struct { uint32_t interval; int duty_inc; enum led_color color; int duty; int alternate; uint8_t pulse; } tick[2]; static void tick_battery(void); DECLARE_DEFERRED(tick_battery); static void tick_power(void); DECLARE_DEFERRED(tick_power); static void cancel_tick(enum ec_led_id id) { if (id == EC_LED_ID_BATTERY_LED) hook_call_deferred(&tick_battery_data, -1); else hook_call_deferred(&tick_power_data, -1); } static int config_tick(enum ec_led_id id, const struct led_pattern *pattern) { static const struct led_pattern *patterns[2]; uint32_t stride; if (pattern == patterns[id]) /* This pattern was already set */ return -1; patterns[id] = pattern; if (!pattern->pulse) { /* This is a steady pattern. cancel the tick */ cancel_tick(id); set_color(id, pattern->color, 100); return 1; } stride = PULSE_INTERVAL(pattern->pulse); if (IS_PULSING(pattern->pulse)) { tick[id].interval = LED_PULSE_TICK_US; tick[id].duty_inc = 100 / (stride / LED_PULSE_TICK_US); } else { tick[id].interval = stride; tick[id].duty_inc = 100; } tick[id].color = pattern->color; tick[id].duty = 0; tick[id].alternate = 0; tick[id].pulse = pattern->pulse; return 0; } /* * When pulsing, brightness is incremented by every usec * from 0 to 100%. Then it's decremented from 100% to 0. */ static void pulse_led(enum ec_led_id id) { if (tick[id].duty + tick[id].duty_inc > 100) { tick[id].duty_inc = tick[id].duty_inc * -1; } else if (tick[id].duty + tick[id].duty_inc < 0) { if (IS_ALTERNATE(tick[id].pulse)) { /* Falling phase landing. Flip the alternate flag. */ tick[id].alternate = !tick[id].alternate; if (tick[id].alternate) return; } tick[id].duty_inc = tick[id].duty_inc * -1; } tick[id].duty += tick[id].duty_inc; set_color(id, tick[id].color, tick[id].duty); } static uint32_t tick_led(enum ec_led_id id) { uint32_t elapsed; uint32_t start = get_time().le.lo; uint32_t next; if (led_auto_control_is_enabled(id)) pulse_led(id); if (tick[id].alternate) /* Skip 2 phases (rising & falling) */ next = PULSE_INTERVAL(tick[id].pulse) * 2; else next = tick[id].interval; elapsed = get_time().le.lo - start; return next > elapsed ? next - elapsed : 0; } static void tick_battery(void) { hook_call_deferred(&tick_battery_data, tick_led(EC_LED_ID_BATTERY_LED)); } static void tick_power(void) { hook_call_deferred(&tick_power_data, tick_led(EC_LED_ID_POWER_LED)); } static void start_tick(enum ec_led_id id, const struct led_pattern *pattern) { if (config_tick(id, pattern)) /* * If this pattern is already active, ticking must have started * already. So, we don't re-start ticking to prevent LED from * blinking at every SOC change. * * If this pattern is static, we skip ticking as well. */ return; if (id == EC_LED_ID_BATTERY_LED) tick_battery(); else tick_power(); } static void led_alert(int enable) { if (enable) start_tick(EC_LED_ID_BATTERY_LED, &battery_error); else led_charge_hook(); } static void led_factory(int enable) { if (enable) start_tick(EC_LED_ID_BATTERY_LED, &battery_factory); else led_charge_hook(); } void config_led(enum ec_led_id id, enum led_charge_state charge) { const led_patterns *pattern; pattern = patterns[id]; if (!pattern) return; /* This LED isn't present */ start_tick(id, &(*pattern)[charge][power_state]); } void config_leds(enum led_charge_state charge) { config_led(EC_LED_ID_BATTERY_LED, charge); config_led(EC_LED_ID_POWER_LED, charge); } static void call_handler(void) { int soc; enum charge_state cs; if (!led_auto_control_is_enabled(EC_LED_ID_BATTERY_LED)) return; cs = charge_get_state(); soc = charge_get_display_charge(); if (soc < 0) cs = PWR_STATE_ERROR; switch (cs) { case PWR_STATE_DISCHARGE: case PWR_STATE_DISCHARGE_FULL: if (soc < low_battery_soc) start_tick(EC_LED_ID_BATTERY_LED, &low_battery); else config_led(EC_LED_ID_BATTERY_LED, LED_STATE_DISCHARGE); config_led(EC_LED_ID_POWER_LED, LED_STATE_DISCHARGE); break; case PWR_STATE_CHARGE_NEAR_FULL: case PWR_STATE_CHARGE: if (soc >= 1000) config_leds(LED_STATE_FULL); else config_leds(LED_STATE_CHARGE); break; case PWR_STATE_ERROR: /* It doesn't matter what 'charge' state we pass because power * LED (if it exists) is orthogonal to battery state. */ config_led(EC_LED_ID_POWER_LED, 0); led_alert(1); break; case PWR_STATE_IDLE: /* External power connected in IDLE. This is also used to show * factory mode when 'ectool chargecontrol idle' is run during * factory process. */ if (charge_get_flags() & CHARGE_FLAG_FORCE_IDLE) led_factory(1); break; default: ; } } /* LED state transition handlers */ static void s0(void) { power_state = LED_STATE_S0; call_handler(); } DECLARE_HOOK(HOOK_CHIPSET_RESUME, s0, HOOK_PRIO_DEFAULT); DECLARE_HOOK(HOOK_CHIPSET_STARTUP, s0, HOOK_PRIO_DEFAULT); static void s3(void) { power_state = LED_STATE_S3; call_handler(); } DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, s3, HOOK_PRIO_DEFAULT); static void s5(void) { power_state = LED_STATE_S5; call_handler(); } DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, s5, HOOK_PRIO_DEFAULT); static void led_charge_hook(void) { call_handler(); } DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE, led_charge_hook, HOOK_PRIO_DEFAULT); static void print_config(enum ec_led_id id) { ccprintf("ID:%d\n", id); ccprintf(" Color:%d\n", tick[id].color); ccprintf(" Duty:%d\n", tick[id].duty); ccprintf(" Duty Increment:%d\n", tick[id].duty_inc); ccprintf(" Interval:%d\n", tick[id].interval); } static int command_led(int argc, char **argv) { enum ec_led_id id = EC_LED_ID_BATTERY_LED; static int alert = 0; static int factory; if (argc < 2) return EC_ERROR_PARAM_COUNT; if (!strcasecmp(argv[1], "debug")) { led_auto_control(id, !led_auto_control_is_enabled(id)); ccprintf("o%s\n", led_auto_control_is_enabled(id) ? "ff" : "n"); } else if (!strcasecmp(argv[1], "off")) { set_color(id, LED_OFF, 0); } else if (!strcasecmp(argv[1], "red")) { set_color(id, LED_RED, 100); } else if (!strcasecmp(argv[1], "white")) { set_color(id, LED_WHITE, 100); } else if (!strcasecmp(argv[1], "amber")) { set_color(id, LED_AMBER, 100); } else if (!strcasecmp(argv[1], "alert")) { alert = !alert; led_alert(alert); } else if (!strcasecmp(argv[1], "s0")) { s0(); } else if (!strcasecmp(argv[1], "s3")) { s3(); } else if (!strcasecmp(argv[1], "s5")) { s5(); } else if (!strcasecmp(argv[1], "conf")) { print_config(id); } else if (!strcasecmp(argv[1], "factory")) { factory = !factory; led_factory(factory); } else { return EC_ERROR_PARAM1; } return EC_SUCCESS; } DECLARE_CONSOLE_COMMAND(led, command_led, "[debug|red|green|amber|off|alert|s0|s3|s5|conf|factory]", "Turn on/off LED."); void led_get_brightness_range(enum ec_led_id led_id, uint8_t *brightness_range) { /* * We return amber=100, white=100 regardless of OEM ID or led_id. This * function is for ectool led command, which is used to test LED * functionality. */ brightness_range[EC_LED_COLOR_AMBER] = 100; brightness_range[EC_LED_COLOR_WHITE] = 100; } int led_set_brightness(enum ec_led_id id, const uint8_t *brightness) { if (brightness[EC_LED_COLOR_AMBER]) return set_color(id, LED_AMBER, brightness[EC_LED_COLOR_AMBER]); else if (brightness[EC_LED_COLOR_WHITE]) return set_color(id, LED_WHITE, brightness[EC_LED_COLOR_WHITE]); else return set_color(id, LED_OFF, 0); }