From c06fec2f0347056617540676e6e1176a9ee8ea14 Mon Sep 17 00:00:00 2001 From: Huang Yiwei Date: Wed, 6 Apr 2022 17:50:10 +0800 Subject: [PATCH] soc: qcom: Add snapshot of watchdog driver This is a snapshot of the soc watchdog, gunyah watchdog and watchdog core driver as of msm-5.15 commit <908a517b050d> ("Merge "sched/walt: remove duplicate definitions of rt_task_arrival_time""). Change-Id: I78134e1b42e0abd74c3b4630c135715ee959afa1 Signed-off-by: Huang Yiwei --- drivers/soc/qcom/Kconfig | 97 +++ drivers/soc/qcom/Makefile | 2 + drivers/soc/qcom/qcom_soc_wdt.c | 148 +++++ drivers/soc/qcom/qcom_wdt_core.c | 992 ++++++++++++++++++++++++++++++ drivers/virt/Kconfig | 2 + drivers/virt/Makefile | 1 + drivers/virt/gunyah/Kconfig | 27 + drivers/virt/gunyah/Makefile | 2 + drivers/virt/gunyah/gh_virt_wdt.c | 305 +++++++++ include/linux/gunyah/gh_errno.h | 77 +++ include/soc/qcom/watchdog.h | 169 +++++ 11 files changed, 1822 insertions(+) create mode 100644 drivers/soc/qcom/qcom_soc_wdt.c create mode 100644 drivers/soc/qcom/qcom_wdt_core.c create mode 100644 drivers/virt/gunyah/Kconfig create mode 100644 drivers/virt/gunyah/Makefile create mode 100644 drivers/virt/gunyah/gh_virt_wdt.c create mode 100644 include/linux/gunyah/gh_errno.h create mode 100644 include/soc/qcom/watchdog.h diff --git a/drivers/soc/qcom/Kconfig b/drivers/soc/qcom/Kconfig index 1c3ac3aabe1c..059f13828fb1 100644 --- a/drivers/soc/qcom/Kconfig +++ b/drivers/soc/qcom/Kconfig @@ -245,6 +245,103 @@ config QCOM_LOGBUF_VENDOR_HOOKS debugging issues which are manifestation of failure during initial bootup. +config QCOM_WDT_CORE + tristate "Qualcomm Technologies, Inc. Watchdog Support" + depends on ARCH_QCOM + help + This enables the watchdog framework for Qualcomm Technologies, Inc. + devices. It causes a kernel panic if the watchdog times out. It allows + for the detection of cpu hangs and deadlocks. It does not run during the + bootup process, so it will not catch any early lockups. Enabling this + only enables the framework, an individual Qualcomm Technologies, Inc. + watchdog module must be loaded along with this for watchdog + functionality. + +config QCOM_SOC_WATCHDOG + tristate "Qualcomm Technologies, Inc. Soc Watchdog" + depends on QCOM_WDT_CORE + help + This enables the Qualcomm Technologies, Inc. watchdog module for the + Soc. It provides an interface to perform watchdog actions such as + setting the bark/bite time and also petting the hardware watchdog. To + utilize this the Qualcomm Technologies, Inc. watchdog framework must + also be enabled. + +config QCOM_IRQ_STAT + bool "QCOM IRQ stats" + depends on QCOM_WDT_CORE + help + This give irq stats for top hitter at + watchdog pet, watchdog bark and kernel panics. + This provides additional debug information + for irq counts on cpu and ipi counts. + +config QCOM_FORCE_WDOG_BITE_ON_PANIC + bool "QCOM force watchdog bite on panic" + depends on QCOM_WDT_CORE + help + This forces a watchdog bite when the device restarts + due to a kernel panic. On certain MSM SoCs, + this provides additional debugging + information. + +config QCOM_WDOG_BITE_EARLY_PANIC + bool "QCOM early panic watchdog bite" + depends on QCOM_WDT_CORE && QCOM_FORCE_WDOG_BITE_ON_PANIC + help + This forces a watchdog bite early in panic sequence. On certain + MSM SoCs, this provides us additional debugging information at the + context of the crash. If this option is disabled, then bite occurs + later in panic, which permits more of the restart sequence to run + (e.g. more dmesg to flushed to console). + +config QCOM_WATCHDOG_BARK_TIME + depends on QCOM_WDT_CORE + int "Qualcomm Technologies, Inc. Watchdog bark time in ms" + default 11000 + range 11000 11000 + help + The amount of time, in milliseconds, that should elapse after + a watchdog timer reset before a bark interrupt is sent from the + watchdog. + +config QCOM_WATCHDOG_PET_TIME + depends on QCOM_WDT_CORE + int "Qualcomm Technologies, Inc. Watchdog pet time in ms" + default 9360 + range 9360 9360 + help + The amount of time, in milliseconds, that should elapse before + a watchdog pet is initiated to reset the watchdog timer to 0. + +config QCOM_WATCHDOG_IPI_PING + depends on QCOM_WDT_CORE + bool "Qualcomm Technologies, Inc. Watchdog ipi ping" + default y + help + This boolean flag gives the watchdog driver the ability to send a + keep-alive ping to other cpu's if it is set to 1. Otherwise, when + it is set to 0 no keep alive pings will be sent. + +config QCOM_WATCHDOG_WAKEUP_ENABLE + depends on QCOM_WDT_CORE + bool "Qualcomm Technologies, Inc. Watchdog wakeup enable" + default y + help + This boolean flag allows the non secure watchdog counter to freeze + and unfreeze automatically across the system suspend and resume + path. + +config QCOM_WATCHDOG_USERSPACE_PET + depends on QCOM_WDT_CORE + bool "Qualcomm Technologies, Inc. Watchdog user pet enable" + default n + help + This boolean flag allows enabling the userspace-watchdog feature. + This feature requires userspace to pet the watchdog every in an + interval that matches the time set in the pet-time config. + The feature is supported through device sysfs files. + config QCOM_WCNSS_CTRL tristate "Qualcomm WCNSS control driver" depends on ARCH_QCOM || COMPILE_TEST diff --git a/drivers/soc/qcom/Makefile b/drivers/soc/qcom/Makefile index 5dbac3170cfa..aed7171b7f06 100644 --- a/drivers/soc/qcom/Makefile +++ b/drivers/soc/qcom/Makefile @@ -21,6 +21,8 @@ obj-$(CONFIG_QCOM_SMP2P) += smp2p.o obj-$(CONFIG_QCOM_SMSM) += smsm.o obj-$(CONFIG_QCOM_SOCINFO) += socinfo.o obj-$(CONFIG_QCOM_SPM) += spm.o +obj-$(CONFIG_QCOM_WDT_CORE) += qcom_wdt_core.o +obj-$(CONFIG_QCOM_SOC_WATCHDOG) += qcom_soc_wdt.o obj-$(CONFIG_QCOM_STATS) += qcom_stats.o obj-$(CONFIG_QCOM_WCNSS_CTRL) += wcnss_ctrl.o obj-$(CONFIG_QCOM_MSM_IPCC) += qcom_ipcc.o diff --git a/drivers/soc/qcom/qcom_soc_wdt.c b/drivers/soc/qcom/qcom_soc_wdt.c new file mode 100644 index 000000000000..5f3dc091fe9a --- /dev/null +++ b/drivers/soc/qcom/qcom_soc_wdt.c @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved. + * + */ + +#include +#include +#include +#include +#include +#include + +#define WDT0_RST 0x04 +#define WDT0_EN 0x08 +#define WDT0_STS 0x0C +#define WDT0_BARK_TIME 0x10 +#define WDT0_BITE_TIME 0x14 +#define WDT_HZ 32765 + +static inline int qcom_soc_set_wdt_bark(u32 time, + struct msm_watchdog_data *wdog_dd) +{ + __raw_writel((time * WDT_HZ)/1000, wdog_dd->base + WDT0_BARK_TIME); + /* Make sure register write is complete before proceeding */ + mb(); + return 0; +} + +static inline int qcom_soc_set_wdt_bite(u32 time, + struct msm_watchdog_data *wdog_dd) +{ + __raw_writel((time * WDT_HZ)/1000, wdog_dd->base + WDT0_BITE_TIME); + /* Make sure register write is complete before proceeding */ + mb(); + return 0; +} + + + +static inline int qcom_soc_reset_wdt(struct msm_watchdog_data *wdog_dd) +{ + __raw_writel(1, wdog_dd->base + WDT0_RST); + /* Make sure register write is complete before proceeding */ + mb(); + return 0; +} + +static inline int qcom_soc_enable_wdt(u32 val, + struct msm_watchdog_data *wdog_dd) +{ + __raw_writel(val, wdog_dd->base + WDT0_EN); + /* Make sure register write is complete before proceeding */ + mb(); + return 0; +} + +static inline int qcom_soc_disable_wdt(struct msm_watchdog_data *wdog_dd) +{ + __raw_writel(0, wdog_dd->base + WDT0_EN); + /* Make sure register write is complete before proceeding */ + mb(); + return 0; +} + +static inline int qcom_soc_show_wdt_status(struct msm_watchdog_data *wdog_dd) +{ + dev_err(wdog_dd->dev, "Wdog - STS: 0x%x, CTL: 0x%x, BARK TIME: 0x%x, BITE TIME: 0x%x\n", + __raw_readl(wdog_dd->base + WDT0_STS), + __raw_readl(wdog_dd->base + WDT0_EN), + __raw_readl(wdog_dd->base + WDT0_BARK_TIME), + __raw_readl(wdog_dd->base + WDT0_BITE_TIME)); + return 0; +} + +static struct qcom_wdt_ops qcom_soc_wdt_ops = { + .set_bark_time = qcom_soc_set_wdt_bark, + .set_bite_time = qcom_soc_set_wdt_bite, + .reset_wdt = qcom_soc_reset_wdt, + .enable_wdt = qcom_soc_enable_wdt, + .disable_wdt = qcom_soc_disable_wdt, + .show_wdt_status = qcom_soc_show_wdt_status +}; + +static int qcom_soc_wdt_probe(struct platform_device *pdev) +{ + struct resource *res; + struct msm_watchdog_data *wdog_dd; + + wdog_dd = devm_kzalloc(&pdev->dev, sizeof(*wdog_dd), GFP_KERNEL); + if (!wdog_dd) + return -ENOMEM; + res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "wdt-base"); + if (!res) + return -ENODEV; + + wdog_dd->base = devm_ioremap_resource(&pdev->dev, res); + if (!wdog_dd->base) { + dev_err(&pdev->dev, "%s cannot map wdog register space\n", + __func__); + return -ENXIO; + } + wdog_dd->ops = &qcom_soc_wdt_ops; + + return qcom_wdt_register(pdev, wdog_dd, "msm-watchdog"); +} + +static const struct dev_pm_ops qcom_soc_dev_pm_ops = { +#ifdef CONFIG_PM_SLEEP + .suspend_late = qcom_wdt_pet_suspend, + .resume_early = qcom_wdt_pet_resume, +#endif +}; + +static const struct of_device_id qcom_soc_match_table[] = { + { .compatible = "qcom,msm-watchdog" }, + {} +}; + +static struct platform_driver qcom_soc_wdt_driver = { + .probe = qcom_soc_wdt_probe, + .remove = qcom_wdt_remove, + .driver = { + .name = "msm_watchdog", + .pm = &qcom_soc_dev_pm_ops, + .of_match_table = qcom_soc_match_table, + }, +}; + +static int __init init_watchdog(void) +{ + return platform_driver_register(&qcom_soc_wdt_driver); +} + +#if IS_MODULE(CONFIG_QCOM_SOC_WATCHDOG) +module_init(init_watchdog); +#else +pure_initcall(init_watchdog); +#endif + +static __exit void exit_watchdog(void) +{ + platform_driver_unregister(&qcom_soc_wdt_driver); +} +module_exit(exit_watchdog); +MODULE_DESCRIPTION("QCOM Soc Watchdog Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/soc/qcom/qcom_wdt_core.c b/drivers/soc/qcom/qcom_wdt_core.c new file mode 100644 index 000000000000..f350f4b0ebc5 --- /dev/null +++ b/drivers/soc/qcom/qcom_wdt_core.c @@ -0,0 +1,992 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2012-2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MASK_SIZE 32 +#define COMPARE_RET -1 + +typedef int (*compare_t) (const void *lhs, const void *rhs); +static struct msm_watchdog_data *wdog_data; + +static void qcom_wdt_dump_cpu_alive_mask(struct msm_watchdog_data *wdog_dd) +{ + static char alive_mask_buf[MASK_SIZE]; + + scnprintf(alive_mask_buf, MASK_SIZE, "%*pb1", cpumask_pr_args( + &wdog_dd->alive_mask)); + dev_info(wdog_dd->dev, "cpu alive mask from last pet %s\n", + alive_mask_buf); +} + +#ifdef CONFIG_QCOM_IRQ_STAT +static int cmp_irq_info_fn(const void *a, const void *b) +{ + struct qcom_irq_info *lhs = (struct qcom_irq_info *)a; + struct qcom_irq_info *rhs = (struct qcom_irq_info *)b; + + if (lhs->total_count < rhs->total_count) + return 1; + + if (lhs->total_count > rhs->total_count) + return COMPARE_RET; + + return 0; +} + +static void swap_irq_info_fn(void *a, void *b, int size) +{ + struct qcom_irq_info temp; + struct qcom_irq_info *lhs = (struct qcom_irq_info *)a; + struct qcom_irq_info *rhs = (struct qcom_irq_info *)b; + + temp = *lhs; + *lhs = *rhs; + *rhs = temp; +} + +static struct qcom_irq_info *search(struct qcom_irq_info *key, + struct qcom_irq_info *base, + size_t num, compare_t cmp) +{ + struct qcom_irq_info *pivot = NULL; + int result; + + while (num > 0) { + pivot = base + (num >> 1); + result = cmp(key, pivot); + + if (result == 0) + goto out; + + if (result > 0) { + base = pivot + 1; + num--; + } + + if (num) + num >>= 1; + } + +out: + if (pivot) + pr_debug("*pivot:%u key:%u\n", + pivot->total_count, key->total_count); + + return pivot; +} + +static void print_irq_stat(struct msm_watchdog_data *wdog_dd) +{ + int index; + int cpu, ipi_nr; + struct qcom_irq_info *info; + + pr_info("(virq:irq_count)- "); + for (index = 0; index < NR_TOP_HITTERS; index++) { + info = &wdog_dd->irq_counts[index]; + pr_cont("%u:%u ", info->irq, info->total_count); + } + pr_cont("\n"); + + pr_info("(cpu:irq_count)- "); + for_each_possible_cpu(cpu) + pr_cont("%u:%u ", cpu, wdog_dd->tot_irq_count[cpu]); + pr_cont("\n"); + + pr_info("(ipi:irq_count)- "); + ipi_nr = nr_ipi_get(); + for (index = 0; index < ipi_nr; index++) { + info = &wdog_dd->ipi_counts[index]; + pr_cont("%u:%u ", info->irq, info->total_count); + } + pr_cont("\n"); +} + +static void compute_irq_stat(struct work_struct *work) +{ + unsigned int count; + int index = 0, cpu, irq, ipi_nr; + struct irq_desc *desc, **desc_ipi_arr; + struct qcom_irq_info *pos; + struct qcom_irq_info *start; + struct qcom_irq_info key = {0}; + unsigned int running; + struct msm_watchdog_data *wdog_dd = container_of(work, + struct msm_watchdog_data, + irq_counts_work); + + size_t arr_size = ARRAY_SIZE(wdog_dd->irq_counts); + + /* avoid parallel execution from bark handler and queued + * irq_counts_work. + */ + running = atomic_xchg(&wdog_dd->irq_counts_running, 1); + if (running) + return; + + /* per irq counts */ + rcu_read_lock(); + for_each_irq_nr(irq) { + desc = irq_to_desc(irq); + if (!desc) + continue; + + count = kstat_irqs_usr(irq); + if (!count) + continue; + + if (index < arr_size) { + wdog_dd->irq_counts[index].irq = irq; + wdog_dd->irq_counts[index].total_count = count; + for_each_possible_cpu(cpu) + wdog_dd->irq_counts[index].irq_counter[cpu] = + *per_cpu_ptr(desc->kstat_irqs, cpu); + + index++; + if (index == arr_size) + sort(wdog_dd->irq_counts, arr_size, + sizeof(*pos), cmp_irq_info_fn, + swap_irq_info_fn); + + continue; + } + + key.total_count = count; + start = wdog_dd->irq_counts + (arr_size - 1); + pos = search(&key, wdog_dd->irq_counts, + arr_size, cmp_irq_info_fn); + + if (pos && (pos->total_count >= key.total_count)) { + if (pos < start) + pos++; + else + pos = NULL; + } + + pr_debug("count :%u irq:%u\n", count, irq); + if (pos && pos < start) { + start--; + for (; start >= pos ; start--) + *(start + 1) = *start; + } + + if (pos) { + pos->irq = irq; + pos->total_count = count; + for_each_possible_cpu(cpu) + pos->irq_counter[cpu] = + *per_cpu_ptr(desc->kstat_irqs, cpu); + } + } + rcu_read_unlock(); + + /* per cpu total irq counts */ + for_each_possible_cpu(cpu) + wdog_dd->tot_irq_count[cpu] = kstat_cpu_irqs_sum(cpu); + + /* per IPI counts */ + ipi_nr = nr_ipi_get(); + desc_ipi_arr = ipi_desc_get(); + for (index = 0; index < ipi_nr; index++) { + wdog_dd->ipi_counts[index].total_count = 0; + wdog_dd->ipi_counts[index].irq = index; + irq = irq_desc_get_irq(desc_ipi_arr[index]); + for_each_possible_cpu(cpu) { + wdog_dd->ipi_counts[index].irq_counter[cpu] = + kstat_irqs_cpu(irq, cpu); + wdog_dd->ipi_counts[index].total_count += + wdog_dd->ipi_counts[index].irq_counter[cpu]; + } + } + + print_irq_stat(wdog_dd); + atomic_xchg(&wdog_dd->irq_counts_running, 0); +} + +static void queue_irq_counts_work(struct work_struct *irq_counts_work) +{ + queue_work(system_unbound_wq, irq_counts_work); +} +#else +static void queue_irq_counts_work(struct work_struct *irq_counts_work) { } +static void compute_irq_stat(struct work_struct *work) { } +#endif + +#ifdef CONFIG_PM_SLEEP +/** + * qcom_wdt_pet_suspend() - Suspends qcom watchdog functionality. + * + * @dev: qcom watchdog device structure + * + * All watchdogs should have the ability to be suspended, this + * will cause the watchdog counter to reset and HW counter will + * freeze when deepest low power mode is entered. + * + */ +int qcom_wdt_pet_suspend(struct device *dev) +{ + struct msm_watchdog_data *wdog_data = + (struct msm_watchdog_data *)dev_get_drvdata(dev); + + if (!wdog_data) + return 0; + + if (wdog_data->user_pet_enabled) + del_timer_sync(&wdog_data->user_pet_timer); + + spin_lock(&wdog_data->freeze_lock); + wdog_data->freeze_in_progress = true; + spin_unlock(&wdog_data->freeze_lock); + wdog_data->ops->reset_wdt(wdog_data); + del_timer_sync(&wdog_data->pet_timer); + if (wdog_data->wakeup_irq_enable) { + wdog_data->last_pet = sched_clock(); + return 0; + } + + wdog_data->ops->disable_wdt(wdog_data); + wdog_data->enabled = false; + wdog_data->last_pet = sched_clock(); + return 0; +} +EXPORT_SYMBOL(qcom_wdt_pet_suspend); + +/** + * qcom_wdt_pet_resume() - Resumes qcom watchdog after a suspend. + * + * @dev: qcom watchdog device structure + * + * All watchdogs should have the ability to be resumed after a suspend. + * This will cause the watchdog counter to be reset and resumed. + * + */ +int qcom_wdt_pet_resume(struct device *dev) +{ + struct msm_watchdog_data *wdog_data = + (struct msm_watchdog_data *)dev_get_drvdata(dev); + unsigned long delay_time = 0; + + if (!wdog_data) + return 0; + + if (wdog_data->user_pet_enabled) { + delay_time = msecs_to_jiffies(wdog_data->bark_time + 3 * 1000); + wdog_data->user_pet_timer.expires = jiffies + delay_time; + add_timer(&wdog_data->user_pet_timer); + } + + delay_time = msecs_to_jiffies(wdog_data->pet_time); + spin_lock(&wdog_data->freeze_lock); + wdog_data->pet_timer.expires = jiffies + delay_time; + add_timer(&wdog_data->pet_timer); + wdog_data->freeze_in_progress = false; + spin_unlock(&wdog_data->freeze_lock); + if (wdog_data->wakeup_irq_enable) { + wdog_data->ops->reset_wdt(wdog_data); + wdog_data->last_pet = sched_clock(); + return 0; + } + + wdog_data->ops->enable_wdt(1, wdog_data); + wdog_data->ops->reset_wdt(wdog_data); + wdog_data->enabled = true; + wdog_data->last_pet = sched_clock(); + return 0; +} +EXPORT_SYMBOL(qcom_wdt_pet_resume); +#endif + +static void qcom_wdt_reset_on_oops(struct msm_watchdog_data *wdog_dd, + int timeout) +{ + wdog_dd->ops->reset_wdt(wdog_dd); + wdog_dd->ops->set_bark_time((timeout + 10) * 1000, + wdog_dd); + wdog_dd->ops->set_bite_time((timeout + 10) * 1000, + wdog_dd); +} + +static int qcom_wdt_panic_handler(struct notifier_block *this, + unsigned long event, void *ptr) +{ + struct msm_watchdog_data *wdog_dd = container_of(this, + struct msm_watchdog_data, panic_blk); + + wdog_dd->in_panic = true; + if (WDOG_BITE_EARLY_PANIC) { + pr_info("Triggering early bite\n"); + qcom_wdt_trigger_bite(); + } + if (panic_timeout == 0) { + wdog_dd->ops->disable_wdt(wdog_dd); + } else { + qcom_wdt_reset_on_oops(wdog_dd, panic_timeout); + } + return NOTIFY_DONE; +} + +#ifdef CONFIG_QCOM_MINIDUMP_PANIC_DUMP +static int qcom_wdt_die_handler(struct notifier_block *this, + unsigned long val, void *data) +{ + struct msm_watchdog_data *wdog_dd = container_of(this, + struct msm_watchdog_data, die_blk); + + qcom_wdt_reset_on_oops(wdog_dd, 5); + return NOTIFY_DONE; +} + +static void qcom_wdt_register_die_notifier(struct msm_watchdog_data *wdog_dd) +{ + wdog_dd->die_blk.notifier_call = qcom_wdt_die_handler; + wdog_dd->die_blk.priority = INT_MAX - 1; + register_die_notifier(&wdog_dd->die_blk); +} + +static void qcom_wdt_unregister_die_notifier(struct msm_watchdog_data *wdog_dd) +{ + unregister_die_notifier(&wdog_dd->die_blk); +} +#else +static void qcom_wdt_register_die_notifier(struct msm_watchdog_data *wdog_dd) { } +static void qcom_wdt_unregister_die_notifier(struct msm_watchdog_data *wdog_dd) { } +#endif + +static void qcom_wdt_disable(struct msm_watchdog_data *wdog_dd) +{ + wdog_dd->ops->disable_wdt(wdog_dd); + + if (wdog_dd->irq_ppi) { + disable_percpu_irq(wdog_dd->bark_irq); + free_percpu_irq(wdog_dd->bark_irq, + (void __percpu *)wdog_dd->wdog_cpu_dd); + } else { + devm_free_irq(wdog_dd->dev, wdog_dd->bark_irq, wdog_dd); + } + + wdog_dd->enabled = false; + /*Ensure all cpus see update to enable*/ + smp_mb(); + atomic_notifier_chain_unregister(&panic_notifier_list, + &wdog_dd->panic_blk); + qcom_wdt_unregister_die_notifier(wdog_dd); + unregister_restart_handler(&wdog_dd->restart_blk); + del_timer_sync(&wdog_dd->pet_timer); + if (wdog_dd->user_pet_enabled) + del_timer_sync(&wdog_dd->user_pet_timer); + wdog_dd->ops->disable_wdt(wdog_dd); + dev_err(wdog_dd->dev, "QCOM Apps Watchdog deactivated\n"); +} + +static int restart_wdog_handler(struct notifier_block *this, + unsigned long event, void *ptr) +{ + struct msm_watchdog_data *wdog_dd = container_of(this, + struct msm_watchdog_data, restart_blk); + if (WDOG_BITE_ON_PANIC && wdog_dd->in_panic) { + /* + * Trigger a watchdog bite here and if this fails, + * device will take the usual restart path. + */ + pr_info("Triggering late bite\n"); + qcom_wdt_trigger_bite(); + } + return NOTIFY_DONE; +} + +static ssize_t qcom_wdt_disable_get(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + int ret; + int disable_val; + + mutex_lock(&wdog_dd->disable_lock); + disable_val = wdog_dd->enabled ? 0 : 1; + ret = scnprintf(buf, PAGE_SIZE, "%d\n", disable_val); + mutex_unlock(&wdog_dd->disable_lock); + return ret; +} + +static ssize_t qcom_wdt_disable_set(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + u8 disable; + int ret; + + ret = kstrtou8(buf, 10, &disable); + if (ret) { + dev_err(wdog_dd->dev, "invalid user input\n"); + return ret; + } + if (disable == 1) { + mutex_lock(&wdog_dd->disable_lock); + if (!wdog_dd->enabled) { + dev_err(wdog_dd->dev, "MSM Apps Watchdog already disabled\n"); + mutex_unlock(&wdog_dd->disable_lock); + return count; + } + ret = qcom_scm_sec_wdog_deactivate(); + if (ret) { + dev_err(wdog_dd->dev, + "Failed to deactivate secure wdog, ret = %d\n", + ret); + } + qcom_wdt_disable(wdog_dd); + mutex_unlock(&wdog_dd->disable_lock); + } else { + pr_err("invalid operation, only disable = 1 supported\n"); + return -EINVAL; + } + return count; +} + +static DEVICE_ATTR(disable, 0600, qcom_wdt_disable_get, qcom_wdt_disable_set); + +/* + * Userspace Watchdog Support: + * Write 1 to the "user_pet_enabled" file to enable hw support for a + * userspace watchdog. + * Userspace is required to pet the watchdog by continuing to write 1 + * to this file in the expected interval. + * Userspace may disable this requirement by writing 0 to this same + * file. + */ +static void __qcom_wdt_user_pet(struct msm_watchdog_data *wdog_dd) +{ + wdog_dd->user_pet_complete = true; + wake_up(&wdog_dd->pet_complete); +} + +static ssize_t qcom_wdt_user_pet_enabled_get(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + int ret; + + ret = scnprintf(buf, PAGE_SIZE, "%d\n", + wdog_dd->user_pet_enabled); + return ret; +} + +static ssize_t qcom_wdt_user_pet_enabled_set(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + int ret; + unsigned long delay_time = 0; + bool already_enabled = wdog_dd->user_pet_enabled; + + ret = strtobool(buf, &wdog_dd->user_pet_enabled); + if (ret) { + dev_err(wdog_dd->dev, "invalid user input\n"); + return ret; + } + + delay_time = msecs_to_jiffies(wdog_dd->bark_time + 3 * 1000); + if (wdog_dd->user_pet_enabled) + mod_timer(&wdog_dd->user_pet_timer, jiffies + delay_time); + else if (already_enabled) + del_timer_sync(&wdog_dd->user_pet_timer); + + __qcom_wdt_user_pet(wdog_dd); + + return count; +} + +static DEVICE_ATTR(user_pet_enabled, 0600, qcom_wdt_user_pet_enabled_get, + qcom_wdt_user_pet_enabled_set); + +static ssize_t qcom_wdt_pet_time_get(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + int ret; + + ret = scnprintf(buf, PAGE_SIZE, "%d\n", wdog_dd->pet_time); + return ret; +} + +static DEVICE_ATTR(pet_time, 0400, qcom_wdt_pet_time_get, NULL); + +static ssize_t wakeup_enable_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + + return scnprintf(buf, PAGE_SIZE, "%d\n", wdog_dd->wakeup_irq_enable); +} +static ssize_t wakeup_enable_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct msm_watchdog_data *wdog_dd = dev_get_drvdata(dev); + u8 enable; + int ret; + + ret = kstrtou8(buf, 10, &enable); + if (ret || enable > 1) { + dev_err(wdog_dd->dev, "invalid user input\n"); + return ret ? : -EINVAL; + } + mutex_lock(&wdog_dd->disable_lock); + /* echo 1 > wakeup_enable means wakeup irq is enabled */ + wdog_dd->wakeup_irq_enable = enable; + if (wdog_dd->enabled) { + u8 val = BIT(EN); + + if (wdog_dd->wakeup_irq_enable) + val |= BIT(UNMASKED_INT_EN); + wdog_dd->ops->enable_wdt(val, wdog_dd); + } + mutex_unlock(&wdog_dd->disable_lock); + return count; +} +static DEVICE_ATTR_ADMIN_RW(wakeup_enable); + +static void qcom_wdt_keep_alive_response(void *info) +{ + struct msm_watchdog_data *wdog_dd = info; + int cpu = smp_processor_id(); + + cpumask_set_cpu(cpu, &wdog_dd->alive_mask); + wdog_dd->ping_end[cpu] = sched_clock(); + /* Make sure alive mask is cleared and set in order */ + smp_mb(); +} + +/* + * If this function does not return, it implies one of the + * other cpu's is not responsive. + */ +static void qcom_wdt_ping_other_cpus(struct msm_watchdog_data *wdog_dd) +{ + int cpu; + + cpumask_clear(&wdog_dd->alive_mask); + /* Make sure alive mask is cleared and set in order */ + smp_mb(); + for_each_cpu(cpu, cpu_online_mask) { + if (!wdog_dd->cpu_idle_pc_state[cpu]) { + wdog_dd->ping_start[cpu] = sched_clock(); + smp_call_function_single(cpu, + qcom_wdt_keep_alive_response, + wdog_dd, 1); + } + } +} + +static void qcom_wdt_pet_task_wakeup(struct timer_list *t) +{ + struct msm_watchdog_data *wdog_dd = + from_timer(wdog_dd, t, pet_timer); + wdog_dd->timer_expired = true; + wdog_dd->timer_fired = sched_clock(); + wake_up(&wdog_dd->pet_complete); +} +static void qcom_wdt_user_pet_bite(struct timer_list *t) +{ + struct msm_watchdog_data *wdog_dd = + from_timer(wdog_dd, t, user_pet_timer); + if (!wdog_dd->user_pet_complete) { + dev_info(wdog_dd->dev, "QCOM Apps Watchdog user pet timeout!\n"); + qcom_wdt_trigger_bite(); + } +} + +static __ref int qcom_wdt_kthread(void *arg) +{ + struct msm_watchdog_data *wdog_dd = arg; + unsigned long delay_time = 0; + struct sched_param param = {.sched_priority = MAX_RT_PRIO-1}; + int ret, cpu; + + sched_setscheduler(current, SCHED_FIFO, ¶m); + while (!kthread_should_stop()) { + do { + ret = wait_event_interruptible(wdog_dd->pet_complete, + wdog_dd->timer_expired); + } while (ret != 0); + + wdog_dd->thread_start = sched_clock(); + for_each_cpu(cpu, cpu_present_mask) + wdog_dd->ping_start[cpu] = wdog_dd->ping_end[cpu] = 0; + + if (wdog_dd->do_ipi_ping) + qcom_wdt_ping_other_cpus(wdog_dd); + + do { + ret = wait_event_interruptible(wdog_dd->pet_complete, + wdog_dd->user_pet_complete); + } while (ret != 0); + + wdog_dd->timer_expired = false; + wdog_dd->user_pet_complete = !wdog_dd->user_pet_enabled; + + if (wdog_dd->enabled) { + delay_time = msecs_to_jiffies(wdog_dd->pet_time); + wdog_dd->ops->reset_wdt(wdog_dd); + wdog_dd->last_pet = sched_clock(); + } + /* Check again before scheduling + * Could have been changed on other cpu + */ + if (!kthread_should_stop()) { + spin_lock(&wdog_dd->freeze_lock); + if (!wdog_dd->freeze_in_progress) + mod_timer(&wdog_dd->pet_timer, + jiffies + delay_time); + spin_unlock(&wdog_dd->freeze_lock); + } + + queue_irq_counts_work(&wdog_dd->irq_counts_work); + } + return 0; +} + +static int qcom_wdt_cpu_pm_notify(struct notifier_block *this, + unsigned long action, void *v) +{ + struct msm_watchdog_data *wdog_dd = container_of(this, + struct msm_watchdog_data, wdog_cpu_pm_nb); + int cpu; + + cpu = raw_smp_processor_id(); + + switch (action) { + case CPU_PM_ENTER: + wdog_dd->cpu_idle_pc_state[cpu] = 1; + break; + case CPU_PM_ENTER_FAILED: + case CPU_PM_EXIT: + wdog_dd->cpu_idle_pc_state[cpu] = 0; + break; + } + + return NOTIFY_OK; +} + +/** + * qcom_wdt_remove() - Removes the watchdog and stops it's kthread. + * + * @pdev: watchdog platform_device + * + * Upon the removal of the module all watchdog data along with the kthread + * will be cleaned up and the watchdog device will be removed from memory. + * + */ +int qcom_wdt_remove(struct platform_device *pdev) +{ + struct msm_watchdog_data *wdog_dd = platform_get_drvdata(pdev); + + if (!IPI_CORES_IN_LPM) + cpu_pm_unregister_notifier(&wdog_dd->wdog_cpu_pm_nb); + + mutex_lock(&wdog_dd->disable_lock); + if (wdog_dd->enabled) + qcom_wdt_disable(wdog_dd); + + mutex_unlock(&wdog_dd->disable_lock); + device_remove_file(wdog_dd->dev, &dev_attr_disable); + if (wdog_dd->irq_ppi) + free_percpu((void __percpu *)wdog_dd->wdog_cpu_dd); + irq_dispose_mapping(wdog_dd->bark_irq); + dev_info(wdog_dd->dev, "QCOM Apps Watchdog Exit - Deactivated\n"); + del_timer_sync(&wdog_dd->pet_timer); + if (wdog_dd->user_pet_enabled) + del_timer_sync(&wdog_dd->user_pet_timer); + wdog_dd->timer_expired = true; + wdog_dd->user_pet_complete = true; + kthread_stop(wdog_dd->watchdog_task); + flush_work(&wdog_dd->irq_counts_work); + return 0; +} +EXPORT_SYMBOL(qcom_wdt_remove); + +/** + * qcom_wdt_trigger_bite - Executes a watchdog bite. + * + * Return: function will not return, to allow for the bite to occur + */ +void qcom_wdt_trigger_bite(void) +{ + if (!wdog_data) + return; + compute_irq_stat(&wdog_data->irq_counts_work); + dev_err(wdog_data->dev, "Causing a QCOM Apps Watchdog bite!\n"); + wdog_data->ops->show_wdt_status(wdog_data); + wdog_data->ops->set_bite_time(1, wdog_data); + wdog_data->ops->reset_wdt(wdog_data); + /* Delay to make sure bite occurs */ + mdelay(10000); + /* + * This function induces the non-secure bite and control + * should not return to the calling function. Non-secure + * bite interrupt is affined to all the cores and it may + * not be handled by the same cores which configured + * non-secure bite. So add forever loop here. + */ + while (1) + udelay(1); + +} +EXPORT_SYMBOL(qcom_wdt_trigger_bite); + +static irqreturn_t qcom_wdt_bark_handler(int irq, void *dev_id) +{ + struct msm_watchdog_data *wdog_dd = dev_id; + unsigned long nanosec_rem; + unsigned long long t = sched_clock(); + + nanosec_rem = do_div(t, 1000000000); + dev_info(wdog_dd->dev, "QCOM Apps Watchdog bark! Now = %lu.%06lu\n", + (unsigned long) t, nanosec_rem / 1000); + + nanosec_rem = do_div(wdog_dd->last_pet, 1000000000); + dev_info(wdog_dd->dev, "QCOM Apps Watchdog last pet at %lu.%06lu\n", + (unsigned long) wdog_dd->last_pet, nanosec_rem / 1000); + if (wdog_dd->do_ipi_ping) + qcom_wdt_dump_cpu_alive_mask(wdog_dd); + + if (wdog_dd->freeze_in_progress) + dev_info(wdog_dd->dev, "Suspend in progress\n"); + + md_dump_process(); + qcom_wdt_trigger_bite(); + + return IRQ_HANDLED; +} + +static irqreturn_t qcom_wdt_ppi_bark(int irq, void *dev_id_percpu) +{ + void *dev_id = raw_cpu_ptr((void __percpu *)dev_id_percpu); + struct msm_watchdog_data *wdog_dd = *((struct msm_watchdog_data **)dev_id); + + return qcom_wdt_bark_handler(irq, wdog_dd); +} + +static int qcom_wdt_init_sysfs(struct msm_watchdog_data *wdog_dd) +{ + int error = 0; + + error |= device_create_file(wdog_dd->dev, &dev_attr_disable); + error |= device_create_file(wdog_dd->dev, &dev_attr_wakeup_enable); + if (QCOM_WATCHDOG_USERSPACE_PET) { + error |= device_create_file(wdog_dd->dev, &dev_attr_pet_time); + error |= device_create_file(wdog_dd->dev, + &dev_attr_user_pet_enabled); + } + if (error) + dev_err(wdog_dd->dev, "cannot create sysfs attribute\n"); + + return error; +} + +static int qcom_wdt_init(struct msm_watchdog_data *wdog_dd, + struct platform_device *pdev) +{ + unsigned long delay_time; + uint32_t val; + int ret; + void *wdog_cpu_dd_v; + + if (wdog_dd->irq_ppi) { + wdog_dd->wdog_cpu_dd = alloc_percpu(struct msm_watchdog_data *); + if (!wdog_dd->wdog_cpu_dd) { + dev_err(wdog_dd->dev, "failed to allocate cpu data\n"); + return -ENOMEM; + } + wdog_cpu_dd_v = raw_cpu_ptr((void __percpu *)wdog_dd->wdog_cpu_dd); + *((struct msm_watchdog_data **)wdog_cpu_dd_v) = wdog_dd; + ret = request_percpu_irq(wdog_dd->bark_irq, qcom_wdt_ppi_bark, + "apps_wdog_bark", + (void __percpu *)wdog_dd->wdog_cpu_dd); + if (ret) { + dev_err(wdog_dd->dev, "failed to request bark irq\n"); + free_percpu((void __percpu *)wdog_dd->wdog_cpu_dd); + return ret; + } + } else { + ret = devm_request_irq(wdog_dd->dev, wdog_dd->bark_irq, + qcom_wdt_bark_handler, + IRQF_TRIGGER_RISING | IRQF_NO_SUSPEND, + "apps_wdog_bark", wdog_dd); + if (ret) { + dev_err(wdog_dd->dev, "failed to request bark irq: %d\n", ret); + return -EINVAL; + } + } + INIT_WORK(&wdog_dd->irq_counts_work, compute_irq_stat); + atomic_set(&wdog_dd->irq_counts_running, 0); + delay_time = msecs_to_jiffies(wdog_dd->pet_time); + wdog_dd->ops->set_bark_time(wdog_dd->bark_time, wdog_dd); + wdog_dd->ops->set_bite_time(wdog_dd->bark_time + 3 * 1000, wdog_dd); + wdog_dd->panic_blk.priority = INT_MAX - 1; + wdog_dd->panic_blk.notifier_call = qcom_wdt_panic_handler; + atomic_notifier_chain_register(&panic_notifier_list, + &wdog_dd->panic_blk); + qcom_wdt_register_die_notifier(wdog_dd); + wdog_dd->restart_blk.priority = 255; + wdog_dd->restart_blk.notifier_call = restart_wdog_handler; + register_restart_handler(&wdog_dd->restart_blk); + mutex_init(&wdog_dd->disable_lock); + init_waitqueue_head(&wdog_dd->pet_complete); + wdog_dd->timer_expired = false; + wdog_dd->user_pet_complete = true; + wdog_dd->user_pet_enabled = false; + spin_lock_init(&wdog_dd->freeze_lock); + wdog_dd->freeze_in_progress = false; + wake_up_process(wdog_dd->watchdog_task); + timer_setup(&wdog_dd->pet_timer, qcom_wdt_pet_task_wakeup, 0); + wdog_dd->pet_timer.expires = jiffies + delay_time; + add_timer(&wdog_dd->pet_timer); + timer_setup(&wdog_dd->user_pet_timer, qcom_wdt_user_pet_bite, 0); + val = BIT(EN); + if (wdog_dd->wakeup_irq_enable) + val |= BIT(UNMASKED_INT_EN); + + ret = wdog_dd->ops->enable_wdt(val, wdog_dd); + if (ret) { + atomic_notifier_chain_unregister(&panic_notifier_list, + &wdog_dd->panic_blk); + qcom_wdt_unregister_die_notifier(wdog_dd); + unregister_restart_handler(&wdog_dd->restart_blk); + + if (wdog_dd->irq_ppi) { + free_percpu_irq(wdog_dd->bark_irq, + (void __percpu *)wdog_dd->wdog_cpu_dd); + free_percpu((void __percpu *)wdog_dd->wdog_cpu_dd); + } + + del_timer_sync(&wdog_dd->pet_timer); + flush_work(&wdog_dd->irq_counts_work); + dev_err(wdog_dd->dev, "Failed Initializing QCOM Apps Watchdog\n"); + return ret; + } + + wdog_dd->ops->reset_wdt(wdog_dd); + wdog_dd->last_pet = sched_clock(); + wdog_dd->enabled = true; + + qcom_wdt_init_sysfs(wdog_dd); + + if (wdog_dd->irq_ppi) + enable_percpu_irq(wdog_dd->bark_irq, 0); + if (!IPI_CORES_IN_LPM) { + wdog_dd->wdog_cpu_pm_nb.notifier_call = qcom_wdt_cpu_pm_notify; + cpu_pm_register_notifier(&wdog_dd->wdog_cpu_pm_nb); + } + dev_info(wdog_dd->dev, "QCOM Apps Watchdog Initialized\n"); + + return 0; +} + +static void qcom_wdt_dump_pdata(struct msm_watchdog_data *pdata) +{ + dev_dbg(pdata->dev, "wdog bark_time %d", pdata->bark_time); + dev_dbg(pdata->dev, "wdog pet_time %d", pdata->pet_time); + dev_dbg(pdata->dev, "wdog perform ipi ping %d", pdata->do_ipi_ping); + dev_dbg(pdata->dev, "wdog base address is 0x%lx\n", (unsigned long) + pdata->base); +} + +static void qcom_wdt_dt_to_pdata(struct platform_device *pdev, + struct msm_watchdog_data *pdata) +{ + pdata->bark_irq = platform_get_irq(pdev, 0); + pdata->irq_ppi = irq_is_percpu(pdata->bark_irq); + pdata->bark_time = QCOM_WATCHDOG_BARK_TIME; + pdata->pet_time = QCOM_WATCHDOG_PET_TIME; + pdata->do_ipi_ping = QCOM_WATCHDOG_IPI_PING; + pdata->wakeup_irq_enable = QCOM_WATCHDOG_WAKEUP_ENABLE; + qcom_wdt_dump_pdata(pdata); +} + +/** + * qcom_wdt_register() - Creates QCOM Apps watchdog device. + * + * @pdev: watchdog platform_device + * @ops: watchdog operations + * + * All QCOM Apps watchdogs should be created the same way, this acts + * as a framework for this purpose. + * + * 0 on success, negative errno on failure. + */ +int qcom_wdt_register(struct platform_device *pdev, + struct msm_watchdog_data *wdog_dd, + char *wdog_dd_name) +{ + struct md_region md_entry; + int ret; + + if (!pdev || !wdog_dd || !wdog_dd_name) { + pr_err("wdt_register input incorrect\n"); + return -EINVAL; + } + + qcom_wdt_dt_to_pdata(pdev, wdog_dd); + wdog_data = wdog_dd; + wdog_dd->dev = &pdev->dev; + platform_set_drvdata(pdev, wdog_dd); + cpumask_clear(&wdog_dd->alive_mask); + wdog_dd->watchdog_task = kthread_create(qcom_wdt_kthread, wdog_dd, + wdog_dd_name); + if (IS_ERR(wdog_dd->watchdog_task)) { + ret = PTR_ERR(wdog_dd->watchdog_task); + goto err; + } + ret = qcom_wdt_init(wdog_dd, pdev); + if (ret) { + kthread_stop(wdog_dd->watchdog_task); + goto err; + } + + /* Add wdog info to minidump table */ + strlcpy(md_entry.name, "KWDOGDATA", sizeof(md_entry.name)); + md_entry.virt_addr = (uintptr_t)wdog_dd; + md_entry.phys_addr = virt_to_phys(wdog_dd); + md_entry.size = sizeof(*wdog_dd); + if (msm_minidump_add_region(&md_entry) < 0) + dev_err(wdog_dd->dev, "Failed to add Wdt data in Minidump\n"); + + return 0; +err: + return ret; +} +EXPORT_SYMBOL(qcom_wdt_register); + +MODULE_DESCRIPTION("QCOM Watchdog Driver Core"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/virt/Kconfig b/drivers/virt/Kconfig index 8061e8ef449f..823663d67a95 100644 --- a/drivers/virt/Kconfig +++ b/drivers/virt/Kconfig @@ -36,4 +36,6 @@ source "drivers/virt/vboxguest/Kconfig" source "drivers/virt/nitro_enclaves/Kconfig" source "drivers/virt/acrn/Kconfig" + +source "drivers/virt/gunyah/Kconfig" endif diff --git a/drivers/virt/Makefile b/drivers/virt/Makefile index 3e272ea60cd9..add61dc0d953 100644 --- a/drivers/virt/Makefile +++ b/drivers/virt/Makefile @@ -8,3 +8,4 @@ obj-y += vboxguest/ obj-$(CONFIG_NITRO_ENCLAVES) += nitro_enclaves/ obj-$(CONFIG_ACRN_HSM) += acrn/ +obj-$(CONFIG_GUNYAH_DRIVERS) += gunyah/ diff --git a/drivers/virt/gunyah/Kconfig b/drivers/virt/gunyah/Kconfig new file mode 100644 index 000000000000..e32f62a5bd34 --- /dev/null +++ b/drivers/virt/gunyah/Kconfig @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only + +menuconfig GUNYAH_DRIVERS + bool "Gunyah Virtualization drivers" + depends on ARM64 + help + The Gunyah drivers are the helper interfaces that runs on the + virtual machines that provides support such as memory/device + sharing, IRQ sharing, IPC/signalling mechanisms, and so on. + + Say Y here to enable the drivers needed to work on Gunyah + virtualization environment. + + If you say N, all options in this submenu will be skipped and disabled. + +if GUNYAH_DRIVERS + +config GH_VIRT_WATCHDOG + tristate "Gunyah Virtual Watchdog Driver" + depends on QCOM_WDT_CORE + help + This enables the Qualcomm Technologies, Inc. watchdog module for + the Gunyah hypervisor. It provides an interface to perform watchdog + actions such as setting the bark/bite time and also petting the + watchdog in the hypervisor. + +endif diff --git a/drivers/virt/gunyah/Makefile b/drivers/virt/gunyah/Makefile new file mode 100644 index 000000000000..79aaed4d2f24 --- /dev/null +++ b/drivers/virt/gunyah/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_GH_VIRT_WATCHDOG)+= gh_virt_wdt.o diff --git a/drivers/virt/gunyah/gh_virt_wdt.c b/drivers/virt/gunyah/gh_virt_wdt.c new file mode 100644 index 000000000000..56f0c1d0cc6d --- /dev/null +++ b/drivers/virt/gunyah/gh_virt_wdt.c @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved. + * + */ + +#include +#include +#include +#include +#include +#include + +#define VIRT_WDT_CONTROL \ + ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ + ARM_SMCCC_OWNER_VENDOR_HYP, 0x0005) +#define VIRT_WDT_STATUS \ + ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ + ARM_SMCCC_OWNER_VENDOR_HYP, 0x0006) +#define VIRT_WDT_PET \ + ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ + ARM_SMCCC_OWNER_VENDOR_HYP, 0x0007) +#define VIRT_WDT_SET_TIME \ + ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32,\ + ARM_SMCCC_OWNER_VENDOR_HYP, 0x0008) +#define VIRT_WDT_NO_CHANGE 0xFFFF + +/** + * gh_wdt_call() - Sends ARM SMCCC 1.1 Calls to the hypervisor + * + * @smc_id: The smc id needed to interact with the watchdog in the hypervisor + * @arg1: A u32 value to be sent to the the hypervisor + * @arg2: A u16 value to be sent to the the hypervisor + * @arg3: A u16 value to be sent to the the hypervisor + * + * The hypervisor takes input via ARM SMCCC Calls. The position of + * these values matter. u16 values are needed to set both the bark + * and bite time. In all other cases only the u32 (arg1) value is required. + * + * return: 0 on success, negative errno on failure. + */ +static struct arm_smccc_res gh_wdt_call(u32 smc_id, u32 arg1, + u16 arg2, u16 arg3) +{ + struct arm_smccc_res res; + + if (smc_id == VIRT_WDT_SET_TIME) + /* virtual watchdog expecting u16 values for bark and bite */ + arm_smccc_1_1_smc(smc_id, arg2, arg3, &res); + else + arm_smccc_1_1_smc(smc_id, arg1, &res); + + return res; +} + +/** + * gh_set_wdt_bark() - Sets the bark time for the virtual watchdog + * + * @time: A u32 value to be converted to milliseconds (u16) + * @wdog_dd: The qcom watchdog data structure + * + * The hypervisor requires both the bark and the bite time in the same + * call. To update one and not the other, the value VIRT_WDT_NO_CHANGE + * is used. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_set_wdt_bark(u32 time, struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + u16 bark_time; + + bark_time = (u16) time; + res = gh_wdt_call(VIRT_WDT_SET_TIME, 0, bark_time, VIRT_WDT_NO_CHANGE); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed to set bark time for vDOG, hret = %d ret = %d\n", + hret, ret); + } + + return ret; +} + +/** + * gh_set_wdt_bite() - Sets the bite time for the virtual watchdog + * + * @time: A u32 value to be converted to milliseconds (u16) + * @wdog_dd: The qcom watchdog data structure + * + * The hypervisor requires both the bark and the bite time in the same + * call. To update one and not the other, the value VIRT_WDT_NO_CHANGE + * is used. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_set_wdt_bite(u32 time, struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + u16 bite_time; + + bite_time = (u16) time; + res = gh_wdt_call(VIRT_WDT_SET_TIME, 0, VIRT_WDT_NO_CHANGE, bite_time); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed to set bite time for vWDOG, hret = %d ret = %d\n", + hret, ret); + } + + return ret; +} + +/** + * gh_reset_wdt() - Resets the virtual watchdog timer + * + * @wdog_dd: The qcom watchdog data structure + * + * VIRT_WDT_PET is used to reset the virtual watchdog. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_reset_wdt(struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + + res = gh_wdt_call(VIRT_WDT_PET, 0, 0, 0); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed to reset vWDOG, hret = %d ret = %d\n", + hret, ret); + } + + return ret; +} + +/** + * gh_enable_wdt() - Enables the virtual watchdog + * + * @wdog_dd: The qcom watchdog data structure + * @state: state value to send to watchdog + * + * VIRT_WDT_CONTROL is used to enable the virtual watchdog. + * Bit 0 is used to enable the watchdog. When this Bit is set to + * 1 the watchdog is enabled. NOTE: Bit 1 must always be set to + * 1 as this bit is reserved in the hypervisor and Bit 1 is + * expected to be 1. If this Bit is not set, the hypervisor will + * return an error. So to enable the watchdog you must use the value 3. + * An error from the hypervisor is expected if you try to enable the + * watchdog when its already enabled. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_enable_wdt(u32 state, struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + + if (wdog_dd->enabled) { + dev_err(wdog_dd->dev, "vWDT already enabled\n"); + return 0; + } + res = gh_wdt_call(VIRT_WDT_CONTROL, 3, 0, 0); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed enabling vWDOG, hret = %d ret = %d\n", + hret, ret); + } + + return ret; +} + +/** + * gh_disable_wdt() - Disables the virtual watchdog + * + * @wdog_dd: The qcom watchdog data structure + * + * VIRT_WDT_CONTROL is used to disable the virtual watchdog. + * Bit 0 is used to disable the watchdog. When this Bit is set to + * 0 the watchdog is disabled. NOTE: Bit 1 must always be set to + * 1 as this bit is reserved in the hypervisor and Bit 1 is + * expected to be 1. If this Bit is not set, the hypervisor will + * return an error. So to disable the watchdog you must use the value 2. + * An error from the hypervisor is expected if you try to disable the + * watchdog when its already disabled. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_disable_wdt(struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + + if (!wdog_dd->enabled) { + dev_err(wdog_dd->dev, "vWDT already disabled\n"); + return 0; + } + res = gh_wdt_call(VIRT_WDT_CONTROL, 2, 0, 0); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed disabling VDOG, hret = %d ret = %d\n", + hret, ret); + } + + return ret; +} + +/** + * gh_get_wdt_status() - Displays the status of the virtual watchdog + * + * @wdog_dd: The qcom watchdog data structure + * + * VIRT_WDT_STATUS is used to display status of the virtual watchdog. + * + * return: 0 on success, negative errno on failure. + */ +static int gh_show_wdt_status(struct msm_watchdog_data *wdog_dd) +{ + struct arm_smccc_res res; + int hret, ret; + + res = gh_wdt_call(VIRT_WDT_STATUS, 0, 0, 0); + hret = res.a0; + ret = gh_remap_error(hret); + if (hret) { + dev_err(wdog_dd->dev, "failed to get vWDOG status, hret = %d ret = %d\n", + hret, ret); + } else { + dev_err(wdog_dd->dev, + "vWdog-CTL: %d, vWdog-time since last pet: %d, vWdog-expired status: %d\n", + res.a1 & 1, res.a2, (res.a1 >> 31) & 1); + } + + return ret; +} + +static struct qcom_wdt_ops gh_wdt_ops = { + .set_bark_time = gh_set_wdt_bark, + .set_bite_time = gh_set_wdt_bite, + .reset_wdt = gh_reset_wdt, + .enable_wdt = gh_enable_wdt, + .disable_wdt = gh_disable_wdt, + .show_wdt_status = gh_show_wdt_status +}; + +static int gh_wdt_probe(struct platform_device *pdev) +{ + struct msm_watchdog_data *wdog_dd; + + wdog_dd = devm_kzalloc(&pdev->dev, sizeof(*wdog_dd), GFP_KERNEL); + if (!wdog_dd) + return -ENOMEM; + wdog_dd->ops = &gh_wdt_ops; + + return qcom_wdt_register(pdev, wdog_dd, "gh-watchdog"); +} + +static const struct dev_pm_ops gh_wdt_dev_pm_ops = { +#ifdef CONFIG_PM_SLEEP + .suspend_late = qcom_wdt_pet_suspend, + .resume_early = qcom_wdt_pet_resume, +#endif +}; + +static const struct of_device_id gh_wdt_match_table[] = { + { .compatible = "qcom,gh-watchdog" }, + { .compatible = "qcom,hh-watchdog" }, + {} +}; + +static struct platform_driver gh_wdt_driver = { + .probe = gh_wdt_probe, + .remove = qcom_wdt_remove, + .driver = { + .name = "gh-watchdog", + .pm = &gh_wdt_dev_pm_ops, + .of_match_table = gh_wdt_match_table, + }, +}; + +static int __init init_watchdog(void) +{ + return platform_driver_register(&gh_wdt_driver); +} + +#if IS_MODULE(CONFIG_GH_VIRT_WATCHDOG) +module_init(init_watchdog); +#else +pure_initcall(init_watchdog); +#endif + +static __exit void exit_watchdog(void) +{ + platform_driver_unregister(&gh_wdt_driver); +} +module_exit(exit_watchdog); +MODULE_DESCRIPTION("QCOM Gunyah Watchdog Driver"); +MODULE_LICENSE("GPL v2"); diff --git a/include/linux/gunyah/gh_errno.h b/include/linux/gunyah/gh_errno.h new file mode 100644 index 000000000000..5c013cb175b5 --- /dev/null +++ b/include/linux/gunyah/gh_errno.h @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2020-2021, The Linux Foundation. All rights reserved. + * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved. + * + */ + +#ifndef __GH_ERRNO_H +#define __GH_ERRNO_H + +#include + +#define GH_ERROR_OK 0 +#define GH_ERROR_UNIMPLEMENTED -1 +#define GH_ERROR_RETRY -2 + +#define GH_ERROR_ARG_INVAL 1 +#define GH_ERROR_ARG_SIZE 2 +#define GH_ERROR_ARG_ALIGN 3 + +#define GH_ERROR_NOMEM 10 + +#define GH_ERROR_ADDR_OVFL 20 +#define GH_ERROR_ADDR_UNFL 21 +#define GH_ERROR_ADDR_INVAL 22 + +#define GH_ERROR_DENIED 30 +#define GH_ERROR_BUSY 31 +#define GH_ERROR_IDLE 32 + +#define GH_ERROR_IRQ_BOUND 40 +#define GH_ERROR_IRQ_UNBOUND 41 + +#define GH_ERROR_CSPACE_CAP_NULL 50 +#define GH_ERROR_CSPACE_CAP_REVOKED 51 +#define GH_ERROR_CSPACE_WRONG_OBJ_TYPE 52 +#define GH_ERROR_CSPACE_INSUF_RIGHTS 53 +#define GH_ERROR_CSPACE_FULL 54 + +#define GH_ERROR_MSGQUEUE_EMPTY 60 +#define GH_ERROR_MSGQUEUE_FULL 61 + +static inline int gh_remap_error(int gh_error) +{ + switch (gh_error) { + case GH_ERROR_OK: + return 0; + case GH_ERROR_NOMEM: + return -ENOMEM; + case GH_ERROR_DENIED: + case GH_ERROR_CSPACE_CAP_NULL: + case GH_ERROR_CSPACE_CAP_REVOKED: + case GH_ERROR_CSPACE_WRONG_OBJ_TYPE: + case GH_ERROR_CSPACE_INSUF_RIGHTS: + case GH_ERROR_CSPACE_FULL: + return -EACCES; + case GH_ERROR_BUSY: + case GH_ERROR_IDLE: + return -EBUSY; + case GH_ERROR_IRQ_BOUND: + case GH_ERROR_IRQ_UNBOUND: + case GH_ERROR_MSGQUEUE_FULL: + case GH_ERROR_MSGQUEUE_EMPTY: + return -EPERM; + case GH_ERROR_UNIMPLEMENTED: + case GH_ERROR_ARG_INVAL: + case GH_ERROR_ARG_SIZE: + case GH_ERROR_ARG_ALIGN: + case GH_ERROR_ADDR_OVFL: + case GH_ERROR_ADDR_UNFL: + case GH_ERROR_ADDR_INVAL: + default: + return -EINVAL; + } +} + +#endif diff --git a/include/soc/qcom/watchdog.h b/include/soc/qcom/watchdog.h new file mode 100644 index 000000000000..63efc1f5dc8e --- /dev/null +++ b/include/soc/qcom/watchdog.h @@ -0,0 +1,169 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copyright (c) 2019-2020 The Linux Foundation. All rights reserved. + * Copyright (c) 2021 Qualcomm Innovation Center, Inc. All rights reserved. + */ + +#ifndef _SOC_QCOM_WATCHDOG_H_ +#define _SOC_QCOM_WATCHDOG_H_ + +#ifdef CONFIG_QCOM_FORCE_WDOG_BITE_ON_PANIC +#define WDOG_BITE_ON_PANIC 1 +#else +#define WDOG_BITE_ON_PANIC 0 +#endif + +#ifdef CONFIG_QCOM_WDOG_BITE_EARLY_PANIC +#define WDOG_BITE_EARLY_PANIC 1 +#else +#define WDOG_BITE_EARLY_PANIC 0 +#endif + +/* + * Watchdog ipi optimization: + * Does not ping cores in low power mode at pet time to save power. + * This feature is enabled by default. + * + * Can be turned off, by enabling CONFIG_QCOM_WDOG_IPI_ENABLE. + */ +#ifdef CONFIG_QCOM_WDOG_IPI_ENABLE +#define IPI_CORES_IN_LPM 1 +#else +#define IPI_CORES_IN_LPM 0 +#endif + +#if IS_ENABLED(CONFIG_QCOM_WDT_CORE) +#include +#include + +/** + * The enable constant that can be used between the core framework and the + * watchdog driver. + */ +#define EN 0 +#define UNMASKED_INT_EN 1 + +/* Watchdog property values */ +#define QCOM_WATCHDOG_BARK_TIME CONFIG_QCOM_WATCHDOG_BARK_TIME +#define QCOM_WATCHDOG_PET_TIME CONFIG_QCOM_WATCHDOG_PET_TIME + +#ifdef CONFIG_QCOM_WATCHDOG_IPI_PING +#define QCOM_WATCHDOG_IPI_PING 1 +#else +#define QCOM_WATCHDOG_IPI_PING 0 +#endif + +#ifdef CONFIG_QCOM_WATCHDOG_WAKEUP_ENABLE +#define QCOM_WATCHDOG_WAKEUP_ENABLE 1 +#else +#define QCOM_WATCHDOG_WAKEUP_ENABLE 0 +#endif + +#ifdef CONFIG_QCOM_WATCHDOG_USERSPACE_PET +#define QCOM_WATCHDOG_USERSPACE_PET 1 +#else +#define QCOM_WATCHDOG_USERSPACE_PET 0 +#endif + + +#define WDOG_NR_IPI 10 +#define NR_TOP_HITTERS 10 + +struct qcom_wdt_ops; +struct msm_watchdog_data; + + +/** qcom_irq_info - IRQ stats + * + * @irq: linux/virtual irq numer. + * @total_count: sum of irq occurrence count on all cpu's. + * @irq_counter: irq occurrence count on each cpu. + */ +struct qcom_irq_info { + unsigned int irq; + unsigned int total_count; + unsigned int irq_counter[NR_CPUS]; +}; + +/** qcom_wdt_ops - The msm-watchdog-devices operations + * + * @set_bark_time: The routine for setting the watchdog bark time. + * @set_bite_time: The routine for setting the watchdog bite time. + * @reset_wdt: The routine for resetting the watchdog timer. + * @enable_wdt: The routine for enabling the watchdog. + * @disable_wdt: The routine for disabling the watchdog. + * @show_wdt_status: The routine that shows the status of the watchdog. + * + * The qcom_wdt_ops structure contains a list of operations that are + * used to control the watchdog. + */ +struct qcom_wdt_ops { + int (*set_bark_time)(u32 time, struct msm_watchdog_data *wdog_dd); + int (*set_bite_time)(u32 time, struct msm_watchdog_data *wdog_dd); + int (*reset_wdt)(struct msm_watchdog_data *wdog_dd); + int (*enable_wdt)(u32 val, struct msm_watchdog_data *wdog_dd); + int (*disable_wdt)(struct msm_watchdog_data *wdog_dd); + int (*show_wdt_status)(struct msm_watchdog_data *wdog_dd); +}; + +/* + * user_pet_enable: + * Require userspace to write to a sysfs file every pet_time milliseconds. + * Disabled by default on boot. + */ +struct msm_watchdog_data { + void __iomem *base; + struct device *dev; + struct qcom_wdt_ops *ops; + unsigned int pet_time; + unsigned int bark_time; + unsigned int bark_irq; + bool do_ipi_ping; + bool in_panic; + bool wakeup_irq_enable; + bool irq_ppi; + unsigned long long last_pet; + cpumask_t alive_mask; + struct mutex disable_lock; + struct msm_watchdog_data * __percpu *wdog_cpu_dd; + struct notifier_block panic_blk; + struct notifier_block die_blk; + struct notifier_block wdog_cpu_pm_nb; + struct notifier_block restart_blk; + + bool enabled; + bool user_pet_enabled; + + struct task_struct *watchdog_task; + struct timer_list pet_timer; + wait_queue_head_t pet_complete; + + bool timer_expired; + bool user_pet_complete; + unsigned long long timer_fired; + unsigned long long thread_start; + unsigned long long ping_start[NR_CPUS]; + unsigned long long ping_end[NR_CPUS]; + int cpu_idle_pc_state[NR_CPUS]; + bool freeze_in_progress; + spinlock_t freeze_lock; + struct work_struct irq_counts_work; + struct qcom_irq_info irq_counts[NR_TOP_HITTERS]; + struct qcom_irq_info ipi_counts[WDOG_NR_IPI]; + unsigned int tot_irq_count[NR_CPUS]; + atomic_t irq_counts_running; + struct timer_list user_pet_timer; +}; + +extern void qcom_wdt_trigger_bite(void); +int qcom_wdt_register(struct platform_device *pdev, + struct msm_watchdog_data *wdog_dd, + char *wdog_dd_name); +int qcom_wdt_pet_suspend(struct device *dev); +int qcom_wdt_pet_resume(struct device *dev); +int qcom_wdt_remove(struct platform_device *pdev); +#else +static inline void qcom_wdt_trigger_bite(void) { } +#endif + +#endif