/* 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/Battery LED control for Eve */ #include "charge_manager.h" #include "charge_state.h" #include "chipset.h" #include "console.h" #include "extpower.h" #include "gpio.h" #include "hooks.h" #include "led_common.h" #include "pwm.h" #include "math_util.h" #include "registers.h" #include "task.h" #include "util.h" #define CPRINTF(format, args...) cprintf(CC_PWM, format, ## args) #define CPRINTS(format, args...) cprints(CC_PWM, format, ## args) #define LED_TICK_TIME (500 * MSEC) #define LED_TICKS_PER_BEAT 1 #define NUM_PHASE 2 #define DOUBLE_TAP_TICK_LEN (LED_TICKS_PER_BEAT * 8) #define LED_FRAC_BITS 4 #define LED_STEP_MSEC 45 /* List of LED colors used */ enum led_color { LED_OFF = 0, LED_RED, LED_GREEN, LED_BLUE, LED_WHITE, LED_RED_2_3, LED_RED_1_3, /* Number of colors, not a color itself */ LED_COLOR_COUNT }; /* List of supported LED patterns */ enum led_pattern { SOLID_GREEN = 0, WHITE_GREEN, SOLID_WHITE, WHITE_RED, SOLID_RED, PULSE_RED_1, PULSE_RED_2, BLINK_RED, OFF, LED_NUM_PATTERNS, }; enum led_side { LED_LEFT = 0, LED_RIGHT, LED_BOTH }; static int led_debug; static int double_tap; static int double_tap_tick_count; static int led_pattern; static int led_ticks; static enum led_color led_current_color; const enum ec_led_id supported_led_ids[] = { EC_LED_ID_LEFT_LED, EC_LED_ID_RIGHT_LED}; const int supported_led_ids_count = ARRAY_SIZE(supported_led_ids); /* * LED patterns are described as two phases. Each phase has an associated LED * color and length in beats. The length of each beat is defined by the macro * LED_TICKS_PER_BEAT. */ struct led_phase { uint8_t color[NUM_PHASE]; uint8_t len[NUM_PHASE]; }; /* * Pattern table. The len field is beats per color. 0 for len indicates that a * particular pattern never changes from the first phase. */ static const struct led_phase pattern[LED_NUM_PATTERNS] = { { {LED_GREEN, LED_GREEN}, {0, 0} }, { {LED_WHITE, LED_GREEN}, {2, 4} }, { {LED_WHITE, LED_WHITE}, {0, 0} }, { {LED_WHITE, LED_RED}, {2, 4} }, { {LED_RED, LED_RED}, {0, 0} }, { {LED_RED, LED_RED_2_3}, {4, 4} }, { {LED_RED, LED_RED_1_3}, {2, 4} }, { {LED_RED, LED_OFF}, {1, 6} }, { {LED_OFF, LED_OFF}, {0, 0} }, }; /* * Brightness vs. color, in the order of off, red, green and blue. Values are * for % on PWM duty cycle time. */ #define PWM_CHAN_PER_LED 3 static const uint8_t color_brightness[LED_COLOR_COUNT][PWM_CHAN_PER_LED] = { /* {Red, Green, Blue}, */ [LED_OFF] = {0, 0, 0}, [LED_RED] = {80, 0, 0}, [LED_GREEN] = {0, 80, 0}, [LED_BLUE] = {0, 0, 80}, [LED_WHITE] = {100, 100, 100}, [LED_RED_2_3] = {40, 0, 0}, [LED_RED_1_3] = {20, 0, 0}, }; /* * When a double tap event occurs, a LED pattern is displayed based on the * current battery charge level. The LED patterns used for double tap under low * battery conditions are same patterns displayed when the battery is not * charging. The table below shows what battery charge level displays which * pattern. */ struct range_map { uint8_t max; uint8_t pattern; }; #if (CONFIG_USB_PD_TRY_SRC_MIN_BATT_SOC >= 3) #error "LED: PULSE_RED_2 battery level <= BLINK_RED level" #endif static const struct range_map pattern_tbl[] = { {CONFIG_USB_PD_TRY_SRC_MIN_BATT_SOC - 1, BLINK_RED}, {3, PULSE_RED_2}, {9, PULSE_RED_1}, {14, SOLID_RED}, {29, WHITE_RED}, {89, SOLID_WHITE}, {97, WHITE_GREEN}, {100, SOLID_GREEN}, }; enum led_state_change { LED_STATE_INTENSITY_DOWN, LED_STATE_INTENSITY_UP, LED_STATE_DONE, }; /* * The PWM % on levels to transition from intensity 0 (black) to intensity 1.0 * (white) in the HSI color space converted back to RGB space (0 - 255) and * converted to a % for PWM. This table is used for Red <--> White and Green * <--> Transitions. In HSI space white = (0, 0, 1), red = (0, .5, .33), green = * (120, .5, .33). For the transitions of interest only S and I are changed and * they are changed linearly in HSI space. */ static const uint8_t trans_steps[] = {0, 4, 9, 16, 24, 33, 44, 56, 69, 84, 100}; /** * Set LED color * * @param pwm Pointer to 3 element RGB color level (0 -> 100) * @param side Left LED, Right LED, or both LEDs */ static void set_color(const uint8_t *pwm, enum led_side side) { int i; static uint8_t saved_duty[LED_BOTH][PWM_CHAN_PER_LED]; /* Set color for left LED */ if (side == LED_LEFT || side == LED_BOTH) { for (i = 0; i < PWM_CHAN_PER_LED; i++) { if (saved_duty[LED_LEFT][i] != pwm[i]) { pwm_set_duty(PWM_CH_LED_L_RED + i, 100 - pwm[i]); saved_duty[LED_LEFT][i] = pwm[i]; } } } /* Set color for right LED */ if (side == LED_RIGHT || side == LED_BOTH) { for (i = 0; i < PWM_CHAN_PER_LED; i++) { if (saved_duty[LED_RIGHT][i] != pwm[i]) { pwm_set_duty(PWM_CH_LED_R_RED + i, 100 - pwm[i]); saved_duty[LED_RIGHT][i] = pwm[i]; } } } } void led_get_brightness_range(enum ec_led_id led_id, uint8_t *brightness_range) { brightness_range[EC_LED_COLOR_RED] = 100; brightness_range[EC_LED_COLOR_BLUE] = 100; brightness_range[EC_LED_COLOR_GREEN] = 100; } int led_set_brightness(enum ec_led_id led_id, const uint8_t *brightness) { switch (led_id) { case EC_LED_ID_LEFT_LED: /* Set brightness for left LED */ pwm_set_duty(PWM_CH_LED_L_RED, 100 - brightness[EC_LED_COLOR_RED]); pwm_set_duty(PWM_CH_LED_L_BLUE, 100 - brightness[EC_LED_COLOR_BLUE]); pwm_set_duty(PWM_CH_LED_L_GREEN, 100 - brightness[EC_LED_COLOR_GREEN]); break; case EC_LED_ID_RIGHT_LED: /* Set brightness for right LED */ pwm_set_duty(PWM_CH_LED_R_RED, 100 - brightness[EC_LED_COLOR_RED]); pwm_set_duty(PWM_CH_LED_R_BLUE, 100 - brightness[EC_LED_COLOR_BLUE]); pwm_set_duty(PWM_CH_LED_R_GREEN, 100 - brightness[EC_LED_COLOR_GREEN]); break; default: return EC_ERROR_UNKNOWN; } return EC_SUCCESS; } void led_register_double_tap(void) { double_tap = 1; } static void led_change_color(int old_idx, int new_idx, enum led_side side) { int i; int step; int state; uint8_t rgb_current[3]; const uint8_t *rgb_target; uint8_t trans[ARRAY_SIZE(trans_steps)]; int increase = 0; /* * Using the color indices, poplulate the current and target R, G, B * arrays. The arrays are indexed R = 0, G = 1, B = 2. If the target of * any of the 3 is greater than the current, then this color change is * an increase in intensity. Otherwise, it's a decrease. */ rgb_target = color_brightness[new_idx]; for (i = 0; i < PWM_CHAN_PER_LED; i++) { rgb_current[i] = color_brightness[old_idx][i]; if (rgb_current[i] < rgb_target[i]) { /* increase in color */ increase = 1; } } /* Check to see if increasing or decreasing color */ if (increase) { state = LED_STATE_INTENSITY_UP; /* First entry of transition table == current level */ step = 1; } else { /* Last entry of transition table == current level */ step = ARRAY_SIZE(trans_steps) - 2; state = LED_STATE_INTENSITY_DOWN; } /* * Populate transition table based on the number of R, G, B components * changing. If only 1 componenet is changing, then can just do linear * steps over the range. If more than 1 component is changing, then * this is a white <--> color transition and will use * the precomputed steps which are derived by converting to HSI space * and then linearly transitioning S and I to go from the starting color * to white and vice versa. */ if (old_idx == LED_WHITE || new_idx == LED_WHITE) { for (i = 0; i < ARRAY_SIZE(trans_steps); i++) trans[i] = trans_steps[i]; } else { int delta_per_step; int step_value; int start_lvl; int total_change; /* Assume that the R component (index = 0) is changing */ int rgb_index = 0; /* * Since the new or old color is not white, then this change * must involve only either red or green. There are no red <--> * green transitions. So only 1 color is being changed in this * case. Assume it's red (index = 0), but check if it's green * (index = 1). */ if (old_idx == LED_GREEN || new_idx == LED_GREEN) rgb_index = 1; /* * Determine the total change assuming current level is higher * than target level. The transitions steps are always ordered * lower to higher. The starting index is adjusted if intensity * is decreasing. */ start_lvl = rgb_target[rgb_index]; if (state == LED_STATE_INTENSITY_UP) /* * Increasing in intensity, current level or R/G is * the starting level. */ start_lvl = rgb_current[rgb_index]; /* * Compute change per step using fractional bits. The step * change accumulates fractional bits and is truncated after * rounding before being added to the starting value. */ total_change = ABS(rgb_current[rgb_index] - rgb_target[rgb_index]); delta_per_step = (total_change << LED_FRAC_BITS) / (ARRAY_SIZE(trans_steps) - 1); step_value = 0; for (i = 0; i < ARRAY_SIZE(trans_steps); i++) { trans[i] = start_lvl + ((step_value + (1 << (LED_FRAC_BITS - 1))) >> LED_FRAC_BITS); step_value += delta_per_step; } } /* Will loop here until the color change is complete. */ while (state != LED_STATE_DONE) { int change = 0; if (state == LED_STATE_INTENSITY_DOWN) { /* * Colors are going from higher to lower level. If the * current level of R, G, or B is higher than both * the next step in the transition table and and the * target level, then move to the larger of the two. The * MAX is used to make sure that it doens't drop below * the target level. */ for (i = 0; i < PWM_CHAN_PER_LED; i++) { if ((rgb_current[i] > rgb_target[i]) && (rgb_current[i] >= trans[step])) { rgb_current[i] = MAX(trans[step], rgb_target[i]); change = 1; } } /* * If nothing changed this iteration, or if lowest table * entry has been used, then the change is complete. */ if (!change || --step < 0) state = LED_STATE_DONE; } else if (state == LED_STATE_INTENSITY_UP) { /* * Colors are going from lower to higher level. If the * current level of R, G, B is lower than both the * target level and the transition table entry for a * given color, then move up to the MIN of next * transition step and target level. */ for (i = 0; i < PWM_CHAN_PER_LED; i++) { if ((rgb_current[i] < rgb_target[i]) && (rgb_current[i] <= trans[step])) { rgb_current[i] = MIN(trans[step], rgb_target[i]); change = 1; } } /* * If nothing changed this iteration, or if highest * table entry has been used, then the change is * complete. */ if (!change || ++step >= ARRAY_SIZE(trans_steps)) state = LED_STATE_DONE; } /* Apply current R, G, B levels */ set_color(rgb_current, side); msleep(LED_STEP_MSEC); } } static void led_manage_pattern(int side) { int color; int phase; /* Determine pattern phase */ phase = led_ticks < LED_TICKS_PER_BEAT * pattern[led_pattern].len[0] ? 0 : 1; color = pattern[led_pattern].color[phase]; /* If color is changing, then manage the transition */ if (led_current_color != color) { led_change_color(led_current_color, color, side); led_current_color = color; } /* Set color for the current phase */ set_color(color_brightness[color], side); /* * Update led_ticks. If the len field is 0, then the pattern * being used is just one color so no need to increase the tick count. */ if (pattern[led_pattern].len[0]) if (++led_ticks == LED_TICKS_PER_BEAT * (pattern[led_pattern].len[0] + pattern[led_pattern].len[1])) led_ticks = 0; /* If double tap display is active, decrement its counter */ if (double_tap_tick_count) double_tap_tick_count--; } static void eve_led_set_power_battery(void) { enum charge_state chg_state = charge_get_state(); int side; int percent_chg; enum led_pattern pattern = led_pattern; int tap = 0; if (double_tap) { /* Clear double tap indication */ if (!chipset_in_state(CHIPSET_STATE_ON)) /* If not in S0, then set tap on */ tap = 1; double_tap = 0; } /* Get active charge port which maps directly to left/right LED */ side = charge_manager_get_active_charge_port(); /* Ensure that side can be safely used as an index */ if (side < 0 || side >= CONFIG_USB_PD_PORT_MAX_COUNT) side = LED_BOTH; /* Get percent charge */ percent_chg = charge_get_percent(); if (chg_state == PWR_STATE_CHARGE_NEAR_FULL || ((chg_state == PWR_STATE_DISCHARGE_FULL) && extpower_is_present())) { pattern = SOLID_GREEN; double_tap_tick_count = 0; } else if (chg_state == PWR_STATE_CHARGE) { pattern = SOLID_WHITE; double_tap_tick_count = 0; } else if (!double_tap_tick_count) { int i; /* * Not currently charging. Select the pattern based on * the battery charge level. If there is no double tap * event to process, then only the low battery patterns * are relevant. */ for (i = 0; i < ARRAY_SIZE(pattern_tbl); i++) { if (percent_chg <= pattern_tbl[i].max) { pattern = pattern_tbl[i].pattern; break; } } /* * The patterns used for double tap and for not charging * state are the same for low battery cases. But, if * battery charge is high enough to be above SOLID_RED, * then only display LED pattern if double tap has * occurred. */ if (tap == 0 && pattern <= WHITE_RED) pattern = OFF; else /* Start double tap LED sequence */ double_tap_tick_count = DOUBLE_TAP_TICK_LEN; } /* If the LED pattern will change, then reset tick count and set * new pattern. */ if (pattern != led_pattern) { led_ticks = 0; led_pattern = pattern; } /* * If external charger is connected, then make sure only the LED that's * on the side with the charger is turned on. */ if (side != LED_BOTH) set_color(color_brightness[LED_OFF], side ^ 1); /* Update LED pattern */ led_manage_pattern(side); } static void led_init(void) { /* * Enable PWMs and set to 0% duty cycle. If they're disabled, * seems to ground the pins instead of letting them float. */ /* Initialize PWM channels for left LED */ pwm_enable(PWM_CH_LED_L_RED, 1); pwm_enable(PWM_CH_LED_L_GREEN, 1); pwm_enable(PWM_CH_LED_L_BLUE, 1); /* Initialize PWM channels for right LED */ pwm_enable(PWM_CH_LED_R_RED, 1); pwm_enable(PWM_CH_LED_R_GREEN, 1); pwm_enable(PWM_CH_LED_R_BLUE, 1); set_color(color_brightness[LED_OFF], LED_BOTH); led_pattern = OFF; led_ticks = 0; double_tap_tick_count = 0; } /* After pwm_pin_init() */ DECLARE_HOOK(HOOK_INIT, led_init, HOOK_PRIO_DEFAULT); void led_task(void *u) { uint32_t start_time; uint32_t task_duration; while (1) { start_time = get_time().le.lo; if (led_auto_control_is_enabled(EC_LED_ID_LEFT_LED) && led_auto_control_is_enabled(EC_LED_ID_RIGHT_LED) && led_debug != 1) { eve_led_set_power_battery(); } /* Compute time for this iteration */ task_duration = get_time().le.lo - start_time; /* * Compute wait time required to for next desired LED tick. If * the duration exceeds the tick time, then don't sleep. */ if (task_duration < LED_TICK_TIME) usleep(LED_TICK_TIME - task_duration); } } /******************************************************************/ /* Console commands */ static int command_led(int argc, char **argv) { int side = LED_BOTH; char *e; enum led_color color; if (argc > 1) { if (argc > 2) { side = strtoi(argv[2], &e, 10); if (*e) return EC_ERROR_PARAM2; if (side > 1) return EC_ERROR_PARAM2; } if (!strcasecmp(argv[1], "debug")) { led_debug ^= 1; CPRINTF("led_debug = %d\n", led_debug); return EC_SUCCESS; } if (!strcasecmp(argv[1], "off")) color = LED_OFF; else if (!strcasecmp(argv[1], "red")) color = LED_RED; else if (!strcasecmp(argv[1], "green")) color = LED_GREEN; else if (!strcasecmp(argv[1], "blue")) color = LED_BLUE; else if (!strcasecmp(argv[1], "white")) color = LED_WHITE; else return EC_ERROR_PARAM1; set_color(color_brightness[color], side); } return EC_SUCCESS; } DECLARE_CONSOLE_COMMAND(led, command_led, "[debug|red|green|blue|white|amber|off <0|1>]", "Change LED color");