coreboot-libre-fam15h-rdimm/3rdparty/chromeec/board/eve/led.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");