coreboot-libre-fam15h-rdimm/3rdparty/chromeec/board/nami/led.c

614 lines
16 KiB
C
Raw Normal View History

2024-03-04 11:14:53 +01:00
/* 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 and battery LED control for Nami and its variants
*
* This is an event-driven LED control library. It does not use tasks or
* periodical hooks (HOOK_TICK, HOOK_SECOND), thus, it's more resource
* efficient.
*
* The library defines LED states and assigns an LED behavior to each state.
* The state space consists of tuple of (charge state, power state).
* In each LED state, a color and a pulse interval can be defined.
*
* Charging states are queried each time there is a state transition, thus, not
* stored. We hook power state transitions (e.g. s0->s3) and save the
* destination states (e.g. s3) in power_state.
*
* When system is suspending and AC is unplugged, there will be race condition
* between a power state hook and a charge state hook but whichever is called
* first or last the result will be the same.
*
* Currently, it supports two LEDs, called 'battery LED' and 'power LED'.
* It assumes the battery LED is connected to a PWM pin and the power LED is
* connected to a regular GPIO pin.
*/
#include "cros_board_info.h"
#include "charge_state.h"
#include "chipset.h"
#include "console.h"
#include "ec_commands.h"
#include "gpio.h"
#include "hooks.h"
#include "led_common.h"
#include "power.h"
#include "pwm.h"
#include "timer.h"
#include "util.h"
const enum ec_led_id supported_led_ids[] = {
EC_LED_ID_BATTERY_LED, EC_LED_ID_POWER_LED};
const int supported_led_ids_count = ARRAY_SIZE(supported_led_ids);
enum led_color {
LED_OFF = 0,
LED_RED,
LED_GREEN,
LED_AMBER,
LED_WHITE,
LED_WARM_WHITE,
LED_FACTORY,
/* Number of colors, not a color itself */
LED_COLOR_COUNT
};
/* Charging states of LED's interests */
enum led_charge_state {
LED_STATE_DISCHARGE = 0,
LED_STATE_CHARGE,
LED_STATE_FULL,
LED_CHARGE_STATE_COUNT,
};
/* Power states of LED's interests */
enum led_power_state {
LED_STATE_S0 = 0,
LED_STATE_S3,
LED_STATE_S5,
LED_POWER_STATE_COUNT,
};
/* Defines a LED pattern for a single state */
struct led_pattern {
uint8_t color;
/* Bit 0-5: Interval in 100 msec. 0=solid. Max is 3.2 sec.
* Bit 6: 1=alternate (on-off-off-off), 0=regular (on-off-on-off)
* Bit 7: 1=pulse, 0=blink */
uint8_t pulse;
};
#define PULSE_NO 0
#define PULSE(interval) (BIT(7) | (interval))
#define BLINK(interval) (interval)
#define ALTERNATE(interval) (BIT(6) | (interval))
#define IS_PULSING(pulse) ((pulse) & 0x80)
#define IS_ALTERNATE(pulse) ((pulse) & 0x40)
#define PULSE_INTERVAL(pulse) (((pulse) & 0x3f) * 100 * MSEC)
/* 40 msec for nice and smooth transition. */
#define LED_PULSE_TICK_US (40 * MSEC)
typedef struct led_pattern led_patterns[LED_CHARGE_STATE_COUNT]
[LED_POWER_STATE_COUNT];
/*
* Nami/Vayne - One dual color LED:
* Charging Amber on (S0/S3/S5)
* Charging (full) White on (S0/S3/S5)
* Discharge in S0 White on
* Discharge in S3/S0ix Pulsing (rising for 2 sec , falling for 2 sec)
* Discharge in S5 Off
* Battery Error Amber on 1sec off 1sec
* Factory mode White on 2sec, Amber on 2sec
*/
const static led_patterns battery_pattern_0 = {
/* discharging: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE(10)}, {LED_OFF, PULSE_NO}},
/* charging: s0, s3, s5 */
{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};
/*
* Sona - Battery LED (dual color)
*/
const static led_patterns battery_pattern_1 = {
/* discharging: s0, s3, s5 */
{{LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}},
/* charging: s0, s3, s5 */
{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};
/*
* Pantheon - AC In/Battery LED(dual color):
* Connected to AC power / Charged (100%) White (solid on)
* Connected to AC power / Charging(1% -99%) Amber (solid on)
* Not connected to AC power Off
*/
const static led_patterns battery_pattern_2 = {
/* discharging: s0, s3, s5 */
{{LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}, {LED_OFF, PULSE_NO}},
/* charging: s0, s3, s5 */
{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};
/*
* Sona - Power LED (single color)
*/
const static led_patterns power_pattern_1 = {
/* discharging: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
/* charging: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
};
/*
* Pantheon - Power LED
* S0: White on
* S3/S0ix: White 1 second on, 3 second off
* S5: Off
*/
const static led_patterns power_pattern_2 = {
/* discharging: s0, s3, s5 */
{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
/* charging: s0, s3, s5 */
{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
/* full: s0, s3, s5 */
{{LED_WHITE, 0}, {LED_WHITE, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
};
/*
* Akali - battery LED
* Charge: Amber on (s0/s3/s5)
* Full: Blue on (s0/s3/s5)
* Discharge in S0: Blue on
* Discharge in S3: Amber on 1 sec off 3 sec
* Discharge in S5: Off
* Battery Error: Amber on 1sec off 1sec
* Factory mode : Blue on 2sec, Amber on 2sec
*/
const static led_patterns battery_pattern_3 = {
/* discharging: s0, s3, s5 */
{{LED_WHITE, 0}, {LED_AMBER, ALTERNATE(BLINK(10))}, {LED_OFF, 0}},
/* charging: s0, s3, s5 */
{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};
const static led_patterns battery_pattern_4 = {
/* discharging: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, BLINK(10)}, {LED_OFF, PULSE_NO}},
/* charging: s0, s3, s5 */
{{LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}, {LED_AMBER, PULSE_NO}},
/* full: s0, s3, s5 */
{{LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}, {LED_WHITE, PULSE_NO}},
};
/* Patterns for battery LED and power LED. Initialized at run-time. */
static led_patterns const *patterns[2];
/* Pattern for battery error. Only blinking battery LED is supported. */
static struct led_pattern battery_error = {LED_AMBER, BLINK(10)};
/* Pattern for low state of charge. Only battery LED is supported. */
static struct led_pattern low_battery = {LED_WHITE, BLINK(10)};
/* Pattern for factory mode. Blinking 2-color battery LED. */
static struct led_pattern battery_factory = {LED_FACTORY, BLINK(20)};
static int low_battery_soc;
static void led_charge_hook(void);
static enum led_power_state power_state;
static void led_init(void)
{
switch (oem) {
case PROJECT_NAMI:
case PROJECT_VAYNE:
patterns[0] = &battery_pattern_0;
break;
case PROJECT_SONA:
if (model == MODEL_SYNDRA) {
/* Syndra doesn't have power LED */
patterns[0] = &battery_pattern_4;
} else {
patterns[0] = &battery_pattern_1;
patterns[1] = &power_pattern_1;
}
battery_error.pulse = BLINK(5);
low_battery_soc = 100; /* 10.0% */
break;
case PROJECT_PANTHEON:
patterns[0] = &battery_pattern_2;
patterns[1] = &power_pattern_2;
battery_error.color = LED_OFF;
battery_error.pulse = 0;
break;
case PROJECT_AKALI:
patterns[0] = &battery_pattern_3;
break;
default:
break;
}
pwm_enable(PWM_CH_LED1, 1);
pwm_enable(PWM_CH_LED2, 1);
/* After sysjump, power_state is cleared. Thus, we need to actively
* retrieve it. */
if (chipset_in_state(CHIPSET_STATE_ANY_OFF))
power_state = LED_STATE_S5;
else if (chipset_in_state(CHIPSET_STATE_ANY_SUSPEND))
power_state = LED_STATE_S3;
else
power_state = LED_STATE_S0;
}
DECLARE_HOOK(HOOK_INIT, led_init, HOOK_PRIO_DEFAULT);
static int set_color_battery(enum led_color color, int duty)
{
int led1 = 0;
int led2 = 0;
if (duty < 0 || 100 < duty)
return EC_ERROR_UNKNOWN;
switch (color) {
case LED_OFF:
break;
case LED_AMBER:
led2 = 1;
break;
case LED_WHITE:
led1 = 1;
break;
case LED_WARM_WHITE:
led1 = 1;
led2 = 1;
break;
case LED_FACTORY:
break;
default:
return EC_ERROR_UNKNOWN;
}
if (color != LED_FACTORY) {
pwm_set_duty(PWM_CH_LED1, led1 ? duty : 0);
pwm_set_duty(PWM_CH_LED2, led2 ? duty : 0);
} else {
pwm_set_duty(PWM_CH_LED1, duty ? 100 : 0);
pwm_set_duty(PWM_CH_LED2, duty ? 0 : 100);
}
return EC_SUCCESS;
}
static int set_color_power(enum led_color color, int duty)
{
if (color == LED_OFF)
duty = 0;
gpio_set_level(GPIO_LED1, !duty /* Reversed logic */);
return EC_SUCCESS;
}
static int set_color(enum ec_led_id id, enum led_color color, int duty)
{
switch (id) {
case EC_LED_ID_BATTERY_LED:
return set_color_battery(color, duty);
case EC_LED_ID_POWER_LED:
return set_color_power(color, duty);
default:
return EC_ERROR_UNKNOWN;
}
}
static struct {
uint32_t interval;
int duty_inc;
enum led_color color;
int duty;
int alternate;
uint8_t pulse;
} tick[2];
static void tick_battery(void);
DECLARE_DEFERRED(tick_battery);
static void tick_power(void);
DECLARE_DEFERRED(tick_power);
static void cancel_tick(enum ec_led_id id)
{
if (id == EC_LED_ID_BATTERY_LED)
hook_call_deferred(&tick_battery_data, -1);
else
hook_call_deferred(&tick_power_data, -1);
}
static int config_tick(enum ec_led_id id, const struct led_pattern *pattern)
{
static const struct led_pattern *patterns[2];
uint32_t stride;
if (pattern == patterns[id])
/* This pattern was already set */
return -1;
patterns[id] = pattern;
if (!pattern->pulse) {
/* This is a steady pattern. cancel the tick */
cancel_tick(id);
set_color(id, pattern->color, 100);
return 1;
}
stride = PULSE_INTERVAL(pattern->pulse);
if (IS_PULSING(pattern->pulse)) {
tick[id].interval = LED_PULSE_TICK_US;
tick[id].duty_inc = 100 / (stride / LED_PULSE_TICK_US);
} else {
tick[id].interval = stride;
tick[id].duty_inc = 100;
}
tick[id].color = pattern->color;
tick[id].duty = 0;
tick[id].alternate = 0;
tick[id].pulse = pattern->pulse;
return 0;
}
/*
* When pulsing, brightness is incremented by <duty_inc> every <interval> usec
* from 0 to 100%. Then it's decremented from 100% to 0.
*/
static void pulse_led(enum ec_led_id id)
{
if (tick[id].duty + tick[id].duty_inc > 100) {
tick[id].duty_inc = tick[id].duty_inc * -1;
} else if (tick[id].duty + tick[id].duty_inc < 0) {
if (IS_ALTERNATE(tick[id].pulse)) {
/* Falling phase landing. Flip the alternate flag. */
tick[id].alternate = !tick[id].alternate;
if (tick[id].alternate)
return;
}
tick[id].duty_inc = tick[id].duty_inc * -1;
}
tick[id].duty += tick[id].duty_inc;
set_color(id, tick[id].color, tick[id].duty);
}
static uint32_t tick_led(enum ec_led_id id)
{
uint32_t elapsed;
uint32_t start = get_time().le.lo;
uint32_t next;
if (led_auto_control_is_enabled(id))
pulse_led(id);
if (tick[id].alternate)
/* Skip 2 phases (rising & falling) */
next = PULSE_INTERVAL(tick[id].pulse) * 2;
else
next = tick[id].interval;
elapsed = get_time().le.lo - start;
return next > elapsed ? next - elapsed : 0;
}
static void tick_battery(void)
{
hook_call_deferred(&tick_battery_data, tick_led(EC_LED_ID_BATTERY_LED));
}
static void tick_power(void)
{
hook_call_deferred(&tick_power_data, tick_led(EC_LED_ID_POWER_LED));
}
static void start_tick(enum ec_led_id id, const struct led_pattern *pattern)
{
if (config_tick(id, pattern))
/*
* If this pattern is already active, ticking must have started
* already. So, we don't re-start ticking to prevent LED from
* blinking at every SOC change.
*
* If this pattern is static, we skip ticking as well.
*/
return;
if (id == EC_LED_ID_BATTERY_LED)
tick_battery();
else
tick_power();
}
static void led_alert(int enable)
{
if (enable)
start_tick(EC_LED_ID_BATTERY_LED, &battery_error);
else
led_charge_hook();
}
static void led_factory(int enable)
{
if (enable)
start_tick(EC_LED_ID_BATTERY_LED, &battery_factory);
else
led_charge_hook();
}
void config_led(enum ec_led_id id, enum led_charge_state charge)
{
const led_patterns *pattern;
pattern = patterns[id];
if (!pattern)
return; /* This LED isn't present */
start_tick(id, &(*pattern)[charge][power_state]);
}
void config_leds(enum led_charge_state charge)
{
config_led(EC_LED_ID_BATTERY_LED, charge);
config_led(EC_LED_ID_POWER_LED, charge);
}
static void call_handler(void)
{
int soc;
enum charge_state cs;
if (!led_auto_control_is_enabled(EC_LED_ID_BATTERY_LED))
return;
cs = charge_get_state();
soc = charge_get_display_charge();
if (soc < 0)
cs = PWR_STATE_ERROR;
switch (cs) {
case PWR_STATE_DISCHARGE:
case PWR_STATE_DISCHARGE_FULL:
if (soc < low_battery_soc)
start_tick(EC_LED_ID_BATTERY_LED, &low_battery);
else
config_led(EC_LED_ID_BATTERY_LED, LED_STATE_DISCHARGE);
config_led(EC_LED_ID_POWER_LED, LED_STATE_DISCHARGE);
break;
case PWR_STATE_CHARGE_NEAR_FULL:
case PWR_STATE_CHARGE:
if (soc >= 1000)
config_leds(LED_STATE_FULL);
else
config_leds(LED_STATE_CHARGE);
break;
case PWR_STATE_ERROR:
/* It doesn't matter what 'charge' state we pass because power
* LED (if it exists) is orthogonal to battery state. */
config_led(EC_LED_ID_POWER_LED, 0);
led_alert(1);
break;
case PWR_STATE_IDLE:
/* External power connected in IDLE. This is also used to show
* factory mode when 'ectool chargecontrol idle' is run during
* factory process. */
if (charge_get_flags() & CHARGE_FLAG_FORCE_IDLE)
led_factory(1);
break;
default:
;
}
}
/* LED state transition handlers */
static void s0(void)
{
power_state = LED_STATE_S0;
call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_RESUME, s0, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_STARTUP, s0, HOOK_PRIO_DEFAULT);
static void s3(void)
{
power_state = LED_STATE_S3;
call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, s3, HOOK_PRIO_DEFAULT);
static void s5(void)
{
power_state = LED_STATE_S5;
call_handler();
}
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, s5, HOOK_PRIO_DEFAULT);
static void led_charge_hook(void)
{
call_handler();
}
DECLARE_HOOK(HOOK_BATTERY_SOC_CHANGE, led_charge_hook, HOOK_PRIO_DEFAULT);
static void print_config(enum ec_led_id id)
{
ccprintf("ID:%d\n", id);
ccprintf(" Color:%d\n", tick[id].color);
ccprintf(" Duty:%d\n", tick[id].duty);
ccprintf(" Duty Increment:%d\n", tick[id].duty_inc);
ccprintf(" Interval:%d\n", tick[id].interval);
}
static int command_led(int argc, char **argv)
{
enum ec_led_id id = EC_LED_ID_BATTERY_LED;
static int alert = 0;
static int factory;
if (argc < 2)
return EC_ERROR_PARAM_COUNT;
if (!strcasecmp(argv[1], "debug")) {
led_auto_control(id, !led_auto_control_is_enabled(id));
ccprintf("o%s\n", led_auto_control_is_enabled(id) ? "ff" : "n");
} else if (!strcasecmp(argv[1], "off")) {
set_color(id, LED_OFF, 0);
} else if (!strcasecmp(argv[1], "red")) {
set_color(id, LED_RED, 100);
} else if (!strcasecmp(argv[1], "white")) {
set_color(id, LED_WHITE, 100);
} else if (!strcasecmp(argv[1], "amber")) {
set_color(id, LED_AMBER, 100);
} else if (!strcasecmp(argv[1], "alert")) {
alert = !alert;
led_alert(alert);
} else if (!strcasecmp(argv[1], "s0")) {
s0();
} else if (!strcasecmp(argv[1], "s3")) {
s3();
} else if (!strcasecmp(argv[1], "s5")) {
s5();
} else if (!strcasecmp(argv[1], "conf")) {
print_config(id);
} else if (!strcasecmp(argv[1], "factory")) {
factory = !factory;
led_factory(factory);
} else {
return EC_ERROR_PARAM1;
}
return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(led, command_led,
"[debug|red|green|amber|off|alert|s0|s3|s5|conf|factory]",
"Turn on/off LED.");
void led_get_brightness_range(enum ec_led_id led_id, uint8_t *brightness_range)
{
/*
* We return amber=100, white=100 regardless of OEM ID or led_id. This
* function is for ectool led command, which is used to test LED
* functionality.
*/
brightness_range[EC_LED_COLOR_AMBER] = 100;
brightness_range[EC_LED_COLOR_WHITE] = 100;
}
int led_set_brightness(enum ec_led_id id, const uint8_t *brightness)
{
if (brightness[EC_LED_COLOR_AMBER])
return set_color(id, LED_AMBER, brightness[EC_LED_COLOR_AMBER]);
else if (brightness[EC_LED_COLOR_WHITE])
return set_color(id, LED_WHITE, brightness[EC_LED_COLOR_WHITE]);
else
return set_color(id, LED_OFF, 0);
}