blob: 4cab66b8eef441836efdb1385b44c981d9ad24d8 [file]
/* Copyright 2019 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "config.h"
#include "console.h"
#include "ec_commands.h"
#include "hooks.h"
#include "host_command.h"
#include "lpc.h"
#include "power.h"
#include "system.h"
#include "task.h"
#include "timer.h"
#include "util.h"
/* Console output macros */
#define CPRINTS(format, args...) cprints(CC_CHIPSET, format, ##args)
#define CPRINTF(format, args...) cprintf(CC_CHIPSET, format, ##args)
/* Track last reported sleep event */
static enum host_sleep_event host_sleep_state;
#define SYSRQ_WAIT_MSEC 50
/* LCOV_EXCL_START */
/* Function stub that has no behavior, so ignoring for coverage */
__overridable void
power_chipset_handle_host_sleep_event(enum host_sleep_event state,
struct host_sleep_event_context *ctx)
{
/* Default weak implementation -- no action required. */
}
/* LCOV_EXCL_STOP */
static enum ec_status
host_command_host_sleep_event(struct host_cmd_handler_args *args)
{
const struct ec_params_host_sleep_event_v1 *p = args->params;
struct ec_response_host_sleep_event_v1 *r = args->response;
struct host_sleep_event_context ctx;
enum host_sleep_event state = p->sleep_event;
/*
* Treat a reboot after suspend as a resume for notification purposes
* (see b/273327518 for more details)
*/
if (host_sleep_state == HOST_SLEEP_EVENT_S0IX_SUSPEND && state == 0)
state = HOST_SLEEP_EVENT_S0IX_RESUME;
host_sleep_state = state;
ctx.sleep_transitions = 0;
switch (state) {
case HOST_SLEEP_EVENT_S0IX_SUSPEND:
case HOST_SLEEP_EVENT_S3_SUSPEND:
case HOST_SLEEP_EVENT_S3_WAKEABLE_SUSPEND:
ctx.sleep_timeout_ms = EC_HOST_SLEEP_TIMEOUT_DEFAULT;
/* The original version contained only state. */
if (args->version >= 1)
ctx.sleep_timeout_ms =
p->suspend_params.sleep_timeout_ms;
break;
default:
break;
}
power_chipset_handle_host_sleep_event(host_sleep_state, &ctx);
switch (state) {
case HOST_SLEEP_EVENT_S0IX_RESUME:
case HOST_SLEEP_EVENT_S3_RESUME:
if (args->version >= 1) {
r->resume_response.sleep_transitions =
ctx.sleep_transitions;
args->response_size = sizeof(*r);
}
break;
default:
break;
}
return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_HOST_SLEEP_EVENT, host_command_host_sleep_event,
EC_VER_MASK(0) | EC_VER_MASK(1));
enum host_sleep_event power_get_host_sleep_state(void)
{
return host_sleep_state;
}
void power_set_host_sleep_state(enum host_sleep_event state)
{
host_sleep_state = state;
}
/* Flag to notify listeners about suspend/resume events. */
static enum sleep_notify_type sleep_notify = SLEEP_NOTIFY_NONE;
/*
* Note: the following sleep_ functions do not get called in the S3 path on
* Intel devices. On Intel devices, they are called in the S0ix path.
*/
void sleep_set_notify(enum sleep_notify_type notify)
{
sleep_notify = notify;
}
void sleep_notify_transition(enum sleep_notify_type check_state, int hook_id)
{
if (sleep_notify != check_state)
return;
hook_notify(hook_id);
sleep_set_notify(SLEEP_NOTIFY_NONE);
}
#ifdef CONFIG_POWERSEQ_S0IX_COUNTER
atomic_t s0ix_counter;
static void handle_chipset_suspend(void)
{
atomic_inc(&s0ix_counter);
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, handle_chipset_suspend, HOOK_PRIO_LAST);
#endif /* CONFIG_POWERSEQ_S0IX_COUNTER */
/**
* Sleep Hang Recovery Routines.
*
* Only runs in RW to de-risk an unrecoverable boot loop in RO.
*/
#if defined(SECTION_IS_RW) && defined(CONFIG_POWER_SLEEP_FAILURE_DETECTION)
static uint16_t sleep_signal_timeout;
/* Non-const because it may be set by sleeptimeout console cmd */
static uint16_t host_sleep_timeout_default = CONFIG_SLEEP_TIMEOUT_MS;
static uint32_t sleep_signal_transitions;
static enum sleep_hang_type timeout_hang_type;
static void sleep_transition_timeout(void);
DECLARE_DEFERRED(sleep_transition_timeout);
/* LCOV_EXCL_START */
/* Function stub that has no behavior, so ignoring for coverage */
__overridable void power_board_handle_sleep_hang(enum sleep_hang_type hang_type)
{
/* Default empty implementation */
}
/* LCOV_EXCL_STOP */
/* LCOV_EXCL_START */
/* Function stub that has no behavior, so ignoring for coverage */
__overridable void
power_chipset_handle_sleep_hang(enum sleep_hang_type hang_type)
{
/* Default empty implementation */
}
/* LCOV_EXCL_STOP */
/* These counters are reset whenever there's a successful resume */
static int soft_sleep_hang_count;
static int hard_sleep_hang_count;
/* Shutdown or reset on hard hang */
static int shutdown_on_hard_hang;
static void board_handle_hard_sleep_hang(void);
DECLARE_DEFERRED(board_handle_hard_sleep_hang);
/**
* Hard hang detection timers are stopped on any suspend, resume, reset or
* shutdown event.
*/
static void stop_hard_hang_timer(void)
{
hook_call_deferred(&board_handle_hard_sleep_hang_data, -1);
}
DECLARE_HOOK(HOOK_CHIPSET_SUSPEND, stop_hard_hang_timer, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_RESUME, stop_hard_hang_timer, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_RESET, stop_hard_hang_timer, HOOK_PRIO_DEFAULT);
DECLARE_HOOK(HOOK_CHIPSET_SHUTDOWN, stop_hard_hang_timer, HOOK_PRIO_DEFAULT);
/**
* Reboot or shutdown when hard sleep hang detected.
* This timer is stopped on suspend, resume, reset or shutdown events.
*/
static void board_handle_hard_sleep_hang(void)
{
hard_sleep_hang_count += 1;
/* Avoid race condition */
stop_hard_hang_timer();
if (shutdown_on_hard_hang) {
ccprints("Very hard S0ix sleep hang detected!!! "
"Shutting down AP now!");
chipset_force_shutdown(CHIPSET_SHUTDOWN_BOARD_CUSTOM);
return;
}
if (IS_ENABLED(CONFIG_EMULATED_SYSRQ) && (hard_sleep_hang_count == 1)) {
/*
* Send SysRq event to generate a kernel panic. If
* the AP is already in the kernel, the intent is that
* the current CPU stack traces will be output so the
* hang can be debugged. If the CPU is not in the
* kernel, it should be woken, as keyboard input events
* are wake events. Once the AP has booted/woken far
* enough to process the keyboard event, the SysRq
* will generate a panic, which will generate a crash
* report (and the corresponding metrics). A single
* SysRq restarts chrome, while two trigger a kernel
* panic.
*/
CPRINTS("Sending SysRq to trigger AP kernel panic and"
" reboot!");
host_send_sysrq('x');
/*
* Wait a bit so the AP can treat them as separate SysRq
* signals.
*/
crec_msleep(SYSRQ_WAIT_MSEC);
host_send_sysrq('x');
ccprints("AP will be force reset in %dms if hang persists",
CONFIG_HARD_SLEEP_HANG_TIMEOUT);
hook_call_deferred(&board_handle_hard_sleep_hang_data,
CONFIG_HARD_SLEEP_HANG_TIMEOUT * MSEC);
return;
}
ccprints("Consecutive(%d) hard sleep hangs detected!",
hard_sleep_hang_count);
ccprints("Hard S0ix sleep hang detected!! Resetting AP now!");
/* If the AP continues to hang, force a shutdown */
shutdown_on_hard_hang = true;
ccprints("AP will be shutdown in %dms if hang persists",
CONFIG_HARD_SLEEP_HANG_TIMEOUT);
hook_call_deferred(&board_handle_hard_sleep_hang_data,
CONFIG_HARD_SLEEP_HANG_TIMEOUT * MSEC);
chipset_reset(CHIPSET_RESET_HANG_REBOOT);
}
void power_sleep_hang_recovery(enum sleep_hang_type hang_type)
{
soft_sleep_hang_count += 1;
/* Avoid race condition */
stop_hard_hang_timer();
if (hang_type == SLEEP_HANG_S0IX_SUSPEND)
ccprints("S0ix suspend sleep hang detected!");
else if (hang_type == SLEEP_HANG_S0IX_RESUME)
ccprints("S0ix resume sleep hang detected!");
ccprints("Consecutive sleep hang count: soft=%d hard=%d",
soft_sleep_hang_count, hard_sleep_hang_count);
/*
* Start a timer to handle a hard sleep hang, in case the host event
* wake it.
*/
hook_call_deferred(&board_handle_hard_sleep_hang_data,
CONFIG_HARD_SLEEP_HANG_TIMEOUT * MSEC);
/*
* Always send a host event, in case the AP is stuck in FW.
* This will be ignored if the AP is in the OS.
*/
CPRINTS("Warning: Detected sleep hang! Waking host up!");
#ifdef CONFIG_POWER_S0IX
{
host_event_t sleep_wake_mask;
/*
* The S0ix wake mask is not set until the CPU fully suspends
* and enters S0ix, so it must be manually set here to enable
* LPC_HOST_EVENT_WAKE as a wake event before sending the host
* event.
*/
get_lazy_wake_mask(POWER_S0ix, &sleep_wake_mask);
lpc_set_host_event_mask(LPC_HOST_EVENT_WAKE, sleep_wake_mask);
}
#endif
host_set_single_event(EC_HOST_EVENT_HANG_DETECT);
}
/**
* Reset hang counters whenever a resume is successful
*/
static void reset_hang_counters(void)
{
if (hard_sleep_hang_count || soft_sleep_hang_count)
ccprints("Successful S0ix resume after consecutive hangs: "
"soft=%d hard=%d",
soft_sleep_hang_count, hard_sleep_hang_count);
hard_sleep_hang_count = 0;
soft_sleep_hang_count = 0;
shutdown_on_hard_hang = false;
}
DECLARE_HOOK(HOOK_CHIPSET_RESUME, reset_hang_counters, HOOK_PRIO_DEFAULT);
static void sleep_increment_transition(void)
{
if ((sleep_signal_transitions & EC_HOST_RESUME_SLEEP_TRANSITIONS_MASK) <
EC_HOST_RESUME_SLEEP_TRANSITIONS_MASK)
sleep_signal_transitions += 1;
}
K_MUTEX_DEFINE(sleep_transition_timeout_lock);
void sleep_suspend_transition(void)
{
sleep_increment_transition();
mutex_lock(&sleep_transition_timeout_lock);
hook_call_deferred(&sleep_transition_timeout_data, -1);
mutex_unlock(&sleep_transition_timeout_lock);
}
void sleep_resume_transition(void)
{
sleep_increment_transition();
/*
* Start the timer again to ensure the AP doesn't get itself stuck in
* a state where it's no longer in a sleep state (S0ix/S3), but from
* the Linux perspective is still suspended. Perhaps a bug in the SoC-
* internal periodic housekeeping code might result in a situation
* like this.
*/
mutex_lock(&sleep_transition_timeout_lock);
if (sleep_signal_timeout) {
timeout_hang_type = SLEEP_HANG_S0IX_RESUME;
hook_call_deferred(&sleep_transition_timeout_data,
(uint32_t)sleep_signal_timeout * 1000);
}
mutex_unlock(&sleep_transition_timeout_lock);
}
static void sleep_transition_timeout(void)
{
/* Mark the timeout. */
sleep_signal_transitions |= EC_HOST_RESUME_SLEEP_TIMEOUT;
hook_call_deferred(&sleep_transition_timeout_data, -1);
if (timeout_hang_type != SLEEP_HANG_NONE) {
power_chipset_handle_sleep_hang(timeout_hang_type);
power_board_handle_sleep_hang(timeout_hang_type);
}
/*
* Perform the recovery after the chipset/board has had a chance to do
* their work, so we don't modify system state (resetting the AP) until
* after they've initiated any debug data collection.
*/
power_sleep_hang_recovery(timeout_hang_type);
}
void sleep_start_suspend(struct host_sleep_event_context *ctx)
{
uint16_t timeout = ctx->sleep_timeout_ms;
sleep_signal_transitions = 0;
/* Use default to indicate no timeout given. */
if (timeout == EC_HOST_SLEEP_TIMEOUT_DEFAULT) {
timeout = host_sleep_timeout_default;
}
/* Use 0xFFFF to disable the timeout */
if (timeout == EC_HOST_SLEEP_TIMEOUT_INFINITE) {
sleep_signal_timeout = 0;
return;
}
mutex_lock(&sleep_transition_timeout_lock);
sleep_signal_timeout = timeout;
timeout_hang_type = SLEEP_HANG_S0IX_SUSPEND;
hook_call_deferred(&sleep_transition_timeout_data,
(uint32_t)timeout * 1000);
mutex_unlock(&sleep_transition_timeout_lock);
}
void sleep_complete_resume(struct host_sleep_event_context *ctx)
{
/*
* Ensure we don't schedule another sleep_transition_timeout
* if the the HOST_SLEEP_EVENT_S0IX_RESUME message arrives before
* the CHIPSET task transitions to the POWER_S0ixS0 state.
*/
mutex_lock(&sleep_transition_timeout_lock);
sleep_signal_timeout = 0;
hook_call_deferred(&sleep_transition_timeout_data, -1);
ctx->sleep_transitions = sleep_signal_transitions;
mutex_unlock(&sleep_transition_timeout_lock);
}
void sleep_reset_tracking(void)
{
sleep_signal_transitions = 0;
sleep_signal_timeout = 0;
timeout_hang_type = SLEEP_HANG_NONE;
}
static enum ec_status
host_command_host_sleep_signal_transitions(struct host_cmd_handler_args *args)
{
struct ec_response_host_sleep_signal_transitions *resp = args->response;
args->response_size = sizeof(*resp);
resp->sleep_signal_transitions = sleep_signal_transitions;
return EC_RES_SUCCESS;
}
DECLARE_HOST_COMMAND(EC_CMD_HOST_SLEEP_SIGNAL_TRANSITIONS,
host_command_host_sleep_signal_transitions,
EC_VER_MASK(0));
static int command_sleep_fail_timeout(int argc, const char **argv)
{
if (argc < 2) {
/* no arguments - just print the current timeout */
} else if (!strcasecmp(argv[1], "default")) {
host_sleep_timeout_default = CONFIG_SLEEP_TIMEOUT_MS;
} else if (!strcasecmp(argv[1], "infinite")) {
host_sleep_timeout_default = EC_HOST_SLEEP_TIMEOUT_INFINITE;
} else {
char *e;
int val;
val = strtoi(argv[1], &e, 10);
if (*e)
return EC_ERROR_PARAM1;
if (val <= 0 || val >= EC_HOST_SLEEP_TIMEOUT_INFINITE) {
ccprintf("Error: timeout range is 1..%d [msec]\n",
EC_HOST_SLEEP_TIMEOUT_INFINITE - 1);
return EC_ERROR_PARAM1;
}
host_sleep_timeout_default = val;
}
if (host_sleep_timeout_default == EC_HOST_SLEEP_TIMEOUT_INFINITE)
ccprintf("Sleep failure detection timeout is disabled\n");
else
ccprintf("Sleep failure detection timeout is %d [msec]\n",
host_sleep_timeout_default);
return EC_SUCCESS;
}
DECLARE_CONSOLE_COMMAND(sleeptimeout, command_sleep_fail_timeout,
"[default | infinite | <msec>]",
"Display or set host sleep failure detection timeout.\n"
"Valid arguments are:\n"
" default\n"
" infinite - disables the timeout\n"
" <msec> - custom length in milliseconds\n"
" <none> - prints the current setting");
#else /* !SECTION_IS_RW && !CONFIG_POWER_SLEEP_FAILURE_DETECTION */
/* No action */
void sleep_suspend_transition(void)
{
}
void sleep_resume_transition(void)
{
}
void sleep_start_suspend(struct host_sleep_event_context *ctx)
{
}
void sleep_complete_resume(struct host_sleep_event_context *ctx)
{
}
void sleep_reset_tracking(void)
{
}
#endif /* SECTION_IS_RW && CONFIG_POWER_SLEEP_FAILURE_DETECTION */