diff options
Diffstat (limited to 'board/sylas/led.c')
-rw-r--r-- | board/sylas/led.c | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/board/sylas/led.c b/board/sylas/led.c new file mode 100644 index 0000000000..3e7132756d --- /dev/null +++ b/board/sylas/led.c @@ -0,0 +1,617 @@ +/* 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) (1 << 7 | (interval)) +#define BLINK(interval) (interval) +#define ALTERNATE(interval) (1 << 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) +{ + uint32_t oem = PROJECT_NAMI; + + cbi_get_oem_id(&oem); + + 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; + 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 <duty_inc> every <interval> 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); +} |