watchdog: Add qcom VM watchdog driver

Watchdog timer is configured with bark and bite time interval.
If VM fails to reset the counter during bark interval, a bark
interrupt (SPI) is send to the VM. Failing to respond to the
bark interrupt causes a bite, when counter reaches the bite
interval. The bite interrupt is routed to secure world, and
causes a system reset.

Change-Id: Ia71a5b1f5e18382fd3c7fb00818c80c150ae1ba5
Signed-off-by: Neeraj Upadhyay <neeraju@codeaurora.org>
Signed-off-by: zhaochen <zhaochen@codeaurora.org>
diff --git a/Documentation/devicetree/bindings/watchdog/qcom-vm-wdt.txt b/Documentation/devicetree/bindings/watchdog/qcom-vm-wdt.txt
new file mode 100644
index 0000000..adade17
--- /dev/null
+++ b/Documentation/devicetree/bindings/watchdog/qcom-vm-wdt.txt
@@ -0,0 +1,32 @@
+* Qualcomm Technologies, Inc. VM  Watchdog
+
+Watchdog timer is configured with bark and bite time interval.
+If Virtual Machine fails to reset the counter during bark interval,
+a bark interrupt (SPI) is send to the VM. Failing to respond to the
+bark interrupt causes a bite, when counter reaches programmed
+bite interval. The bite interrupt is routed to secure world, and
+causes a system reset.
+
+The device tree parameters are:
+
+Required properties:
+
+- compatible : "qcom,vm-wdt"
+- reg : base offset and length of the register set.
+- reg-names : names corresponding to each reg property value.
+        "wdt-base" - physical base address of watchdog timer registers
+- interrupts : should contain bark and bite irq numbers
+- qcom,pet-time : Non zero time interval at which watchdog should be pet in ms.
+- qcom,bark-time : Non zero timeout value for a watchdog bark in ms.
+
+
+Example:
+
+	qcom,vm-wdt@f9017000 {
+		compatible = "qcom,vm-wdt";
+		reg = <0x17980000 0x1000>;
+		reg-names = "wdt-base";
+		interrupts = <0 3 0>, <0 4 0>;
+		qcom,bark-time = <11000>;
+		qcom,pet-time = <10000>;
+	};
diff --git a/MAINTAINERS b/MAINTAINERS
index a605955..34f509a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13057,6 +13057,7 @@
 F:	Documentation/devicetree/bindings/watchdog/
 F:	Documentation/watchdog/
 F:	drivers/watchdog/
+F:	drivers/watchdog/qcom-vm-wdt.c
 F:	include/linux/watchdog.h
 F:	include/uapi/linux/watchdog.h
 
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index 8f8909a..416903e 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -631,6 +631,18 @@
 	  To compile this driver as a module, choose M here: the
 	  module will be called qcom_wdt.
 
+config QCOM_VM_WDT
+	tristate "QCOM VM watchdog"
+	depends on HAS_IOMEM
+	select WATCHDOG_CORE
+	help
+	  Say Y here to include Watchdog timer support for the watchdog found
+	  on QCOM chipsets, for enviroments where hypervisor supports
+	  virtualized watchdog register space for each VM.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called qcom_vm_watchdog.
+
 config MESON_GXBB_WATCHDOG
 	tristate "Amlogic Meson GXBB SoCs watchdog support"
 	depends on ARCH_MESON
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index caa9f4a..fa68d48 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -71,6 +71,7 @@
 obj-$(CONFIG_SIRFSOC_WATCHDOG) += sirfsoc_wdt.o
 obj-$(CONFIG_ST_LPC_WATCHDOG) += st_lpc_wdt.o
 obj-$(CONFIG_QCOM_WDT) += qcom-wdt.o
+obj-$(CONFIG_QCOM_VM_WDT) += qcom-vm-wdt.o
 obj-$(CONFIG_BCM_KONA_WDT) += bcm_kona_wdt.o
 obj-$(CONFIG_TEGRA_WATCHDOG) += tegra_wdt.o
 obj-$(CONFIG_MESON_GXBB_WATCHDOG) += meson_gxbb_wdt.o
