 58e5637333
			
		
	
	
	58e5637333
	
	
	
		
			
			Add device tree support to the DW watchdog timer. Signed-off-by: Dinh Nguyen <dinguyen@altera.com> Acked-by: Jamie Iles <jamie@jamieiles.com> Acked-by: Mark Rutland <mark.rutland@arm.com> Reviewed-by: Pavel Machek <pavel@denx.de> Reviewed-by: Guenter Roeck <linux@roeck-us.net> Signed-off-by: Wim Van Sebroeck <wim@iguana.be> Cc: Viresh Kumar <viresh.linux@gmail.com> Cc: Rob Herring <rob.herring@calxeda.com> Cc: Pawel Moll <pawel.moll@arm.com> Cc: Stephen Warren <swarren@wwwdotorg.org> Cc: Ian Campbell <ijc+devicetree@hellion.org.uk> Cc: devicetree@vger.kernel.org Cc: linux-watchdog@vger.kernel.org
		
			
				
	
	
		
			364 lines
		
	
	
	
		
			8.3 KiB
			
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			364 lines
		
	
	
	
		
			8.3 KiB
			
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Copyright 2010-2011 Picochip Ltd., Jamie Iles
 | |
|  * http://www.picochip.com
 | |
|  *
 | |
|  * This program is free software; you can redistribute it and/or
 | |
|  * modify it under the terms of the GNU General Public License
 | |
|  * as published by the Free Software Foundation; either version
 | |
|  * 2 of the License, or (at your option) any later version.
 | |
|  *
 | |
|  * This file implements a driver for the Synopsys DesignWare watchdog device
 | |
|  * in the many ARM subsystems. The watchdog has 16 different timeout periods
 | |
|  * and these are a function of the input clock frequency.
 | |
|  *
 | |
|  * The DesignWare watchdog cannot be stopped once it has been started so we
 | |
|  * use a software timer to implement a ping that will keep the watchdog alive.
 | |
|  * If we receive an expected close for the watchdog then we keep the timer
 | |
|  * running, otherwise the timer is stopped and the watchdog will expire.
 | |
|  */
 | |
| 
 | |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 | |
| 
 | |
| #include <linux/bitops.h>
 | |
| #include <linux/clk.h>
 | |
| #include <linux/device.h>
 | |
| #include <linux/err.h>
 | |
| #include <linux/fs.h>
 | |
| #include <linux/io.h>
 | |
| #include <linux/kernel.h>
 | |
| #include <linux/miscdevice.h>
 | |
| #include <linux/module.h>
 | |
| #include <linux/moduleparam.h>
 | |
| #include <linux/of.h>
 | |
| #include <linux/pm.h>
 | |
| #include <linux/platform_device.h>
 | |
| #include <linux/spinlock.h>
 | |
| #include <linux/timer.h>
 | |
| #include <linux/uaccess.h>
 | |
| #include <linux/watchdog.h>
 | |
| 
 | |
| #define WDOG_CONTROL_REG_OFFSET		    0x00
 | |
| #define WDOG_CONTROL_REG_WDT_EN_MASK	    0x01
 | |
| #define WDOG_TIMEOUT_RANGE_REG_OFFSET	    0x04
 | |
| #define WDOG_CURRENT_COUNT_REG_OFFSET	    0x08
 | |
| #define WDOG_COUNTER_RESTART_REG_OFFSET     0x0c
 | |
| #define WDOG_COUNTER_RESTART_KICK_VALUE	    0x76
 | |
| 
 | |
| /* The maximum TOP (timeout period) value that can be set in the watchdog. */
 | |
| #define DW_WDT_MAX_TOP		15
 | |
| 
 | |
| static bool nowayout = WATCHDOG_NOWAYOUT;
 | |
| module_param(nowayout, bool, 0);
 | |
| MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started "
 | |
| 		 "(default=" __MODULE_STRING(WATCHDOG_NOWAYOUT) ")");
 | |
| 
 | |
| #define WDT_TIMEOUT		(HZ / 2)
 | |
| 
 | |
| static struct {
 | |
| 	spinlock_t		lock;
 | |
| 	void __iomem		*regs;
 | |
| 	struct clk		*clk;
 | |
| 	unsigned long		in_use;
 | |
| 	unsigned long		next_heartbeat;
 | |
| 	struct timer_list	timer;
 | |
| 	int			expect_close;
 | |
| } dw_wdt;
 | |
