diff options
Diffstat (limited to 'drivers/watchdog/gpio_wdt.c')
| -rw-r--r-- | drivers/watchdog/gpio_wdt.c | 254 | 
1 files changed, 254 insertions, 0 deletions
diff --git a/drivers/watchdog/gpio_wdt.c b/drivers/watchdog/gpio_wdt.c new file mode 100644 index 00000000000..220a9e07cfd --- /dev/null +++ b/drivers/watchdog/gpio_wdt.c @@ -0,0 +1,254 @@ +/* + * Driver for watchdog device controlled through GPIO-line + * + * Author: 2013, Alexander Shiyan <shc_work@mail.ru> + * + * 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. + */ + +#include <linux/err.h> +#include <linux/delay.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/of_gpio.h> +#include <linux/platform_device.h> +#include <linux/reboot.h> +#include <linux/watchdog.h> + +#define SOFT_TIMEOUT_MIN	1 +#define SOFT_TIMEOUT_DEF	60 +#define SOFT_TIMEOUT_MAX	0xffff + +enum { +	HW_ALGO_TOGGLE, +	HW_ALGO_LEVEL, +}; + +struct gpio_wdt_priv { +	int			gpio; +	bool			active_low; +	bool			state; +	unsigned int		hw_algo; +	unsigned int		hw_margin; +	unsigned long		last_jiffies; +	struct notifier_block	notifier; +	struct timer_list	timer; +	struct watchdog_device	wdd; +}; + +static void gpio_wdt_disable(struct gpio_wdt_priv *priv) +{ +	gpio_set_value_cansleep(priv->gpio, !priv->active_low); + +	/* Put GPIO back to tristate */ +	if (priv->hw_algo == HW_ALGO_TOGGLE) +		gpio_direction_input(priv->gpio); +} + +static int gpio_wdt_start(struct watchdog_device *wdd) +{ +	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); + +	priv->state = priv->active_low; +	gpio_direction_output(priv->gpio, priv->state); +	priv->last_jiffies = jiffies; +	mod_timer(&priv->timer, priv->last_jiffies + priv->hw_margin); + +	return 0; +} + +static int gpio_wdt_stop(struct watchdog_device *wdd) +{ +	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); + +	mod_timer(&priv->timer, 0); +	gpio_wdt_disable(priv); + +	return 0; +} + +static int gpio_wdt_ping(struct watchdog_device *wdd) +{ +	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); + +	priv->last_jiffies = jiffies; + +	return 0; +} + +static int gpio_wdt_set_timeout(struct watchdog_device *wdd, unsigned int t) +{ +	wdd->timeout = t; + +	return gpio_wdt_ping(wdd); +} + +static void gpio_wdt_hwping(unsigned long data) +{ +	struct watchdog_device *wdd = (struct watchdog_device *)data; +	struct gpio_wdt_priv *priv = watchdog_get_drvdata(wdd); + +	if (time_after(jiffies, priv->last_jiffies + +		       msecs_to_jiffies(wdd->timeout * 1000))) { +		dev_crit(wdd->dev, "Timer expired. System will reboot soon!\n"); +		return; +	} + +	/* Restart timer */ +	mod_timer(&priv->timer, jiffies + priv->hw_margin); + +	switch (priv->hw_algo) { +	case HW_ALGO_TOGGLE: +		/* Toggle output pin */ +		priv->state = !priv->state; +		gpio_set_value_cansleep(priv->gpio, priv->state); +		break; +	case HW_ALGO_LEVEL: +		/* Pulse */ +		gpio_set_value_cansleep(priv->gpio, !priv->active_low); +		udelay(1); +		gpio_set_value_cansleep(priv->gpio, priv->active_low); +		break; +	} +} + +static int gpio_wdt_notify_sys(struct notifier_block *nb, unsigned long code, +			       void *unused) +{ +	struct gpio_wdt_priv *priv = container_of(nb, struct gpio_wdt_priv, +						  notifier); + +	mod_timer(&priv->timer, 0); + +	switch (code) { +	case SYS_HALT: +	case SYS_POWER_OFF: +		gpio_wdt_disable(priv); +		break; +	default: +		break; +	} + +	return NOTIFY_DONE; +} + +static const struct watchdog_info gpio_wdt_ident = { +	.options	= WDIOF_MAGICCLOSE | WDIOF_KEEPALIVEPING | +			  WDIOF_SETTIMEOUT, +	.identity	= "GPIO Watchdog", +}; + +static const struct watchdog_ops gpio_wdt_ops = { +	.owner		= THIS_MODULE, +	.start		= gpio_wdt_start, +	.stop		= gpio_wdt_stop, +	.ping		= gpio_wdt_ping, +	.set_timeout	= gpio_wdt_set_timeout, +}; + +static int gpio_wdt_probe(struct platform_device *pdev) +{ +	struct gpio_wdt_priv *priv; +	enum of_gpio_flags flags; +	unsigned int hw_margin; +	unsigned long f = 0; +	const char *algo; +	int ret; + +	priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); +	if (!priv) +		return -ENOMEM; + +	priv->gpio = of_get_gpio_flags(pdev->dev.of_node, 0, &flags); +	if (!gpio_is_valid(priv->gpio)) +		return priv->gpio; + +	priv->active_low = flags & OF_GPIO_ACTIVE_LOW; + +	ret = of_property_read_string(pdev->dev.of_node, "hw_algo", &algo); +	if (ret) +		return ret; +	if (!strncmp(algo, "toggle", 6)) { +		priv->hw_algo = HW_ALGO_TOGGLE; +		f = GPIOF_IN; +	} else if (!strncmp(algo, "level", 5)) { +		priv->hw_algo = HW_ALGO_LEVEL; +		f = priv->active_low ? GPIOF_OUT_INIT_HIGH : GPIOF_OUT_INIT_LOW; +	} else { +		return -EINVAL; +	} + +	ret = devm_gpio_request_one(&pdev->dev, priv->gpio, f, +				    dev_name(&pdev->dev)); +	if (ret) +		return ret; + +	ret = of_property_read_u32(pdev->dev.of_node, +				   "hw_margin_ms", &hw_margin); +	if (ret) +		return ret; +	/* Disallow values lower than 2 and higher than 65535 ms */ +	if (hw_margin < 2 || hw_margin > 65535) +		return -EINVAL; + +	/* Use safe value (1/2 of real timeout) */ +	priv->hw_margin = msecs_to_jiffies(hw_margin / 2); + +	watchdog_set_drvdata(&priv->wdd, priv); + +	priv->wdd.info		= &gpio_wdt_ident; +	priv->wdd.ops		= &gpio_wdt_ops; +	priv->wdd.min_timeout	= SOFT_TIMEOUT_MIN; +	priv->wdd.max_timeout	= SOFT_TIMEOUT_MAX; + +	if (watchdog_init_timeout(&priv->wdd, 0, &pdev->dev) < 0) +		priv->wdd.timeout = SOFT_TIMEOUT_DEF; + +	setup_timer(&priv->timer, gpio_wdt_hwping, (unsigned long)&priv->wdd); + +	ret = watchdog_register_device(&priv->wdd); +	if (ret) +		return ret; + +	priv->notifier.notifier_call = gpio_wdt_notify_sys; +	ret = register_reboot_notifier(&priv->notifier); +	if (ret) +		watchdog_unregister_device(&priv->wdd); + +	return ret; +} + +static int gpio_wdt_remove(struct platform_device *pdev) +{ +	struct gpio_wdt_priv *priv = platform_get_drvdata(pdev); + +	del_timer_sync(&priv->timer); +	unregister_reboot_notifier(&priv->notifier); +	watchdog_unregister_device(&priv->wdd); + +	return 0; +} + +static const struct of_device_id gpio_wdt_dt_ids[] = { +	{ .compatible = "linux,wdt-gpio", }, +	{ } +}; +MODULE_DEVICE_TABLE(of, gpio_wdt_dt_ids); + +static struct platform_driver gpio_wdt_driver = { +	.driver	= { +		.name		= "gpio-wdt", +		.owner		= THIS_MODULE, +		.of_match_table	= gpio_wdt_dt_ids, +	}, +	.probe	= gpio_wdt_probe, +	.remove	= gpio_wdt_remove, +}; +module_platform_driver(gpio_wdt_driver); + +MODULE_AUTHOR("Alexander Shiyan <shc_work@mail.ru>"); +MODULE_DESCRIPTION("GPIO Watchdog"); +MODULE_LICENSE("GPL");  | 