diff --git a/drivers/watchdog/qcom-vm-wdt.c b/drivers/watchdog/qcom-vm-wdt.c
new file mode 100644
index 0000000..5e86e44
--- /dev/null
+++ b/drivers/watchdog/qcom-vm-wdt.c
@@ -0,0 +1,475 @@
+/* Copyright (c) 2018, The Linux Foundation. All rights reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 and
+ * only version 2 as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/cpu.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/interrupt.h>
+#include <linux/irq.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/kthread.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/wait.h>
+#include <linux/watchdog.h>
+#include <linux/of_device.h>
+
+#define MASK_SIZE		32
+#define QCOM_VM_WDT_HZ		32765
+#define QCOM_VM_PET_TIMEOUT	9U
+#define QCOM_VM_BARK_TIMEOUT	11U
+
+enum qcom_vm_wdt_reg {
+	WDT_RST,
+	WDT_CTRL,
+	WDT_STS,
+	WDT_BARK_TIME,
+	WDT_BITE_TIME,
+};
+
+static const u32 qcom_vm_wdt_reg_offset_data[] = {
+	[WDT_RST] = 0x4,
+	[WDT_CTRL] = 0x8,
+	[WDT_STS] = 0xC,
+	[WDT_BARK_TIME] = 0x10,
+	[WDT_BITE_TIME] = 0x14,
+};
+
+struct qcom_vm_wdt {
+	struct watchdog_device	wdd;
+	void __iomem		*base;
+	const u32		*layout;
+	unsigned int bark_irq;
+	unsigned int bite_irq;
+	unsigned int bark_time;
+	unsigned int pet_time;
+	unsigned long long last_pet;
+	struct device *dev;
+	struct notifier_block panic_blk;
+	struct timer_list pet_timer;
+	bool timer_expired;
+	wait_queue_head_t pet_complete;
+	struct task_struct *watchdog_task;
+	cpumask_t alive_mask;
+	struct mutex disable_lock;
+};
+
+static int enable = 1;
+module_param(enable, int, 0000);
+
+/* Disable the watchdog in hypervisor */
+static int hyp_enable = 1;
+module_param(hyp_enable, int, 0000);
+
+static void __iomem *qcom_vm_wdt_addr(struct qcom_vm_wdt *wdt,
+					enum qcom_vm_wdt_reg reg)
+{
+	return wdt->base + wdt->layout[reg];
+}
+
+static void dump_cpu_alive_mask(struct qcom_vm_wdt *wdt)
+{
+	static char alive_mask_buf[MASK_SIZE];
+
+	scnprintf(alive_mask_buf, MASK_SIZE, "%*pb1", cpumask_pr_args(
+				&wdt->alive_mask));
+	dev_info(wdt->dev, "cpu alive mask from last pet %s\n",
+				alive_mask_buf);
+}
+
+static irqreturn_t qcom_vm_wdt_bark_handler(int irq, void *dev_id)
+{
+	struct qcom_vm_wdt *wdt = (struct qcom_vm_wdt *)dev_id;
+	unsigned long long t = sched_clock();
+
+	dev_info(wdt->dev, "Watchdog bark! Now = %lu\n",
+			(unsigned long) t);
+	dev_info(wdt->dev, "Watchdog last pet at %lu\n",
+			(unsigned long) wdt->last_pet);
+	dump_cpu_alive_mask(wdt);
+	pr_info("Causing a watchdog bite!");
+	__raw_writel(1 * QCOM_VM_WDT_HZ, qcom_vm_wdt_addr(wdt, WDT_BITE_TIME));
+	/* Make sure bite time is written before we reset */
+	mb();
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	/* Make sure we wait only after reset */
+	mb();
+	/* Delay to make sure bite occurs */
+	msleep(10000);
+
+	panic("Failed to cause a watchdog bite! - Falling back to kernel panic!");
+	return IRQ_HANDLED;
+}
+
+static int qcom_vm_wdt_suspend(struct device *dev)
+{
+	struct qcom_vm_wdt *wdt =
+			(struct qcom_vm_wdt *)dev_get_drvdata(dev);
+
+	if (!enable)
+		return 0;
+
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	/* Make sure watchdog is suspended before setting enable */
+	mb();
+
+	wdt->last_pet = sched_clock();
+	return 0;
+}
+
+static int qcom_vm_wdt_resume(struct device *dev)
+{
+	struct qcom_vm_wdt *wdt =
+			(struct qcom_vm_wdt *)dev_get_drvdata(dev);
+
+	if (!enable)
+		return 0;
+
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	/* Make sure watchdog is suspended before setting enable */
+	mb();
+
+	wdt->last_pet = sched_clock();
+	return 0;
+}
+
+static int qcom_vm_wdt_panic_handler(struct notifier_block *this,
+			      unsigned long event, void *ptr)
+{
+	struct qcom_vm_wdt *wdt = container_of(this, struct qcom_vm_wdt,
+						panic_blk);
+
+	__raw_writel(QCOM_VM_WDT_HZ * (panic_timeout + 10),
+			qcom_vm_wdt_addr(wdt, WDT_BARK_TIME));
+	__raw_writel(QCOM_VM_WDT_HZ * (panic_timeout + 10),
+			qcom_vm_wdt_addr(wdt, WDT_BITE_TIME));
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	/*
+	 * Ensure that bark, bite times, and reset is done, before
+	 * moving forward.
+	 */
+	mb();
+
+	return NOTIFY_DONE;
+}
+
+static void qcom_vm_wdt_disable(struct qcom_vm_wdt *wdt)
+{
+	if (hyp_enable == 1)
+		__raw_writel(0, qcom_vm_wdt_addr(wdt, WDT_CTRL));
+	/* Make sure watchdog is disabled before proceeding */
+	mb();
+	devm_free_irq(wdt->dev, wdt->bark_irq, wdt);
+	enable = 0;
+	/*Ensure all cpus see update to enable*/
+	smp_mb();
+	atomic_notifier_chain_unregister(&panic_notifier_list,
+						&wdt->panic_blk);
+	del_timer_sync(&wdt->pet_timer);
+	/* may be suspended after the first write above */
+	if (hyp_enable == 1)
+		__raw_writel(0, qcom_vm_wdt_addr(wdt, WDT_CTRL));
+	/* Make sure watchdog is disabled before setting enable */
+	mb();
+	pr_info("QCOM VM Watchdog deactivated.\n");
+}
+
+static ssize_t qcom_vm_wdt_disable_get(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	int ret;
+	struct qcom_vm_wdt *wdt = dev_get_drvdata(dev);
+
+	mutex_lock(&wdt->disable_lock);
+	ret = snprintf(buf, PAGE_SIZE, "%d\n", enable == 0 ? 1 : 0);
+	mutex_unlock(&wdt->disable_lock);
+	return ret;
+}
+
+static ssize_t qcom_vm_wdt_disable_set(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	int ret;
+	u8 disable;
+	struct qcom_vm_wdt *wdt = dev_get_drvdata(dev);
+
+	ret = kstrtou8(buf, 10, &disable);
+	if (ret) {
+		dev_err(wdt->dev, "invalid user input\n");
+		return ret;
+	}
+	if (disable == 1) {
+		mutex_lock(&wdt->disable_lock);
+		if (enable == 0) {
+			pr_info("QCOM VM Watchdog already disabled\n");
+			mutex_unlock(&wdt->disable_lock);
+			return count;
+		}
+		qcom_vm_wdt_disable(wdt);
+		mutex_unlock(&wdt->disable_lock);
+	} else {
+		pr_err("invalid operation, only disable = 1 supported\n");
+		return -EINVAL;
+	}
+	return count;
+}
+
+static DEVICE_ATTR(disable, 0600, qcom_vm_wdt_disable_get,
+			qcom_vm_wdt_disable_set);
+
+static void qcom_vm_wdt_hyp_disable(struct qcom_vm_wdt *wdt)
+{
+	__raw_writel(1 << 2, qcom_vm_wdt_addr(wdt, WDT_CTRL));
+	hyp_enable = 0;
+}
+
+static ssize_t qcom_vm_wdt_hyp_disable_get(struct device *dev,
+				struct device_attribute *attr, char *buf)
+{
+	int ret;
+	struct qcom_vm_wdt *wdt = dev_get_drvdata(dev);
+
+	mutex_lock(&wdt->disable_lock);
+	ret = snprintf(buf, PAGE_SIZE, "%d\n", hyp_enable == 0 ? 1 : 0);
+	mutex_unlock(&wdt->disable_lock);
+	return ret;
+}
+
+static ssize_t qcom_vm_wdt_hyp_disable_set(struct device *dev,
+				struct device_attribute *attr,
+				const char *buf, size_t count)
+{
+	int ret;
+	u8 disable;
+	struct qcom_vm_wdt *wdt = dev_get_drvdata(dev);
+
+	ret = kstrtou8(buf, 10, &disable);
+	if (ret) {
+		dev_err(wdt->dev, "invalid user input\n");
+		return ret;
+	}
+	if (disable == 1) {
+		mutex_lock(&wdt->disable_lock);
+		if (hyp_enable == 0) {
+			pr_info("QCOM VM Watchdog already disabled in Hyp\n");
+			mutex_unlock(&wdt->disable_lock);
+			return count;
+		}
+		qcom_vm_wdt_hyp_disable(wdt);
+		mutex_unlock(&wdt->disable_lock);
+	} else {
+		pr_err("invalid operation, only hyp_disable = 1 supported\n");
+		return -EINVAL;
+	}
+	return count;
+}
+
+static DEVICE_ATTR(hyp_disable, 0600, qcom_vm_wdt_hyp_disable_get,
+			qcom_vm_wdt_hyp_disable_set);
+
+static int qcom_vm_wdt_start(struct qcom_vm_wdt *wdt)
+{
+	__raw_writel(0, qcom_vm_wdt_addr(wdt, WDT_CTRL));
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	__raw_writel((wdt->bark_time / 1000) * QCOM_VM_WDT_HZ,
+			qcom_vm_wdt_addr(wdt, WDT_BARK_TIME));
+	__raw_writel((wdt->bark_time / 1000 + 3) * QCOM_VM_WDT_HZ,
+			qcom_vm_wdt_addr(wdt, WDT_BITE_TIME));
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_CTRL));
+	return 0;
+}
+
+static void keep_alive_response(void *info)
+{
+	int cpu = smp_processor_id();
+	struct qcom_vm_wdt *wdt = (struct qcom_vm_wdt *)info;
+
+	cpumask_set_cpu(cpu, &wdt->alive_mask);
+	/* Make sure alive mask is cleared and set in order */
+	smp_mb();
+}
+
+static int qcom_vm_wdt_ping(struct qcom_vm_wdt *wdt)
+{
+	int cpu;
+
+	cpumask_clear(&wdt->alive_mask);
+	for_each_cpu(cpu, cpu_online_mask) {
+		smp_call_function_single(cpu, keep_alive_response, wdt, 1);
+	}
+	__raw_writel(1, qcom_vm_wdt_addr(wdt, WDT_RST));
+	wdt->last_pet = sched_clock();
+	return 0;
+}
+
+static void vm_pet_task_wakeup(unsigned long data)
+{
+	struct qcom_vm_wdt *wdt = (struct qcom_vm_wdt *)data;
+
+	wdt->timer_expired = true;
+	wake_up(&wdt->pet_complete);
+}
+
+static __ref int vm_watchdog_kthread(void *arg)
+{
+	struct qcom_vm_wdt *wdt = (struct qcom_vm_wdt *)arg;
+
+	while (!kthread_should_stop()) {
+		while (wait_event_interruptible(
+			wdt->pet_complete,
+			wdt->timer_expired) != 0)
+			;
+		if (enable)
+			qcom_vm_wdt_ping(wdt);
+		wdt->timer_expired = false;
+		mod_timer(&wdt->pet_timer, jiffies +
+				msecs_to_jiffies(wdt->pet_time));
+	}
+	return 0;
+}
+
+static int qcom_vm_wdt_probe(struct platform_device *pdev)
+{
+	struct qcom_vm_wdt *wdt;
+	struct resource *res;
+	const u32 *regs;
+	int ret, error;
+
+	if (!pdev->dev.of_node || !enable)
+		return -ENODEV;
+
+	regs = of_device_get_match_data(&pdev->dev);
+	if (!regs) {
+		dev_err(&pdev->dev, "Unsupported QCOM WDT module\n");
+		return -ENODEV;
+	}
+
+	wdt = devm_kzalloc(&pdev->dev, sizeof(*wdt), GFP_KERNEL);
+	if (!wdt)
+		return -ENOMEM;
+
+	res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "wdt-base");
+	if (!res) {
+		ret = -ENODEV;
+		goto err;
+	}
+
+	wdt->base = devm_ioremap_resource(&pdev->dev, res);
+	if (IS_ERR(wdt->base)) {
+		ret = PTR_ERR(wdt->base);
+		goto err;
+	}
+
+	wdt->layout = regs;
+	wdt->dev = &pdev->dev;
+
+	wdt->bark_irq = platform_get_irq(pdev, 0);
+	wdt->bite_irq = platform_get_irq(pdev, 1);
+	wdt->last_pet = sched_clock();
+
+	ret = of_property_read_u32(pdev->dev.of_node, "qcom,bark-time",
+					&wdt->bark_time);
+	if (ret)
+		wdt->bark_time = QCOM_VM_BARK_TIMEOUT * 1000;
+	ret = of_property_read_u32(pdev->dev.of_node, "qcom,pet-time",
+					&wdt->pet_time);
+	if (ret)
+		wdt->pet_time = QCOM_VM_PET_TIMEOUT * 1000;
+
+	wdt->watchdog_task = kthread_create(vm_watchdog_kthread, wdt,
+			"qcom_vm_watchdog");
+	if (IS_ERR(wdt->watchdog_task)) {
+		ret = PTR_ERR(wdt->watchdog_task);
+		goto err;
+	}
+
+	init_waitqueue_head(&wdt->pet_complete);
+	wdt->timer_expired = false;
+	wake_up_process(wdt->watchdog_task);
+	init_timer(&wdt->pet_timer);
+	wdt->pet_timer.data = (unsigned long)wdt;
+	wdt->pet_timer.function = vm_pet_task_wakeup;
+	wdt->pet_timer.expires = jiffies +
+				msecs_to_jiffies(wdt->pet_time);
+	add_timer(&wdt->pet_timer);
+	cpumask_clear(&wdt->alive_mask);
+
+	wdt->panic_blk.notifier_call = qcom_vm_wdt_panic_handler;
+	atomic_notifier_chain_register(&panic_notifier_list, &wdt->panic_blk);
+
+	platform_set_drvdata(pdev, wdt);
+	mutex_init(&wdt->disable_lock);
+
+	error = device_create_file(wdt->dev, &dev_attr_disable);
+	error |= device_create_file(wdt->dev, &dev_attr_hyp_disable);
+
+	if (error)
+		dev_err(wdt->dev, "cannot create sysfs attribute\n");
+
+	qcom_vm_wdt_start(wdt);
+	ret = devm_request_irq(&pdev->dev, wdt->bark_irq,
+				qcom_vm_wdt_bark_handler, IRQF_TRIGGER_RISING,
+						"apps_wdog_bark", wdt);
+
+	if (hyp_enable == 0)
+		qcom_vm_wdt_hyp_disable(wdt);
+	return 0;
+
+err:
+	kfree(wdt);
+	return ret;
+}
+
+static int qcom_vm_wdt_remove(struct platform_device *pdev)
+{
+	struct qcom_vm_wdt *wdt = platform_get_drvdata(pdev);
+
+	mutex_lock(&wdt->disable_lock);
+	if (enable)
+		qcom_vm_wdt_disable(wdt);
+	mutex_unlock(&wdt->disable_lock);
+	device_remove_file(wdt->dev, &dev_attr_disable);
+	device_remove_file(wdt->dev, &dev_attr_hyp_disable);
+	dev_info(wdt->dev, "QCOM VM Watchdog Exit - Deactivated\n");
+	del_timer_sync(&wdt->pet_timer);
+	kthread_stop(wdt->watchdog_task);
+	kfree(wdt);
+	return 0;
+}
+
+static const struct of_device_id qcom_vm_wdt_of_table[] = {
+	{ .compatible = "qcom,vm-wdt", .data = qcom_vm_wdt_reg_offset_data },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, qcom_wdt_of_table);
+
+static const struct dev_pm_ops qcom_vm_wdt_dev_pm_ops = {
+	.suspend_noirq = qcom_vm_wdt_suspend,
+	.resume_noirq = qcom_vm_wdt_resume,
+};
+
+static struct platform_driver qcom_vm_watchdog_driver = {
+	.probe	= qcom_vm_wdt_probe,
+	.remove	= qcom_vm_wdt_remove,
+	.driver	= {
+		.name		= KBUILD_MODNAME,
+		.pm = &qcom_vm_wdt_dev_pm_ops,
+		.of_match_table	= qcom_vm_wdt_of_table,
+	},
+};
+module_platform_driver(qcom_vm_watchdog_driver);
+
+MODULE_DESCRIPTION("QCOM VM Watchdog Driver");
+MODULE_LICENSE("GPL v2");