// 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 #include #include #include #include #include #include #include #include #include #include #ifndef MODULE #include #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 "); MODULE_DESCRIPTION("ARM SCMI SystemPower Control driver"); MODULE_LICENSE("GPL");