diff options
Diffstat (limited to 'drivers/firmware')
-rw-r--r-- | drivers/firmware/arm_sdei.c | 256 |
1 files changed, 255 insertions, 1 deletions
diff --git a/drivers/firmware/arm_sdei.c b/drivers/firmware/arm_sdei.c index 8da173cc7e43..c503bc5222ed 100644 --- a/drivers/firmware/arm_sdei.c +++ b/drivers/firmware/arm_sdei.c @@ -7,6 +7,8 @@ #include <linux/arm-smccc.h> #include <linux/bitops.h> #include <linux/compiler.h> +#include <linux/cpuhotplug.h> +#include <linux/cpu_pm.h> #include <linux/errno.h> #include <linux/hardirq.h> #include <linux/kernel.h> @@ -14,12 +16,15 @@ #include <linux/kvm_host.h> #include <linux/list.h> #include <linux/mutex.h> +#include <linux/notifier.h> #include <linux/of.h> #include <linux/of_platform.h> #include <linux/percpu.h> #include <linux/platform_device.h> +#include <linux/pm.h> #include <linux/ptrace.h> #include <linux/preempt.h> +#include <linux/reboot.h> #include <linux/slab.h> #include <linux/smp.h> #include <linux/spinlock.h> @@ -37,7 +42,11 @@ static asmlinkage void (*sdei_firmware_call)(unsigned long function_id, static unsigned long sdei_entry_point; struct sdei_event { + /* These three are protected by the sdei_list_lock */ struct list_head list; + bool reregister; + bool reenable; + u32 event_num; u8 type; u8 priority; @@ -253,6 +262,11 @@ static void _ipi_mask_cpu(void *ignored) sdei_mask_local_cpu(); } +static int sdei_cpuhp_down(unsigned int ignored) +{ + return sdei_mask_local_cpu(); +} + int sdei_unmask_local_cpu(void) { int err; @@ -274,6 +288,11 @@ static void _ipi_unmask_cpu(void *ignored) sdei_unmask_local_cpu(); } +static int sdei_cpuhp_up(unsigned int ignored) +{ + return sdei_unmask_local_cpu(); +} + static void _ipi_private_reset(void *ignored) { int err; @@ -330,6 +349,10 @@ int sdei_event_enable(u32 event_num) return -ENOENT; } + spin_lock(&sdei_list_lock); + event->reenable = true; + spin_unlock(&sdei_list_lock); + if (event->type == SDEI_EVENT_TYPE_SHARED) err = sdei_api_event_enable(event->event_num); mutex_unlock(&sdei_events_lock); @@ -356,6 +379,10 @@ int sdei_event_disable(u32 event_num) return -ENOENT; } + spin_lock(&sdei_list_lock); + event->reenable = false; + spin_unlock(&sdei_list_lock); + if (event->type == SDEI_EVENT_TYPE_SHARED) err = sdei_api_event_disable(event->event_num); mutex_unlock(&sdei_events_lock); @@ -374,6 +401,11 @@ static int _sdei_event_unregister(struct sdei_event *event) { lockdep_assert_held(&sdei_events_lock); + spin_lock(&sdei_list_lock); + event->reregister = false; + event->reenable = false; + spin_unlock(&sdei_list_lock); + if (event->type == SDEI_EVENT_TYPE_SHARED) return sdei_api_event_unregister(event->event_num); @@ -408,6 +440,31 @@ int sdei_event_unregister(u32 event_num) } EXPORT_SYMBOL(sdei_event_unregister); +/* + * unregister events, but don't destroy them as they are re-registered by + * sdei_reregister_shared(). + */ +static int sdei_unregister_shared(void) +{ + int err = 0; + struct sdei_event *event; + + mutex_lock(&sdei_events_lock); + spin_lock(&sdei_list_lock); + list_for_each_entry(event, &sdei_list, list) { + if (event->type != SDEI_EVENT_TYPE_SHARED) + continue; + + err = _sdei_event_unregister(event); + if (err) + break; + } + spin_unlock(&sdei_list_lock); + mutex_unlock(&sdei_events_lock); + + return err; +} + static int sdei_api_event_register(u32 event_num, unsigned long entry_point, void *arg, u64 flags, u64 affinity) { @@ -465,6 +522,175 @@ int sdei_event_register(u32 event_num, sdei_event_callback *cb, void *arg) } EXPORT_SYMBOL(sdei_event_register); +static int sdei_reregister_event(struct sdei_event *event) +{ + int err; + + lockdep_assert_held(&sdei_events_lock); + + err = _sdei_event_register(event); + if (err) { + pr_err("Failed to re-register event %u\n", event->event_num); + sdei_event_destroy(event); + return err; + } + + if (event->reenable) { + if (event->type == SDEI_EVENT_TYPE_SHARED) + err = sdei_api_event_enable(event->event_num); + } + + if (err) + pr_err("Failed to re-enable event %u\n", event->event_num); + + return err; +} + +static int sdei_reregister_shared(void) +{ + int err = 0; + struct sdei_event *event; + + mutex_lock(&sdei_events_lock); + spin_lock(&sdei_list_lock); + list_for_each_entry(event, &sdei_list, list) { + if (event->type != SDEI_EVENT_TYPE_SHARED) + continue; + + if (event->reregister) { + err = sdei_reregister_event(event); + if (err) + break; + } + } + spin_unlock(&sdei_list_lock); + mutex_unlock(&sdei_events_lock); + + return err; +} + +/* When entering idle, mask/unmask events for this cpu */ +static int sdei_pm_notifier(struct notifier_block *nb, unsigned long action, + void *data) +{ + int rv; + + switch (action) { + case CPU_PM_ENTER: + rv = sdei_mask_local_cpu(); + break; + case CPU_PM_EXIT: + case CPU_PM_ENTER_FAILED: + rv = sdei_unmask_local_cpu(); + break; + default: + return NOTIFY_DONE; + } + + if (rv) + return notifier_from_errno(rv); + + return NOTIFY_OK; +} + +static struct notifier_block sdei_pm_nb = { + .notifier_call = sdei_pm_notifier, +}; + +static int sdei_device_suspend(struct device *dev) +{ + on_each_cpu(_ipi_mask_cpu, NULL, true); + + return 0; +} + +static int sdei_device_resume(struct device *dev) +{ + on_each_cpu(_ipi_unmask_cpu, NULL, true); + + return 0; +} + +/* + * We need all events to be reregistered when we resume from hibernate. + * + * The sequence is freeze->thaw. Reboot. freeze->restore. We unregister + * events during freeze, then re-register and re-enable them during thaw + * and restore. + */ +static int sdei_device_freeze(struct device *dev) +{ + int err; + + cpuhp_remove_state(CPUHP_AP_ARM_SDEI_STARTING); + + err = sdei_unregister_shared(); + if (err) + return err; + + return 0; +} + +static int sdei_device_thaw(struct device *dev) +{ + int err; + + /* re-register shared events */ + err = sdei_reregister_shared(); + if (err) { + pr_warn("Failed to re-register shared events...\n"); + sdei_mark_interface_broken(); + return err; + } + + err = cpuhp_setup_state(CPUHP_AP_ARM_SDEI_STARTING, "SDEI", + &sdei_cpuhp_up, &sdei_cpuhp_down); + if (err) + pr_warn("Failed to re-register CPU hotplug notifier...\n"); + + return err; +} + +static int sdei_device_restore(struct device *dev) +{ + int err; + + err = sdei_platform_reset(); + if (err) + return err; + + return sdei_device_thaw(dev); +} + +static const struct dev_pm_ops sdei_pm_ops = { + .suspend = sdei_device_suspend, + .resume = sdei_device_resume, + .freeze = sdei_device_freeze, + .thaw = sdei_device_thaw, + .restore = sdei_device_restore, +}; + +/* + * Mask all CPUs and unregister all events on panic, reboot or kexec. + */ +static int sdei_reboot_notifier(struct notifier_block *nb, unsigned long action, + void *data) +{ + /* + * We are going to reset the interface, after this there is no point + * doing work when we take CPUs offline. + */ + cpuhp_remove_state(CPUHP_AP_ARM_SDEI_STARTING); + + sdei_platform_reset(); + + return NOTIFY_OK; +} + +static struct notifier_block sdei_reboot_nb = { + .notifier_call = sdei_reboot_notifier, +}; + static void sdei_smccc_smc(unsigned long function_id, unsigned long arg0, unsigned long arg1, unsigned long arg2, unsigned long arg3, @@ -547,9 +773,36 @@ static int sdei_probe(struct platform_device *pdev) return 0; } - on_each_cpu(&_ipi_unmask_cpu, NULL, false); + err = cpu_pm_register_notifier(&sdei_pm_nb); + if (err) { + pr_warn("Failed to register CPU PM notifier...\n"); + goto error; + } + + err = register_reboot_notifier(&sdei_reboot_nb); + if (err) { + pr_warn("Failed to register reboot notifier...\n"); + goto remove_cpupm; + } + + err = cpuhp_setup_state(CPUHP_AP_ARM_SDEI_STARTING, "SDEI", + &sdei_cpuhp_up, &sdei_cpuhp_down); + if (err) { + pr_warn("Failed to register CPU hotplug notifier...\n"); + goto remove_reboot; + } return 0; + +remove_reboot: + unregister_reboot_notifier(&sdei_reboot_nb); + +remove_cpupm: + cpu_pm_unregister_notifier(&sdei_pm_nb); + +error: + sdei_mark_interface_broken(); + return err; } static const struct of_device_id sdei_of_match[] = { @@ -560,6 +813,7 @@ static const struct of_device_id sdei_of_match[] = { static struct platform_driver sdei_driver = { .driver = { .name = "sdei", + .pm = &sdei_pm_ops, .of_match_table = sdei_of_match, }, .probe = sdei_probe, |