| 
 | |
| static inline int dw_wdt_is_enabled(void)
 | |
| {
 | |
| 	return readl(dw_wdt.regs + WDOG_CONTROL_REG_OFFSET) &
 | |
| 		WDOG_CONTROL_REG_WDT_EN_MASK;
 | |
| }
 | |
| 
 | |
| static inline int dw_wdt_top_in_seconds(unsigned top)
 | |
| {
 | |
| 	/*
 | |
| 	 * There are 16 possible timeout values in 0..15 where the number of
 | |
| 	 * cycles is 2 ^ (16 + i) and the watchdog counts down.
 | |
| 	 */
 | |
| 	return (1 << (16 + top)) / clk_get_rate(dw_wdt.clk);
 | |
| }
 | |
| 
 | |
| static int dw_wdt_get_top(void)
 | |
| {
 | |
| 	int top = readl(dw_wdt.regs + WDOG_TIMEOUT_RANGE_REG_OFFSET) & 0xF;
 | |
| 
 | |
| 	return dw_wdt_top_in_seconds(top);
 | |
| }
 | |
| 
 | |
| static inline void dw_wdt_set_next_heartbeat(void)
 | |
| {
 | |
| 	dw_wdt.next_heartbeat = jiffies + dw_wdt_get_top() * HZ;
 | |
| }
 | |
| 
 | |
| static int dw_wdt_set_top(unsigned top_s)
 | |
| {
 | |
| 	int i, top_val = DW_WDT_MAX_TOP;
 | |
| 
 | |
| 	/*
 | |
| 	 * Iterate over the timeout values until we find the closest match. We
 | |
| 	 * always look for >=.
 | |
| 	 */
 | |
| 	for (i = 0; i <= DW_WDT_MAX_TOP; ++i)
 | |
| 		if (dw_wdt_top_in_seconds(i) >= top_s) {
 | |
| 			top_val = i;
 | |
| 			break;
 | |
| 		}
 | |
| 
 | |
| 	/* Set the new value in the watchdog. */
 | |
| 	writel(top_val, dw_wdt.regs + WDOG_TIMEOUT_RANGE_REG_OFFSET);
 | |
| 
 | |
| 	dw_wdt_set_next_heartbeat();
 | |
| 
 | |
| 	return dw_wdt_top_in_seconds(top_val);
 | |
| }
 | |
| 
 | |
| static void dw_wdt_keepalive(void)
 | |
| {
 | |
| 	writel(WDOG_COUNTER_RESTART_KICK_VALUE, dw_wdt.regs +
 | |
| 	       WDOG_COUNTER_RESTART_REG_OFFSET);
 | |
| }
 | |
| 
 | |
| static void dw_wdt_ping(unsigned long data)
 | |
| {
 | |
| 	if (time_before(jiffies, dw_wdt.next_heartbeat) ||
 | |
| 	    (!nowayout && !dw_wdt.in_use)) {
 | |
| 		dw_wdt_keepalive();
 | |
| 		mod_timer(&dw_wdt.timer, jiffies + WDT_TIMEOUT);
 | |
| 	} else
 | |
| 		pr_crit("keepalive missed, machine will reset\n");
 | |
| }
 | |
| 
 | |
| static int dw_wdt_open(struct inode *inode, struct file *filp)
 | |
| {
 | |
| 	if (test_and_set_bit(0, &dw_wdt.in_use))
 | |
| 		return -EBUSY;
 | |
| 
 | |
| 	/* Make sure we don't get unloaded. */
 | |
| 	__module_get(THIS_MODULE);
 | |
| 
 | |
| 	spin_lock(&dw_wdt.lock);
 | |
| 	if (!dw_wdt_is_enabled()) {
 | |
| 		/*
 | |
| 		 * The watchdog is not currently enabled. Set the timeout to
 | |
| 		 * the maximum and then start it.
 | |
| 		 */
 | |
| 		dw_wdt_set_top(DW_WDT_MAX_TOP);
 | |
| 		writel(WDOG_CONTROL_REG_WDT_EN_MASK,
 | |
| 		       dw_wdt.regs + WDOG_CONTROL_REG_OFFSET);
 | |
| 	}
 | |
| 
 | |
| 	dw_wdt_set_next_heartbeat();
 | |
| 
 | |
| 	spin_unlock(&dw_wdt.lock);
 | |
| 
 | |
| 	return nonseekable_open(inode, filp);
 | |
| }
 | |
