| // 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"); |