[PATCH v7 3/4] watchdog: Add support for VIA/WonderMedia SoC watchdog functionality

From: Alexey Charkov

Date: Fri Nov 13 2026 - 17:07:34 EST


VIA/WonderMedia SoCs can use their system timer's first channel as a
watchdog device which will reset the system if the clocksource counter
matches the value given in its match register 0 and if the watchdog
function is enabled.

Since the watchdog function is tightly coupled to the timer itself, it
is implemented as an auxiliary device of the timer device. Build it in
rather than as a module, as the timer it depends on is always built in
and the driver is tiny.

Report a watchdog found running at probe, as one armed before a kexec
would be, so that the core keeps it fed until userspace takes over.

Signed-off-by: Alexey Charkov <alchark@xxxxxxxxx>
---
Documentation/watchdog/watchdog-parameters.rst | 7 ++
MAINTAINERS | 1 +
drivers/watchdog/Kconfig | 12 +++
drivers/watchdog/Makefile | 1 +
drivers/watchdog/vt8500-wdt.c | 132 +++++++++++++++++++++++++
5 files changed, 153 insertions(+)

diff --git a/Documentation/watchdog/watchdog-parameters.rst b/Documentation/watchdog/watchdog-parameters.rst
index 348229ffcb43..27453b659545 100644
--- a/Documentation/watchdog/watchdog-parameters.rst
+++ b/Documentation/watchdog/watchdog-parameters.rst
@@ -637,6 +637,13 @@ uniphier_wdt:

-------------------------------------------------

+vt8500_wdt:
+ nowayout:
+ Watchdog cannot be stopped once started
+ (default=kernel config parameter)
+
+-------------------------------------------------
+
w83627hf_wdt:
wdt_io:
w83627hf/thf WDT io port (default 0x2E)
diff --git a/MAINTAINERS b/MAINTAINERS
index e8c2b1101b68..73039535eba2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4010,6 +4010,7 @@ F: drivers/tty/serial/vt8500_serial.c
F: drivers/video/fbdev/vt8500lcdfb.*
F: drivers/video/fbdev/wm8505fb*
F: drivers/video/fbdev/wmt_ge_rops.*
+F: drivers/watchdog/vt8500-wdt.c
F: include/clocksource/vt8500-timer.h

ARM/ZTE ZX29 SOC SUPPORT
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index c884f86be4e3..a252f9c9c2c8 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -1183,6 +1183,18 @@ config SUNPLUS_WATCHDOG
To compile this driver as a module, choose M here: the
module will be called sunplus_wdt.

+config VT8500_WATCHDOG
+ bool "VIA/WonderMedia VT8500 watchdog support"
+ depends on ARCH_VT8500 || COMPILE_TEST
+ select WATCHDOG_CORE
+ select AUXILIARY_BUS
+ help
+ VIA/WonderMedia SoCs can use their system timer as a hardware
+ watchdog, as long as the first timer channel is free from other
+ uses and respective function is enabled in its registers. To
+ make use of it, say Y here and ensure that the device tree
+ lists at least two interrupts for the VT8500 timer device.
+
# X86 (i386 + ia64 + x86_64) Architecture

config ACQUIRE_WDT
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index 19cb25f51624..5a5a111b4815 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -106,6 +106,7 @@ obj-$(CONFIG_SUNPLUS_WATCHDOG) += sunplus_wdt.o
obj-$(CONFIG_MARVELL_GTI_WDT) += marvell_gti_wdt.o
obj-$(CONFIG_GUNYAH_WATCHDOG) += gunyah_wdt.o
obj-$(CONFIG_MA35D1_WDT) += ma35d1_wdt.o
+obj-$(CONFIG_VT8500_WATCHDOG) += vt8500-wdt.o