| 
 | |
| static ssize_t dw_wdt_write(struct file *filp, const char __user *buf,
 | |
| 			    size_t len, loff_t *offset)
 | |
| {
 | |
| 	if (!len)
 | |
| 		return 0;
 | |
| 
 | |
| 	if (!nowayout) {
 | |
| 		size_t i;
 | |
| 
 | |
| 		dw_wdt.expect_close = 0;
 | |
| 
 | |
| 		for (i = 0; i < len; ++i) {
 | |
| 			char c;
 | |
| 
 | |
| 			if (get_user(c, buf + i))
 | |
| 				return -EFAULT;
 | |
| 
 | |
| 			if (c == 'V') {
 | |
| 				dw_wdt.expect_close = 1;
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	dw_wdt_set_next_heartbeat();
 | |
| 	mod_timer(&dw_wdt.timer, jiffies + WDT_TIMEOUT);
 | |
| 
 | |
| 	return len;
 | |
| }
 | |
| 
 | |
| static u32 dw_wdt_time_left(void)
 | |
| {
 | |
| 	return readl(dw_wdt.regs + WDOG_CURRENT_COUNT_REG_OFFSET) /
 | |
| 		clk_get_rate(dw_wdt.clk);
 | |
| }
 | |
| 
 | |
| static const struct watchdog_info dw_wdt_ident = {
 | |
| 	.options	= WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT |
 | |
| 			  WDIOF_MAGICCLOSE,
 | |
| 	.identity	= "Synopsys DesignWare Watchdog",
 | |
| };
 | |
| 
 | |
| static long dw_wdt_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
 | |
| {
 | |
| 	unsigned long val;
 | |
| 	int timeout;
 | |
| 
 | |
| 	switch (cmd) {
 | |
| 	case WDIOC_GETSUPPORT:
 | |
| 		return copy_to_user((void __user *)arg, &dw_wdt_ident,
 | |
| 				    sizeof(dw_wdt_ident)) ? -EFAULT : 0;
 | |
| 
 | |
| 	case WDIOC_GETSTATUS:
 | |
| 	case WDIOC_GETBOOTSTATUS:
 | |
| 		return put_user(0, (int __user *)arg);
 | |
| 
 | |
| 	case WDIOC_KEEPALIVE:
 | |
| 		dw_wdt_set_next_heartbeat();
 | |
| 		return 0;
 | |
| 
 | |
| 	case WDIOC_SETTIMEOUT:
 | |
| 		if (get_user(val, (int __user *)arg))
 | |
| 			return -EFAULT;
 | |
| 		timeout = dw_wdt_set_top(val);
 | |
| 		return put_user(timeout , (int __user *)arg);
 | |
| 
 | |
| 	case WDIOC_GETTIMEOUT:
 | |
| 		return put_user(dw_wdt_get_top(), (int __user *)arg);
 | |
| 
 | |
| 	case WDIOC_GETTIMELEFT:
 | |
| 		/* Get the time left until expiry. */
 | |
| 		if (get_user(val, (int __user *)arg))
 | |
| 			return -EFAULT;
 | |
| 		return put_user(dw_wdt_time_left(), (int __user *)arg);
 | |
| 
 | |
| 	default:
 | |
| 		return -ENOTTY;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| static int dw_wdt_release(struct inode *inode, struct file *filp)
 | |
| {
 | |
| 	clear_bit(0, &dw_wdt.in_use);
 | |
| 
 | |
| 	if (!dw_wdt.expect_close) {
 | |
| 		del_timer(&dw_wdt.timer);
 | |
| 
 | |
| 		if (!nowayout)
 | |
| 			pr_crit("unexpected close, system will reboot soon\n");
 | |
| 		else
 | |
| 			pr_crit("watchdog cannot be disabled, system will reboot soon\n");
 | |
| 	}
 | |
| 
 | |
| 	dw_wdt.expect_close = 0;
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| #ifdef CONFIG_PM_SLEEP
 | |
| static int dw_wdt_suspend(struct device *dev)
 | |
| {
 | |
| 	clk_disable_unprepare(dw_wdt.clk);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static int dw_wdt_resume(struct device *dev)
 | |
| {
 | |
| 	int err = clk_prepare_enable(dw_wdt.clk);
 | |
| 
 | |
| 	if (err)
 | |
| 		return err;
 | |
| 
 | |
| 	dw_wdt_keepalive();
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| #endif /* CONFIG_PM_SLEEP */
 | |
| 
 | |
| static SIMPLE_DEV_PM_OPS(dw_wdt_pm_ops, dw_wdt_suspend, dw_wdt_resume);
 | |
| 
 | |
| static const struct file_operations wdt_fops = {
 | |
| 	.owner		= THIS_MODULE,
 | |
| 	.llseek		= no_llseek,
 | |
| 	.open		= dw_wdt_open,
 | |
| 	.write		= dw_wdt_write,
 | |
| 	.unlocked_ioctl	= dw_wdt_ioctl,
 | |
| 	.release	= dw_wdt_release
 | |
| };
 | |
| 
 | |
| static struct miscdevice dw_wdt_miscdev = {
 | |
| 	.fops		= &wdt_fops,
 | |
| 	.name		= "watchdog",
 | |
| 	.minor		= WATCHDOG_MINOR,
 | |
| };
 | |
| 
 | |
| static int dw_wdt_drv_probe(struct platform_device *pdev)
 | |
| {
 | |
| 	int ret;
 | |
| 	struct resource *mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
 | |
| 
 | |
| 	if (!mem)
 | |
| 		return -EINVAL;
 | |
| 
 | |
| 	dw_wdt.regs = devm_ioremap_resource(&pdev->dev, mem);
 | |
| 	if (IS_ERR(dw_wdt.regs))
 | |
| 		return PTR_ERR(dw_wdt.regs);
 | |
| 
 | |
| 	dw_wdt.clk = devm_clk_get(&pdev->dev, NULL);
 | |
| 	if (IS_ERR(dw_wdt.clk))
 | |
| 		return PTR_ERR(dw_wdt.clk);
 | |
| 
 | |
| 	ret = clk_prepare_enable(dw_wdt.clk);
 | |
| 	if (ret)
 | |
| 		return ret;
 | |
| 
 | |
| 	spin_lock_init(&dw_wdt.lock);
 | |
| 
 | |
| 	ret = misc_register(&dw_wdt_miscdev);
 | |
| 	if (ret)
 | |
| 		goto out_disable_clk;
 | |
| 
 | |
| 	dw_wdt_set_next_heartbeat();
 | |
| 	setup_timer(&dw_wdt.timer, dw_wdt_ping, 0);
 | |
| 	mod_timer(&dw_wdt.timer, jiffies + WDT_TIMEOUT);
 | |
| 
 | |
| 	return 0;
 | |
| 
 | |
| out_disable_clk:
 | |
| 	clk_disable_unprepare(dw_wdt.clk);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| static int dw_wdt_drv_remove(struct platform_device *pdev)
 | |
| {
 | |
| 	misc_deregister(&dw_wdt_miscdev);
 | |
| 
 | |
| 	clk_disable_unprepare(dw_wdt.clk);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| #ifdef CONFIG_OF
 | |
| static const struct of_device_id dw_wdt_of_match[] = {
 | |
| 	{ .compatible = "snps,dw-wdt", },
 | |
| 	{ /* sentinel */ }
 | |
| };
 | |
| MODULE_DEVICE_TABLE(of, dw_wdt_of_match);
 | |
| #endif
 | |
| 
 | |
| static struct platform_driver dw_wdt_driver = {
 | |
| 	.probe		= dw_wdt_drv_probe,
 | |
| 	.remove		= dw_wdt_drv_remove,
 | |
| 	.driver		= {
 | |
| 		.name	= "dw_wdt",
 | |
| 		.owner	= THIS_MODULE,
 | |
| 		.of_match_table = of_match_ptr(dw_wdt_of_match),
 | |
| 		.pm	= &dw_wdt_pm_ops,
 | |
| 	},
 | |
| };
 | |
| 
 | |
| module_platform_driver(dw_wdt_driver);
 | |
| 
 | |
| MODULE_AUTHOR("Jamie Iles");
 | |
| MODULE_DESCRIPTION("Synopsys DesignWare Watchdog Driver");
 | |
| MODULE_LICENSE("GPL");
 |