594 lines
16 KiB
C
594 lines
16 KiB
C
/* 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_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");
|