| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Copyright (C) 2019 Synaptics Incorporated |
| * |
| * Author: Jisheng Zhang <jszhang@kernel.org> |
| */ |
| |
| #include <linux/clk.h> |
| #include <linux/delay.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/reset.h> |
| #include <linux/watchdog.h> |
| |
| #define CONTROL 0x00 |
| #define WD_CONTROL 0x04 |
| #define INT_STAT 0x08 |
| #define TERMINAL_COUNT 0x0c |
| #define RESET_TERMINAL_COUNT 0x10 |
| #define WD_CMD_WINDOWN 0x14 |
| #define CURRENT_COUNT 0x18 |
| |
| struct as390_wdt { |
| void __iomem *base; |
| unsigned long rate; |
| struct watchdog_device wdd; |
| struct reset_control *rst; |
| struct clk *clk; |
| }; |
| |
| static inline u32 rdl(struct as390_wdt *wdt, u32 offset) |
| { |
| return readl_relaxed(wdt->base + offset); |
| } |
| |
| static inline void wrl(struct as390_wdt *wdt, u32 offset, u32 data) |
| { |
| writel_relaxed(data, wdt->base + offset); |
| } |
| |
| static int as390_wdt_ping(struct watchdog_device *wdog) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| |
| local_irq_disable(); |
| wrl(wdt, WD_CONTROL, 0xA5FF3210); |
| wrl(wdt, WD_CONTROL, 0x44BB77DD); |
| local_irq_enable(); |
| |
| return 0; |
| } |
| |
| static int as390_wdt_stop(struct watchdog_device *wdog) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| |
| local_irq_disable(); |
| writel_relaxed(0x0123a5ff, wdt->base + WD_CONTROL); |
| writel_relaxed(0xccee6699, wdt->base + WD_CONTROL); |
| local_irq_enable(); |
| |
| return 0; |
| } |
| |
| static void as390_wdt_set(struct as390_wdt *wdt, u32 ticks) |
| { |
| u32 val; |
| |
| val = rdl(wdt, CONTROL); |
| if (val & (1 << 1)) { |
| as390_wdt_stop(&wdt->wdd); |
| reset_control_reset(wdt->rst); |
| } |
| |
| val = 1 << 0; |
| wrl(wdt, CONTROL, val); |
| |
| if (ticks <= 5000) |
| ticks = 5000 + 1; |
| wrl(wdt, TERMINAL_COUNT, ticks); |
| |
| val |= (1 << 27); |
| val |= (1 << 6); |
| val |= (1 << 5); |
| val |= (1 << 4); |
| wrl(wdt, CONTROL, val); |
| wrl(wdt, RESET_TERMINAL_COUNT, 5000); |
| wrl(wdt, WD_CMD_WINDOWN, 0xffff); |
| |
| val |= (1 << 1); |
| wrl(wdt, CONTROL, val); |
| } |
| |
| static int as390_wdt_restart(struct watchdog_device *wdog, unsigned long action, |
| void *data) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| |
| as390_wdt_set(wdt, 1); |
| /* wait for reset to assert... */ |
| mdelay(500); |
| |
| return 0; |
| } |
| |
| static int as390_wdt_start(struct watchdog_device *wdog) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| |
| as390_wdt_set(wdt, wdog->timeout * wdt->rate); |
| |
| return 0; |
| } |
| |
| static int as390_wdt_set_timeout(struct watchdog_device *wdog, unsigned int t) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| |
| as390_wdt_set(wdt, t * wdt->rate); |
| wdog->timeout = t; |
| |
| return 0; |
| } |
| |
| static unsigned int as390_wdt_get_timeleft(struct watchdog_device *wdog) |
| { |
| struct as390_wdt *wdt = watchdog_get_drvdata(wdog); |
| u32 count = rdl(wdt, TERMINAL_COUNT) - rdl(wdt, CURRENT_COUNT); |
| |
| return count / wdt->rate; |
| } |
| |
| static irqreturn_t as390_wdt_irq_handler(int irq, void *dev_id) |
| { |
| return IRQ_HANDLED; |
| } |
| |
| static const struct watchdog_ops as390_wdt_ops = { |
| .owner = THIS_MODULE, |
| .start = as390_wdt_start, |
| .stop = as390_wdt_stop, |
| .ping = as390_wdt_ping, |
| .set_timeout = as390_wdt_set_timeout, |
| .get_timeleft = as390_wdt_get_timeleft, |
| .restart = as390_wdt_restart, |
| }; |
| |
| static const struct watchdog_info as390_wdt_info = { |
| .options = WDIOF_SETTIMEOUT | WDIOF_MAGICCLOSE |
| | WDIOF_KEEPALIVEPING, |
| .identity = "Synaptics AS390 Watchdog", |
| }; |
| |
| static int as390_wdt_probe(struct platform_device *pdev) |
| { |
| int ret, irq; |
| struct resource *res; |
| struct as390_wdt *wdt; |
| struct watchdog_device *wdd; |
| struct device *dev = &pdev->dev; |
| |
| wdt = devm_kzalloc(dev, sizeof(struct as390_wdt), GFP_KERNEL); |
| if (!wdt) |
| return -ENOMEM; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| wdt->base = devm_ioremap_resource(dev, res); |
| if (IS_ERR(wdt->base)) |
| return PTR_ERR(wdt->base); |
| |
| wdt->rst = devm_reset_control_get_optional(&pdev->dev, NULL); |
| if (IS_ERR(wdt->rst)) |
| return PTR_ERR(wdt->rst); |
| |
| wdt->clk = devm_clk_get(dev, NULL); |
| if (IS_ERR(wdt->clk)) |
| return PTR_ERR(wdt->clk); |
| clk_prepare_enable(wdt->clk); |
| |
| wdd = &wdt->wdd; |
| wdd->info = &as390_wdt_info; |
| wdd->ops = &as390_wdt_ops; |
| wdt->rate = clk_get_rate(wdt->clk); |
| wdd->max_timeout = U32_MAX / wdt->rate; |
| wdd->timeout = wdd->max_timeout; |
| wdd->parent = dev; |
| wdd->min_timeout = 1; |
| |
| irq = platform_get_irq(pdev, 0); |
| if (irq < 0) { |
| ret = irq; |
| goto out_disable_clk; |
| } |
| ret = devm_request_irq(dev, irq, as390_wdt_irq_handler, 0, |
| pdev->name, wdt); |
| if (ret) |
| goto out_disable_clk; |
| |
| watchdog_set_drvdata(wdd, wdt); |
| watchdog_set_restart_priority(wdd, 128); |
| ret = devm_watchdog_register_device(dev, wdd); |
| if (ret) |
| goto out_disable_clk; |
| |
| return 0; |
| |
| out_disable_clk: |
| clk_disable_unprepare(wdt->clk); |
| return ret; |
| } |
| |
| static const struct of_device_id as390_wdt_of_match[] = { |
| { .compatible = "syna,as390-wdt", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, as390_wdt_of_match); |
| |
| static struct platform_driver as390_wdt_driver = { |
| .probe = as390_wdt_probe, |
| .driver = { |
| .name = "as390_wdt", |
| .of_match_table = as390_wdt_of_match, |
| }, |
| }; |
| module_platform_driver(as390_wdt_driver); |
| |
| MODULE_AUTHOR("Jisheng Zhang<jszhang@kernel.org>"); |
| MODULE_DESCRIPTION("Synaptics AS390 Watchdog Driver"); |
| MODULE_LICENSE("GPL v2"); |