/* Copyright 2018 The ChromiumOS Authors * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ /* PWM LED control to conform to Chrome OS LED behaviour specification. */ /* * This assumes that a single logical LED is shared between both power and * charging/battery status. If multiple logical LEDs are present, they all * follow the same patterns. */ #include "battery.h" #include "charge_manager.h" #include "charge_state.h" #include "chipset.h" #include "common.h" #include "console.h" #include "ec_commands.h" #include "hooks.h" #include "led_common.h" #include "led_pwm.h" #include "pwm.h" #include "timer.h" #include "util.h" /* Battery percentage thresholds to blink at different rates. */ #define CRITICAL_LOW_BATTERY_PERCENTAGE 3 #define LOW_BATTERY_PERCENTAGE 10 #define PULSE_TICK (250 * MSEC) #ifndef CONFIG_LED_PWM_TASK_DISABLED static uint8_t led_is_pulsing; #endif /* CONFIG_LED_PWM_TASK_DISABLED */ static int get_led_id_color(enum pwm_led_id id, int color) { #ifdef CONFIG_LED_PWM_ACTIVE_CHARGE_PORT_ONLY int active_chg_port = charge_manager_get_active_charge_port(); /* We should always be able to turn off a LED. */ if (color == -1) return -1; if (led_is_pulsing) return color; /* The inactive charge port LEDs should be off. */ if ((int)id != active_chg_port) return -1; #endif /* CONFIG_LED_PWM_ACTIVE_CHARGE_PORT_ONLY */ return color; } void set_pwm_led_color(enum pwm_led_id id, int color) { struct pwm_led_color_map duty = { 0 }; const struct pwm_led *led = &pwm_leds[id]; if ((id >= CONFIG_LED_PWM_COUNT) || (id < 0) || (color >= EC_LED_COLOR_COUNT) || (color < -1)) return; if (color != -1) { duty.ch0 = led_color_map[color].ch0; duty.ch1 = led_color_map[color].ch1; duty.ch2 = led_color_map[color].ch2; } if (led->ch0 != PWM_LED_NO_CHANNEL) led->set_duty(led->ch0, duty.ch0); if (led->ch1 != PWM_LED_NO_CHANNEL) led->set_duty(led->ch1, duty.ch1); if (led->ch2 != PWM_LED_NO_CHANNEL) led->set_duty(led->ch2, duty.ch2); } static void set_led_color(int color) { /* * We must check if auto control is enabled since the LEDs may be * controlled from the AP at anytime. */ if ((led_auto_control_is_enabled(EC_LED_ID_POWER_LED)) || (led_auto_control_is_enabled(EC_LED_ID_LEFT_LED))) set_pwm_led_color(PWM_LED0, get_led_id_color(PWM_LED0, color)); #if CONFIG_LED_PWM_COUNT >= 2 if (led_auto_control_is_enabled(EC_LED_ID_RIGHT_LED)) set_pwm_led_color(PWM_LED1, get_led_id_color(PWM_LED1, color)); #endif /* CONFIG_LED_PWM_COUNT >= 2 */ } static void set_pwm_led_enable(enum pwm_led_id id, int enable) { #ifndef CONFIG_ZEPHYR const struct pwm_led *led = &pwm_leds[id]; if ((id >= CONFIG_LED_PWM_COUNT) || (id < 0)) return; if (led->ch0 != PWM_LED_NO_CHANNEL) led->enable(led->ch0, enable); if (led->ch1 != PWM_LED_NO_CHANNEL) led->enable(led->ch1, enable); if (led->ch2 != PWM_LED_NO_CHANNEL) led->enable(led->ch2, enable); #endif } static void init_leds_off(void) { /* Turn off LEDs such that they are in a known state with zero duty. */ set_led_color(-1); /* Enable pwm modules for each channels of LEDs */ set_pwm_led_enable(PWM_LED0, 1); #if CONFIG_LED_PWM_COUNT >= 2 set_pwm_led_enable(PWM_LED1, 1); #endif /* CONFIG_LED_PWM_COUNT >= 2 */ } DECLARE_HOOK(HOOK_INIT, init_leds_off, HOOK_PRIO_POST_PWM); #ifndef CONFIG_LED_PWM_TASK_DISABLED static uint8_t pulse_period; static uint8_t pulse_ontime; static enum ec_led_colors pulse_color; static void update_leds(void); static void pulse_leds_deferred(void); DECLARE_DEFERRED(pulse_leds_deferred); static void pulse_leds_deferred(void) { static uint8_t tick_count; if (!led_is_pulsing) { tick_count = 0; /* * Since we're not pulsing anymore, turn the colors off in case * we were in the "on" time. */ set_led_color(-1); /* Then show the desired state. */ update_leds(); return; } if (tick_count < pulse_ontime) set_led_color(pulse_color); else set_led_color(-1); tick_count = (tick_count + 1) % pulse_period; hook_call_deferred(&pulse_leds_deferred_data, PULSE_TICK); } static void pulse_leds(enum ec_led_colors color, int ontime, int period) { pulse_color = color; pulse_ontime = ontime; pulse_period = period; led_is_pulsing = 1; pulse_leds_deferred(); } #ifdef CONFIG_BATTERY static int show_charge_state(void) { enum led_pwr_state chg_st = led_pwr_get_state(); /* * The colors listed below are the default, but can be overridden. * * Solid Amber == Charging * Solid Green == Charging (near full) * Fast Flash Red == Charging error or battery not present */ if (chg_st == PWR_STATE_CHARGE) { led_is_pulsing = 0; set_led_color(CONFIG_LED_PWM_CHARGE_COLOR); } else if (chg_st == PWR_STATE_CHARGE_NEAR_FULL || chg_st == PWR_STATE_DISCHARGE_FULL) { led_is_pulsing = 0; set_led_color(CONFIG_LED_PWM_NEAR_FULL_COLOR); } else if ((battery_is_present() != BP_YES) || (chg_st == PWR_STATE_ERROR)) { /* 500 ms period, 50% duty cycle. */ pulse_leds(CONFIG_LED_PWM_CHARGE_ERROR_COLOR, 1, 2); } else { /* Discharging or not charging. */ #ifdef CONFIG_LED_PWM_CHARGE_STATE_ONLY /* * If we only show the charge state, the only reason we * would pulse the LEDs is if we had an error. If it no longer * exists, stop pulsing the LEDs. */ led_is_pulsing = 0; #endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */ return 0; } return 1; } #endif /* CONFIG_BATTERY */ #ifndef CONFIG_LED_PWM_CHARGE_STATE_ONLY #ifdef CONFIG_BATTERY static int show_battery_state(void) { int batt_percentage = charge_get_percent(); /* * The colors listed below are the default, but can be overridden. * * Fast Flash Amber == Critical Battery * Slow Flash Amber == Low Battery */ if (batt_percentage < CRITICAL_LOW_BATTERY_PERCENTAGE) { /* Flash amber faster (1 second period, 50% duty cycle) */ pulse_leds(CONFIG_LED_PWM_LOW_BATT_COLOR, 2, 4); } else if (batt_percentage < LOW_BATTERY_PERCENTAGE) { /* Flash amber (4 second period, 50% duty cycle) */ pulse_leds(CONFIG_LED_PWM_LOW_BATT_COLOR, 8, 16); } else { /* Sufficient charge, no need to show anything for this. */ return 0; } return 1; } #endif /* CONFIG_BATTERY */ static int show_chipset_state(void) { /* Reflect the SoC state. */ led_is_pulsing = 0; if (chipset_in_state(CHIPSET_STATE_ON)) { /* The LED must be on in the Active state. */ set_led_color(CONFIG_LED_PWM_SOC_ON_COLOR); } else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND)) { /* The power LED must pulse in the suspend state. */ pulse_leds(CONFIG_LED_PWM_SOC_SUSPEND_COLOR, 4, 16); } else { /* Chipset is off, no need to show anything for this. */ return 0; } return 1; } #endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */ static void update_leds(void) { #ifdef CONFIG_BATTERY /* Reflecting the charge state is the highest priority. */ if (show_charge_state()) return; #endif /* CONFIG_BATTERY */ #ifndef CONFIG_LED_PWM_CHARGE_STATE_ONLY #ifdef CONFIG_BATTERY if (show_battery_state()) return; #endif /* CONFIG_BATTERY */ if (show_chipset_state()) return; #endif /* CONFIG_LED_PWM_CHARGE_STATE_ONLY */ set_led_color(-1); } DECLARE_HOOK(HOOK_TICK, update_leds, HOOK_PRIO_DEFAULT); #endif /* CONFIG_LED_PWM_TASK_DISABLED */ #ifdef CONFIG_CMD_LEDTEST static int command_ledtest(int argc, const char **argv) { int enable; int pwm_led_id; int led_id; if (argc < 2) return EC_ERROR_PARAM_COUNT; pwm_led_id = atoi(argv[1]); if ((pwm_led_id < 0) || (pwm_led_id >= CONFIG_LED_PWM_COUNT)) return EC_ERROR_PARAM1; led_id = supported_led_ids[pwm_led_id]; if (argc == 2) { ccprintf("PWM LED %d: led_id=%d, auto_control=%d\n", pwm_led_id, led_id, led_auto_control_is_enabled(led_id) != 0); return EC_SUCCESS; } if (!parse_bool(argv[2], &enable)) return EC_ERROR_PARAM2; /* Inverted because this drives auto control. */ led_auto_control(led_id, !enable); if (argc == 4) { /* Set the color. */ if (!strncmp(argv[3], "red", 3)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_RED); else if (!strncmp(argv[3], "green", 5)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_GREEN); else if (!strncmp(argv[3], "amber", 5)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_AMBER); else if (!strncmp(argv[3], "blue", 4)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_BLUE); else if (!strncmp(argv[3], "white", 5)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_WHITE); else if (!strncmp(argv[3], "yellow", 6)) set_pwm_led_color(pwm_led_id, EC_LED_COLOR_YELLOW); else if (!strncmp(argv[3], "off", 3)) set_pwm_led_color(pwm_led_id, -1); else return EC_ERROR_PARAM3; } return EC_SUCCESS; } DECLARE_CONSOLE_COMMAND(ledtest, command_ledtest, " [color|off]", ""); #endif /* defined(CONFIG_CMD_LEDTEST) */