363 lines
11 KiB
C
363 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* SCMI Generic SystemPower Control driver.
|
|
*
|
|
* Copyright (C) 2020-2022 ARM Ltd.
|
|
*/
|
|
/*
|
|
* In order to handle platform originated SCMI SystemPower requests (like
|
|
* shutdowns or cold/warm resets) we register an SCMI Notification notifier
|
|
* block to react when such SCMI SystemPower events are emitted by platform.
|
|
*
|
|
* Once such a notification is received we act accordingly to perform the
|
|
* required system transition depending on the kind of request.
|
|
*
|
|
* Graceful requests are routed to userspace through the same API methods
|
|
* (orderly_poweroff/reboot()) used by ACPI when handling ACPI Shutdown bus
|
|
* events.
|
|
*
|
|
* Direct forceful requests are not supported since are not meant to be sent
|
|
* by the SCMI platform to an OSPM like Linux.
|
|
*
|
|
* Additionally, graceful request notifications can carry an optional timeout
|
|
* field stating the maximum amount of time allowed by the platform for
|
|
* completion after which they are converted to forceful ones: the assumption
|
|
* here is that even graceful requests can be upper-bound by a maximum final
|
|
* timeout strictly enforced by the platform itself which can ultimately cut
|
|
* the power off at will anytime; in order to avoid such extreme scenario, we
|
|
* track progress of graceful requests through the means of a reboot notifier
|
|
* converting timed-out graceful requests to forceful ones, so at least we
|
|
* try to perform a clean sync and shutdown/restart before the power is cut.
|
|
*
|
|
* Given the peculiar nature of SCMI SystemPower protocol, that is being in
|
|
* charge of triggering system wide shutdown/reboot events, there should be
|
|
* only one SCMI platform actively emitting SystemPower events.
|
|
* For this reason the SCMI core takes care to enforce the creation of one
|
|
* single unique device associated to the SCMI System Power protocol; no matter
|
|
* how many SCMI platforms are defined on the system, only one can be designated
|
|
* to support System Power: as a consequence this driver will never be probed
|
|
* more than once.
|
|
*
|
|
* For similar reasons as soon as the first valid SystemPower is received by
|
|
* this driver and the shutdown/reboot is started, any further notification
|
|
* possibly emitted by the platform will be ignored.
|
|
*/
|
|
|
|
#include <linux/math.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/printk.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/scmi_protocol.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/time64.h>
|
|
#include <linux/timer.h>
|
|
#include <linux/types.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#ifndef MODULE
|
|
#include <linux/fs.h>
|
|
#endif
|
|
|
|
enum scmi_syspower_state {
|
|
SCMI_SYSPOWER_IDLE,
|
|
SCMI_SYSPOWER_IN_PROGRESS,
|
|
SCMI_SYSPOWER_REBOOTING
|
|
};
|
|
|
|
/**
|
|
* struct scmi_syspower_conf - Common configuration
|
|
*
|
|
* @dev: A reference device
|
|
* @state: Current SystemPower state
|
|
* @state_mtx: @state related mutex
|
|
* @required_transition: The requested transition as decribed in the received
|
|
* SCMI SystemPower notification
|
|
* @userspace_nb: The notifier_block registered against the SCMI SystemPower
|
|
* notification to start the needed userspace interactions.
|
|
* @reboot_nb: A notifier_block optionally used to track reboot progress
|
|
* @forceful_work: A worker used to trigger a forceful transition once a
|
|
* graceful has timed out.
|
|
*/
|
|
struct scmi_syspower_conf {
|
|
struct device *dev;
|
|
enum scmi_syspower_state state;
|
|
/* Protect access to state */
|
|
struct mutex state_mtx;
|
|
enum scmi_system_events required_transition;
|
|
|
|
struct notifier_block userspace_nb;
|
|
struct notifier_block reboot_nb;
|
|
|
|
struct delayed_work forceful_work;
|
|
};
|
|
|
|
#define userspace_nb_to_sconf(x) \
|
|
container_of(x, struct scmi_syspower_conf, userspace_nb)
|
|
|
|
#define reboot_nb_to_sconf(x) \
|
|
container_of(x, struct scmi_syspower_conf, reboot_nb)
|
|
|
|
#define dwork_to_sconf(x) \
|
|
container_of(x, struct scmi_syspower_conf, forceful_work)
|
|
|
|
/**
|
|
* scmi_reboot_notifier - A reboot notifier to catch an ongoing successful
|
|
* system transition
|
|
* @nb: Reference to the related notifier block
|
|
* @reason: The reason for the ongoing reboot
|
|
* @__unused: The cmd being executed on a restart request (unused)
|
|
*
|
|
* When an ongoing system transition is detected, compatible with the one
|
|
* requested by SCMI, cancel the delayed work.
|
|
*
|
|
* Return: NOTIFY_OK in any case
|
|
*/
|
|
static int scmi_reboot_notifier(struct notifier_block *nb,
|
|
unsigned long reason, void *__unused)
|
|
{
|
|
struct scmi_syspower_conf *sc = reboot_nb_to_sconf(nb);
|
|
|
|
mutex_lock(&sc->state_mtx);
|
|
switch (reason) {
|
|
case SYS_HALT:
|
|
case SYS_POWER_OFF:
|
|
if (sc->required_transition == SCMI_SYSTEM_SHUTDOWN)
|
|
sc->state = SCMI_SYSPOWER_REBOOTING;
|
|
break;
|
|
case SYS_RESTART:
|
|
if (sc->required_transition == SCMI_SYSTEM_COLDRESET ||
|
|
sc->required_transition == SCMI_SYSTEM_WARMRESET)
|
|
sc->state = SCMI_SYSPOWER_REBOOTING;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (sc->state == SCMI_SYSPOWER_REBOOTING) {
|
|
dev_dbg(sc->dev, "Reboot in progress...cancel delayed work.\n");
|
|
cancel_delayed_work_sync(&sc->forceful_work);
|
|
}
|
|
mutex_unlock(&sc->state_mtx);
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
/**
|
|
* scmi_request_forceful_transition - Request forceful SystemPower transition
|
|
* @sc: A reference to the configuration data
|
|
*
|
|
* Initiates the required SystemPower transition without involving userspace:
|
|
* just trigger the action at the kernel level after issuing an emergency
|
|
* sync. (if possible at all)
|
|
*/
|
|
static inline void
|
|
scmi_request_forceful_transition(struct scmi_syspower_conf *sc)
|
|
{
|
|
dev_dbg(sc->dev, "Serving forceful request:%d\n",
|
|
sc->required_transition);
|
|
|
|
#ifndef MODULE
|
|
emergency_sync();
|
|
#endif
|
|
switch (sc->required_transition) {
|
|
case SCMI_SYSTEM_SHUTDOWN:
|
|
kernel_power_off();
|
|
break;
|
|
case SCMI_SYSTEM_COLDRESET:
|
|
case SCMI_SYSTEM_WARMRESET:
|
|
kernel_restart(NULL);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void scmi_forceful_work_func(struct work_struct *work)
|
|
{
|
|
struct scmi_syspower_conf *sc;
|
|
struct delayed_work *dwork;
|
|
|
|
if (system_state > SYSTEM_RUNNING)
|
|
return;
|
|
|
|
dwork = to_delayed_work(work);
|
|
sc = dwork_to_sconf(dwork);
|
|
|
|
dev_dbg(sc->dev, "Graceful request timed out...forcing !\n");
|
|
mutex_lock(&sc->state_mtx);
|
|
/* avoid deadlock by unregistering reboot notifier first */
|
|
unregister_reboot_notifier(&sc->reboot_nb);
|
|
if (sc->state == SCMI_SYSPOWER_IN_PROGRESS)
|
|
scmi_request_forceful_transition(sc);
|
|
mutex_unlock(&sc->state_mtx);
|
|
}
|
|
|
|
/**
|
|
* scmi_request_graceful_transition - Request graceful SystemPower transition
|
|
* @sc: A reference to the configuration data
|
|
* @timeout_ms: The desired timeout to wait for the shutdown to complete before
|
|
* system is forcibly shutdown.
|
|
*
|
|
* Initiates the required SystemPower transition, requesting userspace
|
|
* co-operation: it uses the same orderly_ methods used by ACPI Shutdown event
|
|
* processing.
|
|
*
|
|
* Takes care also to register a reboot notifier and to schedule a delayed work
|
|
* in order to detect if userspace actions are taking too long and in such a
|
|
* case to trigger a forceful transition.
|
|
*/
|
|
static void scmi_request_graceful_transition(struct scmi_syspower_conf *sc,
|
|
unsigned int timeout_ms)
|
|
{
|
|
unsigned int adj_timeout_ms = 0;
|
|
|
|
if (timeout_ms) {
|
|
int ret;
|
|
|
|
sc->reboot_nb.notifier_call = &scmi_reboot_notifier;
|
|
ret = register_reboot_notifier(&sc->reboot_nb);
|
|
if (!ret) {
|
|
/* Wait only up to 75% of the advertised timeout */
|
|
adj_timeout_ms = mult_frac(timeout_ms, 3, 4);
|
|
INIT_DELAYED_WORK(&sc->forceful_work,
|
|
scmi_forceful_work_func);
|
|
schedule_delayed_work(&sc->forceful_work,
|
|
msecs_to_jiffies(adj_timeout_ms));
|
|
} else {
|
|
/* Carry on best effort even without a reboot notifier */
|
|
dev_warn(sc->dev,
|
|
"Cannot register reboot notifier !\n");
|
|
}
|
|
}
|
|
|
|
dev_dbg(sc->dev,
|
|
"Serving graceful req:%d (timeout_ms:%u adj_timeout_ms:%u)\n",
|
|
sc->required_transition, timeout_ms, adj_timeout_ms);
|
|
|
|
switch (sc->required_transition) {
|
|
case SCMI_SYSTEM_SHUTDOWN:
|
|
/*
|
|
* When triggered early at boot-time the 'orderly' call will
|
|
* partially fail due to the lack of userspace itself, but
|
|
* the force=true argument will start anyway a successful
|
|
* forced shutdown.
|
|
*/
|
|
orderly_poweroff(true);
|
|
break;
|
|
case SCMI_SYSTEM_COLDRESET:
|
|
case SCMI_SYSTEM_WARMRESET:
|
|
orderly_reboot();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* scmi_userspace_notifier - Notifier callback to act on SystemPower
|
|
* Notifications
|
|
* @nb: Reference to the related notifier block
|
|
* @event: The SystemPower notification event id
|
|
* @data: The SystemPower event report
|
|
*
|
|
* This callback is in charge of decoding the received SystemPower report
|
|
* and act accordingly triggering a graceful or forceful system transition.
|
|
*
|
|
* Note that once a valid SCMI SystemPower event starts being served, any
|
|
* other following SystemPower notification received from the same SCMI
|
|
* instance (handle) will be ignored.
|
|
*
|
|
* Return: NOTIFY_OK once a valid SystemPower event has been successfully
|
|
* processed.
|
|
*/
|
|
static int scmi_userspace_notifier(struct notifier_block *nb,
|
|
unsigned long event, void *data)
|
|
{
|
|
struct scmi_system_power_state_notifier_report *er = data;
|
|
struct scmi_syspower_conf *sc = userspace_nb_to_sconf(nb);
|
|
|
|
if (er->system_state >= SCMI_SYSTEM_POWERUP) {
|
|
dev_err(sc->dev, "Ignoring unsupported system_state: 0x%X\n",
|
|
er->system_state);
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
if (!SCMI_SYSPOWER_IS_REQUEST_GRACEFUL(er->flags)) {
|
|
dev_err(sc->dev, "Ignoring forceful notification.\n");
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
/*
|
|
* Bail out if system is already shutting down or an SCMI SystemPower
|
|
* requested is already being served.
|
|
*/
|
|
if (system_state > SYSTEM_RUNNING)
|
|
return NOTIFY_DONE;
|
|
mutex_lock(&sc->state_mtx);
|
|
if (sc->state != SCMI_SYSPOWER_IDLE) {
|
|
dev_dbg(sc->dev,
|
|
"Transition already in progress...ignore.\n");
|
|
mutex_unlock(&sc->state_mtx);
|
|
return NOTIFY_DONE;
|
|
}
|
|
sc->state = SCMI_SYSPOWER_IN_PROGRESS;
|
|
mutex_unlock(&sc->state_mtx);
|
|
|
|
sc->required_transition = er->system_state;
|
|
|
|
/* Leaving a trace in logs of who triggered the shutdown/reboot. */
|
|
dev_info(sc->dev, "Serving shutdown/reboot request: %d\n",
|
|
sc->required_transition);
|
|
|
|
scmi_request_graceful_transition(sc, er->timeout);
|
|
|
|
return NOTIFY_OK;
|
|
}
|
|
|
|
static int scmi_syspower_probe(struct scmi_device *sdev)
|
|
{
|
|
int ret;
|
|
struct scmi_syspower_conf *sc;
|
|
struct scmi_handle *handle = sdev->handle;
|
|
|
|
if (!handle)
|
|
return -ENODEV;
|
|
|
|
ret = handle->devm_protocol_acquire(sdev, SCMI_PROTOCOL_SYSTEM);
|
|
if (ret)
|
|
return ret;
|
|
|
|
sc = devm_kzalloc(&sdev->dev, sizeof(*sc), GFP_KERNEL);
|
|
if (!sc)
|
|
return -ENOMEM;
|
|
|
|
sc->state = SCMI_SYSPOWER_IDLE;
|
|
mutex_init(&sc->state_mtx);
|
|
sc->required_transition = SCMI_SYSTEM_MAX;
|
|
sc->userspace_nb.notifier_call = &scmi_userspace_notifier;
|
|
sc->dev = &sdev->dev;
|
|
|
|
return handle->notify_ops->devm_event_notifier_register(sdev,
|
|
SCMI_PROTOCOL_SYSTEM,
|
|
SCMI_EVENT_SYSTEM_POWER_STATE_NOTIFIER,
|
|
NULL, &sc->userspace_nb);
|
|
}
|
|
|
|
static const struct scmi_device_id scmi_id_table[] = {
|
|
{ SCMI_PROTOCOL_SYSTEM, "syspower" },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(scmi, scmi_id_table);
|
|
|
|
static struct scmi_driver scmi_system_power_driver = {
|
|
.name = "scmi-system-power",
|
|
.probe = scmi_syspower_probe,
|
|
.id_table = scmi_id_table,
|
|
};
|
|
module_scmi_driver(scmi_system_power_driver);
|
|
|
|
MODULE_AUTHOR("Cristian Marussi <cristian.marussi@arm.com>");
|
|
MODULE_DESCRIPTION("ARM SCMI SystemPower Control driver");
|
|
MODULE_LICENSE("GPL");
|