/* * Watchdog driver for Alphascale ASM9260. * * Copyright (c) 2014 Oleksij Rempel <linux@rempel-privat.de> * * Licensed under GPLv2 or later. */ #include <linux/bitops.h> #include <linux/clk.h> #include <linux/delay.h> #include <linux/interrupt.h> #include <linux/io.h> #include <linux/module.h> #include <linux/of.h> #include <linux/platform_device.h> #include <linux/reset.h> #include <linux/watchdog.h> #define CLOCK_FREQ 1000000 /* Watchdog Mode register */ #define HW_WDMOD 0x00 /* Wake interrupt. Set by HW, can't be cleared. */ #define BM_MOD_WDINT BIT(3) /* This bit set if timeout reached. Cleared by SW. */ #define BM_MOD_WDTOF BIT(2) /* HW Reset on timeout */ #define BM_MOD_WDRESET BIT(1) /* WD enable */ #define BM_MOD_WDEN BIT(0) /* * Watchdog Timer Constant register * Minimal value is 0xff, the meaning of this value * depends on used clock: T = WDCLK * (0xff + 1) * 4 */ #define HW_WDTC 0x04 #define BM_WDTC_MAX(freq) (0x7fffffff / (freq)) /* Watchdog Feed register */ #define HW_WDFEED 0x08 /* Watchdog Timer Value register */ #define HW_WDTV 0x0c #define ASM9260_WDT_DEFAULT_TIMEOUT 30 enum asm9260_wdt_mode { HW_RESET, SW_RESET, DEBUG, }; struct asm9260_wdt_priv { struct device *dev; struct watchdog_device wdd; struct clk *clk; struct clk *clk_ahb; struct reset_control *rst; void __iomem *iobase; int irq; unsigned long wdt_freq; enum asm9260_wdt_mode mode; }; static int asm9260_wdt_feed(struct watchdog_device *wdd) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); iowrite32(0xaa, priv->iobase + HW_WDFEED); iowrite32(0x55, priv->iobase + HW_WDFEED); return 0; } static unsigned int asm9260_wdt_gettimeleft(struct watchdog_device *wdd) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); u32 counter; counter = ioread32(priv->iobase + HW_WDTV); return DIV_ROUND_CLOSEST(counter, priv->wdt_freq); } static int asm9260_wdt_updatetimeout(struct watchdog_device *wdd) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); u32 counter; counter = wdd->timeout * priv->wdt_freq; iowrite32(counter, priv->iobase + HW_WDTC); return 0; } static int asm9260_wdt_enable(struct watchdog_device *wdd) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); u32 mode = 0; if (priv->mode == HW_RESET) mode = BM_MOD_WDRESET; iowrite32(BM_MOD_WDEN | mode, priv->iobase + HW_WDMOD); asm9260_wdt_updatetimeout(wdd); asm9260_wdt_feed(wdd); return 0; } static int asm9260_wdt_disable(struct watchdog_device *wdd) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); /* The only way to disable WD is to reset it. */ reset_control_assert(priv->rst); reset_control_deassert(priv->rst); return 0; } static int asm9260_wdt_settimeout(struct watchdog_device *wdd, unsigned int to) { wdd->timeout = to; asm9260_wdt_updatetimeout(wdd); return 0; } static void asm9260_wdt_sys_reset(struct asm9260_wdt_priv *priv) { /* init WD if it was not started */ iowrite32(BM_MOD_WDEN | BM_MOD_WDRESET, priv->iobase + HW_WDMOD); iowrite32(0xff, priv->iobase + HW_WDTC); /* first pass correct sequence */ asm9260_wdt_feed(&priv->wdd); /* * Then write wrong pattern to the feed to trigger reset * ASAP. */ iowrite32(0xff, priv->iobase + HW_WDFEED); mdelay(1000); } static irqreturn_t asm9260_wdt_irq(int irq, void *devid) { struct asm9260_wdt_priv *priv = devid; u32 stat; stat = ioread32(priv->iobase + HW_WDMOD); if (!(stat & BM_MOD_WDINT)) return IRQ_NONE; if (priv->mode == DEBUG) { dev_info(priv->dev, "Watchdog Timeout. Do nothing.\n"); } else { dev_info(priv->dev, "Watchdog Timeout. Doing SW Reset.\n"); asm9260_wdt_sys_reset(priv); } return IRQ_HANDLED; } static int asm9260_restart(struct watchdog_device *wdd, unsigned long action, void *data) { struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); asm9260_wdt_sys_reset(priv); return 0; } static const struct watchdog_info asm9260_wdt_ident = { .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE, .identity = "Alphascale asm9260 Watchdog", }; static const struct watchdog_ops asm9260_wdt_ops = { .owner = THIS_MODULE, .start = asm9260_wdt_enable, .stop = asm9260_wdt_disable, .get_timeleft = asm9260_wdt_gettimeleft, .ping = asm9260_wdt_feed, .set_timeout = asm9260_wdt_settimeout, .restart = asm9260_restart, }; static int asm9260_wdt_get_dt_clks(struct asm9260_wdt_priv *priv) { int err; unsigned long clk; priv->clk = devm_clk_get(priv->dev, "mod"); if (IS_ERR(priv->clk)) { dev_err(priv->dev, "Failed to get \"mod\" clk\n"); return PTR_ERR(priv->clk); } /* configure AHB clock */ priv->clk_ahb = devm_clk_get(priv->dev, "ahb"); if (IS_ERR(priv->clk_ahb)) { dev_err(priv->dev, "Failed to get \"ahb\" clk\n"); return PTR_ERR(priv->clk_ahb); } err = clk_prepare_enable(priv->clk_ahb); if (err) { dev_err(priv->dev, "Failed to enable ahb_clk!\n"); return err; } err = clk_set_rate(priv->clk, CLOCK_FREQ); if (err) { clk_disable_unprepare(priv->clk_ahb); dev_err(priv->dev, "Failed to set rate!\n"); return err; } err = clk_prepare_enable(priv->clk); if (err) { clk_disable_unprepare(priv->clk_ahb); dev_err(priv->dev, "Failed to enable clk!\n"); return err; } /* wdt has internal divider */ clk = clk_get_rate(priv->clk); if (!clk) { clk_disable_unprepare(priv->clk); clk_disable_unprepare(priv->clk_ahb); dev_err(priv->dev, "Failed, clk is 0!\n"); return -EINVAL; } priv->wdt_freq = clk / 2; return 0; } static void asm9260_wdt_get_dt_mode(struct asm9260_wdt_priv *priv) { const char *tmp; int ret; /* default mode */ priv->mode = HW_RESET; ret = of_property_read_string(priv->dev->of_node, "alphascale,mode", &tmp); if (ret < 0) return; if (!strcmp(tmp, "hw")) priv->mode = HW_RESET; else if (!strcmp(tmp, "sw")) priv->mode = SW_RESET; else if (!strcmp(tmp, "debug")) priv->mode = DEBUG; else dev_warn(priv->dev, "unknown reset-type: %s. Using default \"hw\" mode.", tmp); } static int asm9260_wdt_probe(struct platform_device *pdev) { struct asm9260_wdt_priv *priv; struct watchdog_device *wdd; struct resource *res; int ret; const char * const mode_name[] = { "hw", "sw", "debug", }; priv = devm_kzalloc(&pdev->dev, sizeof(struct asm9260_wdt_priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = &pdev->dev; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); priv->iobase = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(priv->iobase)) return PTR_ERR(priv->iobase); ret = asm9260_wdt_get_dt_clks(priv); if (ret) return ret; priv->rst = devm_reset_control_get(&pdev->dev, "wdt_rst"); if (IS_ERR(priv->rst)) return PTR_ERR(priv->rst); wdd = &priv->wdd; wdd->info = &asm9260_wdt_ident; wdd->ops = &asm9260_wdt_ops; wdd->min_timeout = 1; wdd->max_timeout = BM_WDTC_MAX(priv->wdt_freq); wdd->parent = &pdev->dev; watchdog_set_drvdata(wdd, priv); /* * If 'timeout-sec' unspecified in devicetree, assume a 30 second * default, unless the max timeout is less than 30 seconds, then use * the max instead. */ wdd->timeout = ASM9260_WDT_DEFAULT_TIMEOUT; watchdog_init_timeout(wdd, 0, &pdev->dev); asm9260_wdt_get_dt_mode(priv); if (priv->mode != HW_RESET) priv->irq = platform_get_irq(pdev, 0); if (priv->irq > 0) { /* * Not all supported platforms specify an interrupt for the * watchdog, so let's make it optional. */ ret = devm_request_irq(&pdev->dev, priv->irq, asm9260_wdt_irq, 0, pdev->name, priv); if (ret < 0) dev_warn(&pdev->dev, "failed to request IRQ\n"); } watchdog_set_restart_priority(wdd, 128); ret = watchdog_register_device(wdd); if (ret) goto clk_off; platform_set_drvdata(pdev, priv); dev_info(&pdev->dev, "Watchdog enabled (timeout: %d sec, mode: %s)\n", wdd->timeout, mode_name[priv->mode]); return 0; clk_off: clk_disable_unprepare(priv->clk); clk_disable_unprepare(priv->clk_ahb); return ret; } static void asm9260_wdt_shutdown(struct platform_device *pdev) { struct asm9260_wdt_priv *priv = platform_get_drvdata(pdev); asm9260_wdt_disable(&priv->wdd); } static int asm9260_wdt_remove(struct platform_device *pdev) { struct asm9260_wdt_priv *priv = platform_get_drvdata(pdev); asm9260_wdt_disable(&priv->wdd); watchdog_unregister_device(&priv->wdd); clk_disable_unprepare(priv->clk); clk_disable_unprepare(priv->clk_ahb); return 0; } static const struct of_device_id asm9260_wdt_of_match[] = { { .compatible = "alphascale,asm9260-wdt"}, {}, }; MODULE_DEVICE_TABLE(of, asm9260_wdt_of_match); static struct platform_driver asm9260_wdt_driver = { .driver = { .name = "asm9260-wdt", .of_match_table = asm9260_wdt_of_match, }, .probe = asm9260_wdt_probe, .remove = asm9260_wdt_remove, .shutdown = asm9260_wdt_shutdown, }; module_platform_driver(asm9260_wdt_driver); MODULE_DESCRIPTION("asm9260 WatchDog Timer Driver"); MODULE_AUTHOR("Oleksij Rempel <linux@rempel-privat.de>"); MODULE_LICENSE("GPL");