| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) 2026 Nuvoton technology corporation. |
| * |
| * Author: Zi-Yu Chen <zychennvt@gmail.com> |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/clk.h> |
| #include <linux/err.h> |
| #include <linux/init.h> |
| #include <linux/interrupt.h> |
| #include <linux/io.h> |
| #include <linux/iopoll.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/moduleparam.h> |
| #include <linux/of.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_wakeirq.h> |
| #include <linux/property.h> |
| #include <linux/types.h> |
| #include <linux/watchdog.h> |
| |
| #define DRV_NAME "ma35d1-wdt" |
| |
| #define REG_WDT_CTL 0x00 |
| #define REG_WDT_RSTCNT 0x08 |
| |
| #define TOUTSEL GENMASK(11, 8) |
| #define WDTEN BIT(7) |
| #define INTEN BIT(6) |
| #define WKF BIT(5) |
| #define WKEN BIT(4) |
| #define IF BIT(3) |
| #define RSTF BIT(2) |
| #define RSTEN BIT(1) |
| #define SYNC BIT(30) |
| |
| #define WDT_DEFAULT_TIMEOUT 32 |
| #define RESET_COUNTER 0x00005AA5 |
| |
| static bool nowayout = WATCHDOG_NOWAYOUT; |
| static unsigned int timeout; |
| |
| struct ma35d1_wdt_dev { |
| struct watchdog_device wdt_dev; |
| spinlock_t lock; |
| void __iomem *wdt_base; |
| struct clk *clk; |
| unsigned long clk_rate; |
| int irq; |
| }; |
| |
| static int ma35d1_wdt_wait_sync(struct ma35d1_wdt_dev *ma35d1_wdt) |
| { |
| unsigned int val; |
| |
| return readl_relaxed_poll_timeout_atomic(ma35d1_wdt->wdt_base + |
| REG_WDT_CTL, val, !(val & SYNC), 10, 125); |
| } |
| |
| /** |
| * ma35d1_wdt_stop - Stop the watchdog. |
| * |
| * @wdt_dev: watchdog device |
| * |
| * Read the contents of the CTL register, clear the WDTEN bit |
| * in the register and set the access key for successful write. |
| * |
| * Return: 0 on success, negative error otherwise. |
| */ |
| static int ma35d1_wdt_stop(struct watchdog_device *wdt_dev) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = watchdog_get_drvdata(wdt_dev); |
| unsigned int val; |
| int ret; |
| |
| guard(spinlock_irqsave)(&ma35d1_wdt->lock); |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| val &= ~WDTEN; |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| ret = ma35d1_wdt_wait_sync(ma35d1_wdt); |
| if (ret) { |
| dev_err(wdt_dev->parent, "Wait for WDTEN SYNC timeout!\n"); |
| return ret; |
| } |
| return 0; |
| } |
| |
| static int ma35d1_wdt_ping(struct watchdog_device *wdt_dev) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = watchdog_get_drvdata(wdt_dev); |
| |
| writel_relaxed(RESET_COUNTER, ma35d1_wdt->wdt_base + REG_WDT_RSTCNT); |
| |
| return 0; |
| } |
| |
| static int ma35d1_wdt_set_timeout(struct watchdog_device *wdt_dev, |
| unsigned int timeout) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = watchdog_get_drvdata(wdt_dev); |
| unsigned long target_ticks; |
| unsigned int val, i; |
| static const uint8_t toutsel_shifts[] = { 4, 6, 8, 10, 12, |
| 14, 16, 18, 20 }; |
| |
| if (timeout < (ma35d1_wdt->wdt_dev.max_hw_heartbeat_ms / 1000)) { |
| target_ticks = (unsigned long)timeout * ma35d1_wdt->clk_rate; |
| |
| for (i = 0; i < ARRAY_SIZE(toutsel_shifts); i++) { |
| if ((1UL << toutsel_shifts[i]) >= target_ticks) |
| break; |
| } |
| /* To avoid truncation errors (0 seconds) during division. */ |
| wdt_dev->timeout = |
| (1UL << toutsel_shifts[i]) / ma35d1_wdt->clk_rate; |
| if (wdt_dev->timeout == 0) |
| wdt_dev->timeout = 1; |
| |
| } else { |
| i = ARRAY_SIZE(toutsel_shifts) - 1; |
| wdt_dev->timeout = timeout; |
| } |
| |
| guard(spinlock_irqsave)(&ma35d1_wdt->lock); |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| val &= ~TOUTSEL; |
| val |= FIELD_PREP(TOUTSEL, i); |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| |
| ma35d1_wdt_ping(wdt_dev); |
| return 0; |
| } |
| |
| static int ma35d1_wdt_start(struct watchdog_device *wdt_dev) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = watchdog_get_drvdata(wdt_dev); |
| void __iomem *wdt_base = ma35d1_wdt->wdt_base; |
| unsigned int val; |
| int ret; |
| |
| ret = ma35d1_wdt_set_timeout(wdt_dev, wdt_dev->timeout); |
| if (ret < 0) |
| return ret; |
| |
| guard(spinlock_irqsave)(&ma35d1_wdt->lock); |
| val = readl_relaxed(wdt_base + REG_WDT_CTL); |
| val |= (WDTEN | RSTEN); |
| |
| writel_relaxed(val, wdt_base + REG_WDT_CTL); |
| ret = ma35d1_wdt_wait_sync(ma35d1_wdt); |
| if (ret) { |
| dev_err(wdt_dev->parent, "Wait for WDTEN SYNC timeout!\n"); |
| return ret; |
| } |
| |
| writel_relaxed(RESET_COUNTER, wdt_base + REG_WDT_RSTCNT); |
| |
| return 0; |
| } |
| |
| static const struct watchdog_info ma35d1_wdt_info = { |
| .identity = DRV_NAME, |
| .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE | |
| WDIOF_CARDRESET, |
| }; |
| |
| static const struct watchdog_ops ma35d1_wdt_ops = { |
| .owner = THIS_MODULE, |
| .start = ma35d1_wdt_start, |
| .stop = ma35d1_wdt_stop, |
| .ping = ma35d1_wdt_ping, |
| .set_timeout = ma35d1_wdt_set_timeout, |
| }; |
| |
| static irqreturn_t ma35d1_wdt_isr(int irq, void *dev_id) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = dev_id; |
| unsigned int val; |
| |
| /* Clear the flag if set */ |
| guard(spinlock)(&ma35d1_wdt->lock); |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int ma35d1_wdt_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct ma35d1_wdt_dev *ma35d1_wdt; |
| unsigned long clk_rate, val; |
| int ret; |
| |
| ma35d1_wdt = devm_kzalloc(dev, sizeof(*ma35d1_wdt), GFP_KERNEL); |
| if (!ma35d1_wdt) |
| return -ENOMEM; |
| |
| spin_lock_init(&ma35d1_wdt->lock); |
| platform_set_drvdata(pdev, ma35d1_wdt); |
| |
| ma35d1_wdt->wdt_base = devm_platform_ioremap_resource(pdev, 0); |
| if (IS_ERR(ma35d1_wdt->wdt_base)) |
| return PTR_ERR(ma35d1_wdt->wdt_base); |
| |
| ma35d1_wdt->clk = devm_clk_get_enabled(dev, NULL); |
| if (IS_ERR(ma35d1_wdt->clk)) |
| return dev_err_probe(dev, PTR_ERR(ma35d1_wdt->clk), |
| "Can't get Watchdog clock\n"); |
| |
| clk_rate = clk_get_rate(ma35d1_wdt->clk); |
| if (!clk_rate) |
| return -EINVAL; |
| ma35d1_wdt->clk_rate = clk_rate; |
| |
| ma35d1_wdt->irq = platform_get_irq(pdev, 0); |
| if (ma35d1_wdt->irq < 0) |
| return dev_err_probe(dev, ma35d1_wdt->irq, |
| "failed to get irq\n"); |
| |
| ma35d1_wdt->wdt_dev.info = &ma35d1_wdt_info; |
| ma35d1_wdt->wdt_dev.ops = &ma35d1_wdt_ops; |
| ma35d1_wdt->wdt_dev.timeout = WDT_DEFAULT_TIMEOUT; |
| ma35d1_wdt->wdt_dev.min_timeout = 1; |
| ma35d1_wdt->wdt_dev.max_hw_heartbeat_ms = (1U << 20) * 1000 / clk_rate; |
| ma35d1_wdt->wdt_dev.parent = dev; |
| |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| if (val & RSTF) { |
| ma35d1_wdt->wdt_dev.bootstatus = WDIOF_CARDRESET; |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| } |
| |
| if (val & WDTEN) |
| set_bit(WDOG_HW_RUNNING, &ma35d1_wdt->wdt_dev.status); |
| |
| watchdog_set_drvdata(&ma35d1_wdt->wdt_dev, ma35d1_wdt); |
| watchdog_set_nowayout(&ma35d1_wdt->wdt_dev, nowayout); |
| watchdog_init_timeout(&ma35d1_wdt->wdt_dev, timeout, &pdev->dev); |
| |
| ma35d1_wdt_set_timeout(&ma35d1_wdt->wdt_dev, |
| ma35d1_wdt->wdt_dev.timeout); |
| |
| ret = devm_request_irq(dev, ma35d1_wdt->irq, ma35d1_wdt_isr, 0, |
| dev_name(dev), ma35d1_wdt); |
| if (ret) |
| return dev_err_probe(dev, ret, "cannot claim IRQ %d\n", |
| ma35d1_wdt->irq); |
| |
| if (device_property_read_bool(dev, "wakeup-source")) { |
| ret = devm_device_init_wakeup(dev); |
| if (ret) |
| return ret; |
| |
| ret = devm_pm_set_wake_irq(dev, ma35d1_wdt->irq); |
| if (ret) |
| return ret; |
| } |
| |
| watchdog_stop_on_reboot(&ma35d1_wdt->wdt_dev); |
| watchdog_stop_on_unregister(&ma35d1_wdt->wdt_dev); |
| ret = devm_watchdog_register_device(dev, &ma35d1_wdt->wdt_dev); |
| if (ret) |
| return ret; |
| |
| return 0; |
| } |
| |
| static int ma35d1_wdt_suspend(struct device *dev) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = dev_get_drvdata(dev); |
| |
| if (watchdog_active(&ma35d1_wdt->wdt_dev) || |
| watchdog_hw_running(&ma35d1_wdt->wdt_dev)) { |
| u32 val; |
| int ret; |
| |
| guard(spinlock_irqsave)(&ma35d1_wdt->lock); |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| |
| if (device_may_wakeup(dev)) { |
| val &= ~RSTEN; |
| val |= (INTEN | WKEN); |
| } else { |
| val &= ~(WDTEN | RSTEN); |
| } |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| ret = ma35d1_wdt_wait_sync(ma35d1_wdt); |
| if (ret) { |
| dev_err(dev, "Wait for WDTEN SYNC timeout!\n"); |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int ma35d1_wdt_resume(struct device *dev) |
| { |
| struct ma35d1_wdt_dev *ma35d1_wdt = dev_get_drvdata(dev); |
| |
| if (watchdog_active(&ma35d1_wdt->wdt_dev) || |
| watchdog_hw_running(&ma35d1_wdt->wdt_dev)) { |
| u32 val; |
| int ret; |
| |
| guard(spinlock_irqsave)(&ma35d1_wdt->lock); |
| val = readl_relaxed(ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| |
| if (device_may_wakeup(dev)) { |
| val |= RSTEN; |
| val &= ~(INTEN | WKEN); |
| } else { |
| val |= (WDTEN | RSTEN); |
| } |
| writel_relaxed(val, ma35d1_wdt->wdt_base + REG_WDT_CTL); |
| writel_relaxed(RESET_COUNTER, |
| ma35d1_wdt->wdt_base + REG_WDT_RSTCNT); |
| ret = ma35d1_wdt_wait_sync(ma35d1_wdt); |
| if (ret) { |
| dev_err(dev, "Wait for WDTEN SYNC timeout!\n"); |
| return ret; |
| } |
| } |
| return 0; |
| } |
| |
| static DEFINE_SIMPLE_DEV_PM_OPS(ma35d1_wdt_pm_ops, ma35d1_wdt_suspend, |
| ma35d1_wdt_resume); |
| |
| static const struct of_device_id ma35d1_wdt_dt_ids[] = { |
| { .compatible = "nuvoton,ma35d1-wdt" }, |
| { /* sentinel */ }, |
| }; |
| MODULE_DEVICE_TABLE(of, ma35d1_wdt_dt_ids); |
| |
| static struct platform_driver ma35d1_wdt_driver = { |
| .probe = ma35d1_wdt_probe, |
| .driver = { |
| .name = DRV_NAME, |
| .pm = pm_ptr(&ma35d1_wdt_pm_ops), |
| .of_match_table = ma35d1_wdt_dt_ids, |
| }, |
| }; |
| |
| module_platform_driver(ma35d1_wdt_driver); |
| |
| module_param(timeout, uint, 0); |
| MODULE_PARM_DESC(timeout, "Watchdog heartbeat in seconds"); |
| |
| module_param(nowayout, bool, 0); |
| MODULE_PARM_DESC( |
| nowayout, |
| "Watchdog cannot be stopped once started (default=" __MODULE_STRING( |
| WATCHDOG_NOWAYOUT) ")"); |
| |
| MODULE_AUTHOR("Zi-Yu Chen <zychennvt@gmail.com>"); |
| MODULE_DESCRIPTION("Nuvoton MA35D1 Watchdog Timer Driver"); |
| MODULE_LICENSE("GPL"); |