# X86 (i386 + ia64 + x86_64) Architecture
obj-$(CONFIG_ACQUIRE_WDT) += acquirewdt.o
diff --git a/drivers/watchdog/vt8500-wdt.c b/drivers/watchdog/vt8500-wdt.c
new file mode 100644
index 000000000000..e0f838fd1d50
--- /dev/null
+++ b/drivers/watchdog/vt8500-wdt.c
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: 2026 Alexey Charkov <alchark@xxxxxxxxx>
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bits.h>
+#include <linux/container_of.h>
+#include <linux/init.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/limits.h>
+#include <linux/minmax.h>
+#include <linux/moduleparam.h>
+#include <linux/stringify.h>
+#include <linux/types.h>
+#include <linux/watchdog.h>
+
+#include <clocksource/vt8500-timer.h>
+
+/* Offsets within the timer register block */
+#define VT8500_WDT_MATCH_REG 0x0000 /* timer match register 0 */
+#define VT8500_WDT_EN_REG 0x0018
+#define VT8500_WDT_EN BIT(0)
+#define VT8500_WDT_ACC_STS_REG 0x0024 /* timer access status */
+#define VT8500_WDT_ACC_WR_MATCH BIT(0) /* match register 0 write pending */
+
+#define VT8500_WDT_DEFAULT_TIMEOUT 30 /* seconds */
+
+static bool nowayout = WATCHDOG_NOWAYOUT;
+module_param(nowayout, bool, 0);
+MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default="
+ __stringify(WATCHDOG_NOWAYOUT) ")");
+
+/* Arm the watchdog to reset the SoC @cycles counter ticks from now */
+static int vt8500_watchdog_arm(struct vt8500_wdt_info *info, u64 cycles)
+{
+ u32 deadline, sts;
+ int ret;
+
+ /* Let a previous match write settle, as the timer driver does */
+ ret = readl_poll_timeout_atomic(info->regbase + VT8500_WDT_ACC_STS_REG,
+ sts, !(sts & VT8500_WDT_ACC_WR_MATCH),
+ 1, 10000);
+ if (ret)
+ return ret;
+
+ /*
+ * The deadline is matched against the hardware clocksource counter,
+ * which is a u32 value incrementing at VT8500_TIMER_HZ and continuing
+ * past wraparound. When the return value of timer_next is greater than
+ * U32_MAX then the match should occur after the hardware counter wraps
+ * around, thus we take only the lower 32 bits of timer_next return val
+ */
+ deadline = info->timer_next(cycles);
+ writel(deadline, info->regbase + VT8500_WDT_MATCH_REG);
+ writel(VT8500_WDT_EN, info->regbase + VT8500_WDT_EN_REG);
+
+ return 0;
+}
+
+static int vt8500_watchdog_start(struct watchdog_device *wdd)
+{
+ struct vt8500_wdt_info *info = watchdog_get_drvdata(wdd);
+ u32 timeout_ms = min(wdd->timeout * 1000, wdd->max_hw_heartbeat_ms);
+
+ return vt8500_watchdog_arm(info,
+ (u64)timeout_ms * (VT8500_TIMER_HZ / 1000));
+}
+
+static int vt8500_watchdog_stop(struct watchdog_device *wdd)
+{
+ struct vt8500_wdt_info *info = watchdog_get_drvdata(wdd);
+
+ writel(0, info->regbase + VT8500_WDT_EN_REG);
+
+ return 0;
+}
+
+static const struct watchdog_ops vt8500_watchdog_ops = {
+ .start = vt8500_watchdog_start,
+ .stop = vt8500_watchdog_stop,
+};
+
+static const struct watchdog_info vt8500_watchdog_info = {
+ .identity = "VIA VT8500 watchdog",
+ .options = WDIOF_MAGICCLOSE |
+ WDIOF_KEEPALIVEPING |
+ WDIOF_SETTIMEOUT,
+};
+
+static int vt8500_wdt_probe(struct auxiliary_device *auxdev,
+ const struct auxiliary_device_id *id)
+{
+ struct vt8500_wdt_info *info;
+ struct watchdog_device *wdd;
+
+ wdd = devm_kzalloc_obj(&auxdev->dev, *wdd);
+ if (!wdd)
+ return -ENOMEM;
+
+ wdd->info = &vt8500_watchdog_info;
+ wdd->ops = &vt8500_watchdog_ops;
+ wdd->min_timeout = 1;
+ wdd->timeout = VT8500_WDT_DEFAULT_TIMEOUT;
+ wdd->max_hw_heartbeat_ms = U32_MAX / (VT8500_TIMER_HZ / 1000);
+ wdd->parent = &auxdev->dev;
+
+ info = container_of(auxdev, struct vt8500_wdt_info, auxdev);
+ watchdog_set_drvdata(wdd, info);
+ watchdog_set_nowayout(wdd, nowayout);
+
+ /* Let the core feed a watchdog left running, e.g. across kexec */
+ if (readl(info->regbase + VT8500_WDT_EN_REG) & VT8500_WDT_EN)
+ set_bit(WDOG_HW_RUNNING, &wdd->status);
+
+ return devm_watchdog_register_device(&auxdev->dev, wdd);
+}
+
+static const struct auxiliary_device_id vt8500_wdt_ids[] = {
+ { .name = "timer_vt8500.vt8500-wdt" },
+ {}
+};
+
+static struct auxiliary_driver vt8500_wdt_driver = {
+ .name = "vt8500-wdt",
+ .probe = vt8500_wdt_probe,
+ .id_table = vt8500_wdt_ids,
+ .driver = {
+ /* Unbinding would leave the hardware armed with no one to ping */
+ .suppress_bind_attrs = true,
+ },
+};
+builtin_driver(vt8500_wdt_driver, auxiliary_driver_register);

--
2.56.0