/* 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 /* * 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}; /* List of LED colors used */ enum led_color { LED_OFF = 0, LED_RED, LED_GREEN, LED_BLUE, LED_WHITE, LED_RED_HALF, /* Number of colors, not a color itself */ LED_COLOR_COUNT }; /* List of supported LED patterns */ enum led_pattern { OFF = 0, SOLID_GREEN, WHITE_GREEN, SOLID_WHITE, WHITE_RED, SOLID_RED, PULSE_RED, BLINK_RED, LED_NUM_PATTERNS, }; enum led_side { LED_LEFT = 0, LED_RIGHT, LED_BOTH }; struct led_info { /* LED pattern manage variables */ int ticks; int pattern_sel; int tap_tick_count; enum led_color color; /* Color transition variables */ int state; int step; uint8_t rgb_current[3]; const uint8_t *rgb_target; uint8_t trans[ARRAY_SIZE(trans_steps)]; }; /* * 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]; uint8_t tap_len; }; static int led_debug; static int double_tap; static int led_charge_side; static struct led_info led[LED_BOTH]; 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); /* * 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_OFF, LED_OFF}, {0, 0}, DOUBLE_TAP_TICK_LEN }, { {LED_GREEN, LED_GREEN}, {0, 0}, DOUBLE_TAP_TICK_LEN }, { {LED_WHITE, LED_GREEN}, {2, 4}, DOUBLE_TAP_TICK_LEN }, { {LED_WHITE, LED_WHITE}, {0, 0}, DOUBLE_TAP_TICK_LEN }, { {LED_WHITE, LED_RED}, {2, 4}, DOUBLE_TAP_TICK_LEN }, { {LED_RED, LED_RED}, {0, 0}, DOUBLE_TAP_TICK_LEN}, { {LED_RED, LED_RED_HALF}, {4, 4}, DOUBLE_TAP_TICK_LEN * 2 + DOUBLE_TAP_TICK_LEN / 2}, { {LED_RED, LED_OFF}, {1, 5}, DOUBLE_TAP_TICK_LEN * 3 + DOUBLE_TAP_TICK_LEN / 2}, }; /* * 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_HALF] = {40, 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 battery level <= BLINK_RED level" #endif static const struct range_map pattern_tbl[] = { {CONFIG_USB_PD_TRY_SRC_MIN_BATT_SOC - 1, BLINK_RED}, {5, PULSE_RED}, {15, SOLID_RED}, {25, WHITE_RED}, {75, SOLID_WHITE}, {95, WHITE_GREEN}, {100, SOLID_GREEN}, }; enum led_state_change { LED_STATE_INTENSITY_DOWN, LED_STATE_INTENSITY_UP, LED_STATE_DONE, }; /** * 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_setup_color_change(int old_idx, int new_idx, enum led_side side) { int i; 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. */ led[side].rgb_target = color_brightness[new_idx]; for (i = 0; i < PWM_CHAN_PER_LED; i++) { led[side].rgb_current[i] = color_brightness[old_idx][i]; if (led[side].rgb_current[i] < led[side].rgb_target[i]) { /* increase in color */ increase = 1; } } /* Check to see if increasing or decreasing color */ if (increase) { led[side].state = LED_STATE_INTENSITY_UP; /* First entry of transition table == current level */ led[side].step = 1; } else { /* Last entry of transition table == current level */ led[side].step = ARRAY_SIZE(trans_steps) - 2; led[side].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++) led[side].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 = led[side].rgb_target[rgb_index]; if (led[side].state == LED_STATE_INTENSITY_UP) /* * Increasing in intensity, current level or R/G is * the starting level. */ start_lvl = led[side].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(led[side].rgb_current[rgb_index] - led[side].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++) { led[side].trans[i] = start_lvl + ((step_value + (1 << (LED_FRAC_BITS - 1))) >> LED_FRAC_BITS); step_value += delta_per_step; } } } static void led_adjust_color_step(int side) { int i; int change = 0; uint8_t lvl = led[side].trans[led[side].step]; uint8_t *rgb_c = led[side].rgb_current; const uint8_t *rgb_t = led[side].rgb_target; if (led[side].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_c[i] > rgb_t[i]) && (rgb_c[i] >= lvl)) { rgb_c[i] = MAX(lvl, rgb_t[i]); change = 1; } } /* * If nothing changed this iteration, or if lowest table entry * has been used, then the change is complete. */ if (!change || --led[side].step < 0) led[side].state = LED_STATE_DONE; } else if (led[side].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_c[i] < rgb_t[i]) && (rgb_c[i] <= lvl)) { rgb_c[i] = MIN(lvl, rgb_t[i]); change = 1; } } /* * If nothing changed this iteration, or if highest table entry * has been used, then the change is complete. */ if (!change || ++led[side].step >= ARRAY_SIZE(trans_steps)) led[side].state = LED_STATE_DONE; } /* Apply current R, G, B levels */ set_color(rgb_c, side); } static void led_change_color(void) { int i; /* Will loop here until the color change is complete. */ while (led[LED_LEFT].state != LED_STATE_DONE || led[LED_RIGHT].state != LED_STATE_DONE) { for (i = 0; i < LED_BOTH; i++) { if (led[i].state != LED_STATE_DONE) /* Move one step in the transition table */ led_adjust_color_step(i); } msleep(LED_STEP_MSEC); } } static void led_manage_patterns(enum led_pattern *pattern_desired, int tap) { int color; int phase; int i; int color_change = 0; for (i = 0; i < LED_BOTH; i++) { /* For each led check if the pattern needs to change */ if (pattern_desired[i] != led[i].pattern_sel) { /* * Pattern needs to change, but if double tap sequence * is active, then need to wait until that * completes. Unless the pattern change is due to * external charger state change, make that happen * immediately. */ if (i == led_charge_side || !led[i].tap_tick_count) { led[i].ticks = 0; led[i].tap_tick_count = tap ? pattern[pattern_desired[i]].tap_len : 0; led[i].pattern_sel = pattern_desired[i]; } } /* Determine pattern phase and color for current phase */ phase = led[i].ticks < LED_TICKS_PER_BEAT * pattern[led[i].pattern_sel].len[0] ? 0 : 1; color = pattern[led[i].pattern_sel].color[phase]; /* If color is changing, then setup the transition. */ if (led[i].color != color) { led_setup_color_change(led[i].color, color, i); led[i].color = color; color_change = 1; } } if (color_change) /* Change color is done for both LEDs simultaneously */ led_change_color(); for (i = 0; i < LED_BOTH; i++) { /* Set color for the current phase */ set_color(color_brightness[led[i].color], i); /* * 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[i].pattern_sel].len[0]) if (++led[i].ticks == LED_TICKS_PER_BEAT * (pattern[led[i].pattern_sel].len[0] + pattern[led[i].pattern_sel].len[1])) led[i].ticks = 0; /* If double tap display is active, decrement its counter */ if (led[i].tap_tick_count) led[i].tap_tick_count--; } } static enum led_pattern led_get_double_tap_pattern(int percent_chg) { int i; enum led_pattern pattern = OFF; for (i = 0; i < ARRAY_SIZE(pattern_tbl); i++) { if (percent_chg <= pattern_tbl[i].max) { pattern = pattern_tbl[i].pattern; break; } } return pattern; } static void led_select_pattern(enum led_pattern *pattern_desired, int tap) { enum charge_state chg_state = charge_get_state(); int side; int percent_chg; enum led_pattern new_pattern; /* Get active charge port which maps directly to left/right LED */ side = charge_manager_get_active_charge_port(); /* * Maintain a copy of the side associated with charging. If there is no * active charging port, then charge_side = -1. This value is used to * manage the double_tap tick counts on a per LED basis. */ led_charge_side = side; /* 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 (side == LED_BOTH) { /* * External charger is not connected. Find the pattern that * would be used for double tap event. */ new_pattern = led_get_double_tap_pattern(percent_chg); /* * 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 && new_pattern <= WHITE_RED) new_pattern = OFF; /* * When external charger is not connected, always apply pattern * to both LEDs. */ pattern_desired[LED_LEFT] = new_pattern; pattern_desired[LED_RIGHT] = new_pattern; } else { /* * External charger is connected. First determine pattern for * charging side LED. */ if (chg_state == PWR_STATE_CHARGE_NEAR_FULL || ((chg_state == PWR_STATE_DISCHARGE_FULL) && extpower_is_present())) { new_pattern = SOLID_GREEN; } else if (chg_state == PWR_STATE_CHARGE) { new_pattern = SOLID_WHITE; } else { new_pattern = OFF; } pattern_desired[side] = new_pattern; /* Check for double tap for side not associated with charger */ new_pattern = led_get_double_tap_pattern(percent_chg); if (!tap && new_pattern != BLINK_RED) new_pattern = OFF; /* Apply this pattern to the non-charging side LED */ pattern_desired[side ^ 1] = new_pattern; } } static void led_init(void) { int i; /* * 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); /* * Initialize LED descriptors. The members that are used for changing * colors don't neet to be initialized as they are always computed * when a color change is required. */ for (i = 0; i < LED_BOTH; i++) { led[i].pattern_sel = OFF; led[i].color = LED_OFF; led[i].ticks = 0; led[i].tap_tick_count = 0; led[i].state = LED_STATE_DONE; } } void led_task(void *u) { uint32_t start_time; uint32_t task_duration; led_init(); usleep(SECOND); while (1) { enum led_pattern pattern_desired[LED_BOTH]; int tap = 0; start_time = get_time().le.lo; 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; } if (led_auto_control_is_enabled(EC_LED_ID_LEFT_LED) && led_auto_control_is_enabled(EC_LED_ID_RIGHT_LED) && led_debug != 1) { /* Determine desired LED patterns for both LEDS */ led_select_pattern(pattern_desired, tap); /* Update LED patterns/colors (if necessary) */ led_manage_patterns(pattern_desired, tap); } /* 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");