Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d983a643 authored by Neeraj Upadhyay's avatar Neeraj Upadhyay Committed by zhaochen
Browse files

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: default avatarNeeraj Upadhyay <neeraju@codeaurora.org>
Signed-off-by: default avatarzhaochen <zhaochen@codeaurora.org>
parent 2630d499
Loading
Loading
Loading
Loading
+32 −0
Original line number Diff line number Diff line
* 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>;
	};
+1 −0
Original line number Diff line number Diff line
@@ -13057,6 +13057,7 @@ S: Maintained
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

+12 −0
Original line number Diff line number Diff line
@@ -631,6 +631,18 @@ config QCOM_WDT
	  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
+1 −0
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@ obj-$(CONFIG_MOXART_WDT) += moxart_wdt.o
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
+475 −0
Original line number Diff line number Diff line
/* 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");