mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support
@ 2026-11-10 12:39 Artur Kowalski
  2026-11-10 12:39 ` [PATCH 1/6] clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free Artur Kowalski
                   ` (5 more replies)
  0 siblings, 6 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

The motivation is to improve recovery from kernel hangs which have been
steadily driving me to the doorsteps of psych ward for a few months.
Those events are rare, some happen at boot, others while trying to use
tablet for something real like reading PDFs, browsing web (to the extent
possible without working GPU accel), or just using it as thin client for
another device.

As all my attempts to hunt down even slightest hints of what went down
have fallen fouly (device just refuses hang with UART connected),
therefore this patch series is born in the hope it will finally enable
me and others to catch those bugs (use wdt and pstore).

This series adds Tegra20 watchdog support to tegra_wdt. Tegra20's boot
ROM hangs if VDE partition is off after warm reset (for normal reboots
this is handled by 8f0c714ad9be). The series offers two mitigations:

- By default, tegra_wdt stops the watchdog for good before the
  partition is powered off for the first time, using a new PMC power
  off notifier (patches 5 and 6). A watchdog started by the
  bootloader still covers the early boot (until PMC driver comes up).

- Booting with tegra_pmc.vde_always_on=1 keeps the partition powered
  on, so watchdog is fully usable at the expense of higher power
  consumption.

This has been tested on ASUS TF101:

- with tegra_pmc.vde_always_on=1, watchdog stays on, the device has been
  idling for more than hour (heartbeat 10 s, pinged by userspace every 8
  s), no issues encountered. Suspend/resume works. On timeout device
  reboots normally, u-boot still works after warm boot, Linux boots.
  Tested both with userspace opening watchdog device without pinging,
  and by crashing the kernel (panic=0).
- with tegra_pmc.vde_always_on=0 watchdog is permanently turned off when
  the PMC driver powers off VDE partition for the first time, after that
  userspace is denied opening the device with -EBUSY. No issues
  encountered.

Another series for fixing some other issues with warm reboots (not
related to watchdog itself) will follow shortly.

Patch 4 got flagged by checkpatch (WARNING: ENOSYS means 'invalid
syscall nr' and nothing else). I kept it as-is to keep consistency with
existing stubs. If this is not desirable just tell me.

---
Patch 3 depends on patch 1 and should not be merged before or watchdog
driver will conflict with clocksource driver.

To: Daniel Lezcano <daniel.lezcano@kernel.org>
To: Thomas Gleixner <tglx@kernel.org>
To: Thierry Reding <thierry.reding@kernel.org>
To: Jonathan Hunter <jonathanh@nvidia.com>
To: Wim Van Sebroeck <wim@linux-watchdog.org>
To: Guenter Roeck <linux@roeck-us.net>
To: Jonathan Corbet <corbet@lwn.net>
To: Shuah Khan <skhan@linuxfoundation.org>
To: Randy Dunlap <rdunlap@infradead.org>
To: Philipp Zabel <p.zabel@pengutronix.de>
Cc: linux-kernel@vger.kernel.org
Cc: linux-tegra@vger.kernel.org
Cc: linux-watchdog@vger.kernel.org
Cc: linux-doc@vger.kernel.org
Cc: clamor95@gmail.com
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>

---
Artur Kowalski (6):
      clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free
      watchdog: tegra_wdt: Move SoC specific parts into match data
      watchdog: tegra_wdt: Support Tegra20
      soc/tegra: pmc: Add an option to keep the VDE partition on
      soc/tegra: pmc: Add a partition power off notifier
      watchdog: tegra_wdt: Stop on Tegra20 VDE power off

 Documentation/admin-guide/kernel-parameters.txt |  12 +
 Documentation/watchdog/watchdog-parameters.rst  |   5 +
 drivers/clocksource/timer-tegra.c               |  40 ++-
 drivers/soc/tegra/pmc.c                         | 158 +++++++++++-
 drivers/watchdog/tegra_wdt.c                    | 330 +++++++++++++++++++++++-
 include/soc/tegra/pmc.h                         |  19 ++
 6 files changed, 540 insertions(+), 24 deletions(-)
---
base-commit: 05441d697f7f9a927264f4058cbbc6a03dfcac9d
change-id: 20261108-t20-for-upstream-497bc765188c

Best regards,
--  
Artur Kowalski <arturkow2000@gmail.com>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 1/6] clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  2026-11-10 12:39 ` [PATCH 2/6] watchdog: tegra_wdt: Move SoC specific parts into match data Artur Kowalski
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

Tegra20 has no watchdog block. Instead, the clock and reset controller
resets the system when the interrupt of TIMER1 or TIMER2 fires while
the previous one is still pending. A bootloader can start this watchdog
and leave it running for the kernel.

Since commit f6d50ec5f85c ("clocksource/drivers/tegra: Support per-CPU
timers on all Tegra's"), the driver uses TIMER1-2 as the per-CPU clock
event devices on Tegra20, where it used TIMER3 before. When CPU0's
clock event device takes over TIMER1, the clock and reset controller
still resets the system if a timer interrupt fires while the previous
one hasn't been handled yet, which resets about half of the boots
early. If the system survives that, the watchdog stays enabled but
never fires, as CPU0 keeps clearing the timer's interrupts.

Use TIMER3-4 for the per-CPU clock event devices on Tegra20, leaving
TIMER1-2 to the watchdog. Tegra30 needs TIMER1-4 for its four CPUs and
its watchdog uses TIMER5, so keep its mapping.

A watchdog left running by the bootloader is then no longer disarmed,
so it resets the system unless it is pinged. That needs the tegra_wdt
driver, which gains Tegra20 support in the following patches, built in
or loaded before the watchdog's timeout expires.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 drivers/clocksource/timer-tegra.c | 40 ++++++++++++++++++++++++++++++---------
 1 file changed, 31 insertions(+), 9 deletions(-)

diff --git a/drivers/clocksource/timer-tegra.c b/drivers/clocksource/timer-tegra.c
index 35b6ce9deffa..0f63bf8bd706 100644
--- a/drivers/clocksource/timer-tegra.c
+++ b/drivers/clocksource/timer-tegra.c
@@ -208,10 +208,15 @@ static struct clocksource suspend_rtc_clocksource = {
 	.flags	= CLOCK_SOURCE_IS_CONTINUOUS | CLOCK_SOURCE_SUSPEND_NONSTOP,
 };
 
-static inline unsigned int tegra_base_for_cpu(int cpu, bool tegra20)
+/*
+ * The timer used by a CPU is TIMER1-4 on Tegra20 and Tegra30, starting from
+ * @first, or TIMER10-13 on later SoCs.
+ */
+static inline unsigned int tegra_base_for_cpu(int cpu, bool tegra20,
+					      unsigned int first)
 {
 	if (tegra20) {
-		switch (cpu) {
+		switch (cpu + first) {
 		case 0:
 			return TIMER1_BASE;
 		case 1:
@@ -226,10 +231,11 @@ static inline unsigned int tegra_base_for_cpu(int cpu, bool tegra20)
 	return TIMER10_BASE + cpu * 8;
 }
 
-static inline unsigned int tegra_irq_idx_for_cpu(int cpu, bool tegra20)
+static inline unsigned int tegra_irq_idx_for_cpu(int cpu, bool tegra20,
+						 unsigned int first)
 {
 	if (tegra20)
-		return TIMER1_IRQ_IDX + cpu;
+		return TIMER1_IRQ_IDX + first + cpu;
 
 	return TIMER10_IRQ_IDX + cpu;
 }
@@ -248,7 +254,7 @@ static inline unsigned long tegra_rate_for_timer(struct timer_of *to,
 }
 
 static int __init tegra_init_timer(struct device_node *np, bool tegra20,
-				   int rating)
+				   int rating, unsigned int first)
 {
 	struct timer_of *to;
 	int cpu, ret;
@@ -297,12 +303,18 @@ static int __init tegra_init_timer(struct device_node *np, bool tegra20,
 
 	writel_relaxed(usec_config, timer_reg_base + TIMERUS_USEC_CFG);
 
+	if (tegra20 && first + num_possible_cpus() > 4) {
+		pr_err("not enough timers for %u CPUs\n", num_possible_cpus());
+		ret = -EINVAL;
+		goto out;
+	}
+
 	for_each_possible_cpu(cpu) {
 		struct timer_of *cpu_to = per_cpu_ptr(&tegra_to, cpu);
 		unsigned long flags = IRQF_TIMER | IRQF_NOBALANCING;
 		unsigned long rate = tegra_rate_for_timer(to, tegra20);
-		unsigned int base = tegra_base_for_cpu(cpu, tegra20);
-		unsigned int idx = tegra_irq_idx_for_cpu(cpu, tegra20);
+		unsigned int base = tegra_base_for_cpu(cpu, tegra20, first);
+		unsigned int idx = tegra_irq_idx_for_cpu(cpu, tegra20, first);
 		unsigned int irq = irq_of_parse_and_map(np, idx);
 
 		if (!irq) {
@@ -376,12 +388,13 @@ static int __init tegra210_init_timer(struct device_node *np)
 	 * after CPUPORESET signal due to a system design shortcoming,
 	 * hence tegra-timer is more preferable on Tegra210.
 	 */
-	return tegra_init_timer(np, false, 460);
+	return tegra_init_timer(np, false, 460, 0);
 }
 TIMER_OF_DECLARE(tegra210_timer, "nvidia,tegra210-timer", tegra210_init_timer);
 
 static int __init tegra20_init_timer(struct device_node *np)
 {
+	unsigned int first = 0;
 	int rating;
 
 	/*
@@ -398,7 +411,16 @@ static int __init tegra20_init_timer(struct device_node *np)
 	else
 		rating = 330;
 
-	return tegra_init_timer(np, true, rating);
+	/*
+	 * Tegra20 has no watchdog block. Its watchdog is driven by TIMER1 or
+	 * TIMER2, which the bootloader may leave running, so leave both to it
+	 * and use TIMER3-4 for the two CPUs. Tegra30's watchdogs use TIMER5
+	 * and later, and its four CPUs need TIMER1-4.
+	 */
+	if (of_machine_is_compatible("nvidia,tegra20"))
+		first = 2;
+
+	return tegra_init_timer(np, true, rating, first);
 }
 TIMER_OF_DECLARE(tegra20_timer, "nvidia,tegra20-timer", tegra20_init_timer);
 

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 2/6] watchdog: tegra_wdt: Move SoC specific parts into match data
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
  2026-11-10 12:39 ` [PATCH 1/6] clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  2026-11-10 12:39 ` [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20 Artur Kowalski
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

The driver only supports the watchdog blocks of Tegra30 and later. Move
the parts that differ between SoC generations, namely the watchdog
operations, the maximum timeout and the register setup, into a per-SoC
structure selected through the match data, so that support for Tegra20,
which has no watchdog block, can be added.

Suspend and resume call the operations through the watchdog device, so
they work with any SoC.

No functional change.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 drivers/watchdog/tegra_wdt.c | 45 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 36 insertions(+), 9 deletions(-)

diff --git a/drivers/watchdog/tegra_wdt.c b/drivers/watchdog/tegra_wdt.c
index 6b3f65c4d593..bb6233320579 100644
--- a/drivers/watchdog/tegra_wdt.c
+++ b/drivers/watchdog/tegra_wdt.c
@@ -9,6 +9,7 @@
 #include <linux/io.h>
 #include <linux/of.h>
 #include <linux/platform_device.h>
+#include <linux/property.h>
 #include <linux/stringify.h>
 #include <linux/watchdog.h>
 
@@ -63,6 +64,13 @@ struct tegra_wdt {
 	void __iomem		*tmr_regs;
 };
 
+struct tegra_wdt_soc {
+	const struct watchdog_ops *ops;
+	unsigned int max_timeout;
+	int (*init)(struct platform_device *pdev, struct tegra_wdt *wdt,
+		    void __iomem *regs);
+};
+
 #define WDT_HEARTBEAT 120
 static int heartbeat = WDT_HEARTBEAT;
 module_param(heartbeat, int, 0);
@@ -180,14 +188,32 @@ static const struct watchdog_ops tegra_wdt_ops = {
 	.get_timeleft = tegra_wdt_get_timeleft,
 };
 
+static int tegra30_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
+			    void __iomem *regs)
+{
+	wdt->wdt_regs = regs + WDT_BASE;
+	wdt->tmr_regs = regs + WDT_TIMER_BASE;
+
+	return 0;
+}
+
+static const struct tegra_wdt_soc tegra30_wdt_soc = {
+	.ops = &tegra_wdt_ops,
+	.max_timeout = MAX_WDT_TIMEOUT,
+	.init = tegra30_wdt_init,
+};
+
 static int tegra_wdt_probe(struct platform_device *pdev)
 {
 	struct device *dev = &pdev->dev;
+	const struct tegra_wdt_soc *soc;
 	struct watchdog_device *wdd;
 	struct tegra_wdt *wdt;
 	void __iomem *regs;
 	int ret;
 
+	soc = device_get_match_data(dev);
+
 	/* This is the timer base. */
 	regs = devm_platform_ioremap_resource(pdev, 0);
 	if (IS_ERR(regs))
@@ -201,23 +227,24 @@ static int tegra_wdt_probe(struct platform_device *pdev)
 	if (!wdt)
 		return -ENOMEM;
 
-	/* Initialize struct tegra_wdt. */
-	wdt->wdt_regs = regs + WDT_BASE;
-	wdt->tmr_regs = regs + WDT_TIMER_BASE;
-
 	/* Initialize struct watchdog_device. */
 	wdd = &wdt->wdd;
 	wdd->timeout = heartbeat;
 	wdd->info = &tegra_wdt_info;
-	wdd->ops = &tegra_wdt_ops;
+	wdd->ops = soc->ops;
 	wdd->min_timeout = MIN_WDT_TIMEOUT;
-	wdd->max_timeout = MAX_WDT_TIMEOUT;
+	wdd->max_timeout = soc->max_timeout;
 	wdd->parent = dev;
 
 	watchdog_set_drvdata(wdd, wdt);
 
 	watchdog_set_nowayout(wdd, nowayout);
 
+	/* Initialize struct tegra_wdt. */
+	ret = soc->init(pdev, wdt, regs);
+	if (ret)
+		return ret;
+
 	watchdog_stop_on_unregister(wdd);
 	ret = devm_watchdog_register_device(dev, wdd);
 	if (ret)
@@ -236,7 +263,7 @@ static int tegra_wdt_suspend(struct device *dev)
 	struct tegra_wdt *wdt = dev_get_drvdata(dev);
 
 	if (watchdog_active(&wdt->wdd))
-		tegra_wdt_stop(&wdt->wdd);
+		wdt->wdd.ops->stop(&wdt->wdd);
 
 	return 0;
 }
@@ -246,13 +273,13 @@ static int tegra_wdt_resume(struct device *dev)
 	struct tegra_wdt *wdt = dev_get_drvdata(dev);
 
 	if (watchdog_active(&wdt->wdd))
-		tegra_wdt_start(&wdt->wdd);
+		wdt->wdd.ops->start(&wdt->wdd);
 
 	return 0;
 }
 
 static const struct of_device_id tegra_wdt_of_match[] = {
-	{ .compatible = "nvidia,tegra30-timer", },
+	{ .compatible = "nvidia,tegra30-timer", .data = &tegra30_wdt_soc },
 	{ },
 };
 MODULE_DEVICE_TABLE(of, tegra_wdt_of_match);

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
  2026-11-10 12:39 ` [PATCH 1/6] clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free Artur Kowalski
  2026-11-10 12:39 ` [PATCH 2/6] watchdog: tegra_wdt: Move SoC specific parts into match data Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  2026-11-10 16:21   ` Krzysztof Kozlowski
  2026-11-10 12:39 ` [PATCH 4/6] soc/tegra: pmc: Add an option to keep the VDE partition on Artur Kowalski
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

Tegra20 has no watchdog block. Instead, the clock and reset controller
can reset the system when the interrupt of TIMER1 or TIMER2 fires while
the previous one is still pending, enabled through the WDT bits of its
RST_SOURCE register. This relies on the timer driver leaving TIMER1-2
to the watchdog on Tegra20.

Use TIMER1 for it, running periodically with half of the timeout as its
period. Clearing the interrupt alone doesn't restart the period, which
would leave between half and all of the timeout after a ping, so a ping
also rewrites the timer setting, read back from the timer, to restart
it. The timer has no readable interrupt status, so the time left can't
be reported.

RST_SOURCE belongs to the clock and reset controller, whose driver
doesn't use it, so map only this register, without requesting the
controller's region.

A bootloader may leave the watchdog running. Take it over with the
driver's timeout and let the watchdog core ping it until user space
opens the device. Since it then runs without user space having started
it, stop it for suspend and restart it on resume whenever it runs, not
only when user space started it.

A watchdog reset leaves the PMC alone, and the boot ROM hangs if the VDE
power partition is off at that point. A following patch stops the
watchdog before the partition is powered off.

Tegra30 and later list "nvidia,tegra20-timer" as a fallback compatible
string, but they keep matching "nvidia,tegra30-timer", which is more
specific.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 drivers/watchdog/tegra_wdt.c | 164 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 162 insertions(+), 2 deletions(-)

diff --git a/drivers/watchdog/tegra_wdt.c b/drivers/watchdog/tegra_wdt.c
index bb6233320579..7c2c21f90de5 100644
--- a/drivers/watchdog/tegra_wdt.c
+++ b/drivers/watchdog/tegra_wdt.c
@@ -8,6 +8,7 @@
 #include <linux/interrupt.h>
 #include <linux/io.h>
 #include <linux/of.h>
+#include <linux/of_address.h>
 #include <linux/platform_device.h>
 #include <linux/property.h>
 #include <linux/stringify.h>
@@ -57,11 +58,35 @@
 #define TIMER_PTV			0x0
 #define TIMER_EN			(1 << 31)
 #define TIMER_PERIODIC			(1 << 30)
+#define TIMER_PTV_MAX			GENMASK(28, 0)
+#define TIMER_PCR			0x4
+#define TIMER_PCR_INTR_CLR		(1 << 30)
+
+/*
+ * Tegra20 has no watchdog block. Instead, the clock and reset controller can
+ * reset the system when the interrupt of timer 1 or 2 fires while the previous
+ * one is still pending. This is controlled by its RST_SOURCE register.
+ */
+#define T20_WDT_TIMER_BASE		0x0
+#define T20_RST_SOURCE			0x0
+#define T20_RST_SOURCE_WDT_EN		(1 << 5)
+#define T20_RST_SOURCE_WDT_SEL_TMR2	(1 << 4)
+#define T20_RST_SOURCE_WDT_SYS_RST_EN	(1 << 2)
+#define T20_RST_SOURCE_WDT_MASK		(T20_RST_SOURCE_WDT_EN | \
+					 T20_RST_SOURCE_WDT_SEL_TMR2 | \
+					 T20_RST_SOURCE_WDT_SYS_RST_EN)
+
+/*
+ * The reset occurs on the second expiry of the timer, so its period is half
+ * of the timeout. The timer has a fixed 1 MHz clock and counts PTV + 1 ticks.
+ */
+#define T20_MAX_WDT_TIMEOUT		((TIMER_PTV_MAX + 1ull) * 2 / USEC_PER_SEC)
 
 struct tegra_wdt {
 	struct watchdog_device	wdd;
 	void __iomem		*wdt_regs;
 	void __iomem		*tmr_regs;
+	void __iomem		*rst_source;	/* Tegra20 only */
 };
 
 struct tegra_wdt_soc {
@@ -197,6 +222,131 @@ static int tegra30_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
 	return 0;
 }
 
+static int tegra20_wdt_start(struct watchdog_device *wdd)
+{
+	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
+	u32 period = wdd->timeout * USEC_PER_SEC / 2;
+	u32 val;
+
+	writel(TIMER_EN | TIMER_PERIODIC | (period - 1),
+	       wdt->tmr_regs + TIMER_PTV);
+
+	/*
+	 * Clear an interrupt left pending by an earlier use of the timer,
+	 * which would otherwise reset the system on the first expiry.
+	 */
+	writel(TIMER_PCR_INTR_CLR, wdt->tmr_regs + TIMER_PCR);
+
+	val = readl(wdt->rst_source);
+	val &= ~T20_RST_SOURCE_WDT_MASK;
+	val |= T20_RST_SOURCE_WDT_EN | T20_RST_SOURCE_WDT_SYS_RST_EN;
+	writel(val, wdt->rst_source);
+
+	return 0;
+}
+
+static int tegra20_wdt_stop(struct watchdog_device *wdd)
+{
+	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
+	u32 val;
+
+	val = readl(wdt->rst_source);
+	val &= ~T20_RST_SOURCE_WDT_MASK;
+	writel(val, wdt->rst_source);
+
+	writel(0, wdt->tmr_regs + TIMER_PTV);
+	writel(TIMER_PCR_INTR_CLR, wdt->tmr_regs + TIMER_PCR);
+
+	return 0;
+}
+
+static int tegra20_wdt_ping(struct watchdog_device *wdd)
+{
+	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
+
+	/*
+	 * Clearing the interrupt alone doesn't restart the timer's period, so
+	 * the reset could follow as soon as half the timeout after the ping.
+	 * Rewriting the timer setting restarts the period. The interrupt is
+	 * cleared afterwards, so one fired in between doesn't count.
+	 */
+	writel(readl(wdt->tmr_regs + TIMER_PTV), wdt->tmr_regs + TIMER_PTV);
+	writel(TIMER_PCR_INTR_CLR, wdt->tmr_regs + TIMER_PCR);
+
+	return 0;
+}
+
+static int tegra20_wdt_set_timeout(struct watchdog_device *wdd,
+				   unsigned int timeout)
+{
+	wdd->timeout = timeout;
+
+	/* starting again reprograms the running watchdog */
+	if (watchdog_active(wdd))
+		return tegra20_wdt_start(wdd);
+
+	return 0;
+}
+
+static const struct watchdog_ops tegra20_wdt_ops = {
+	.owner = THIS_MODULE,
+	.start = tegra20_wdt_start,
+	.stop = tegra20_wdt_stop,
+	.ping = tegra20_wdt_ping,
+	.set_timeout = tegra20_wdt_set_timeout,
+};
+
+static int tegra20_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
+			    void __iomem *regs)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *np;
+	struct resource res;
+	int ret;
+
+	/*
+	 * RST_SOURCE belongs to the clock and reset controller, whose driver
+	 * doesn't use it. Map only this register, without requesting it, as
+	 * the region belongs to the clock and reset controller.
+	 */
+	np = of_find_compatible_node(NULL, NULL, "nvidia,tegra20-car");
+	if (!np)
+		return dev_err_probe(dev, -ENODEV,
+				     "clock and reset controller not found\n");
+
+	ret = of_address_to_resource(np, 0, &res);
+	of_node_put(np);
+	if (ret)
+		return ret;
+
+	wdt->rst_source = devm_ioremap(dev, res.start + T20_RST_SOURCE,
+				       sizeof(u32));
+	if (!wdt->rst_source)
+		return -ENOMEM;
+
+	wdt->tmr_regs = regs + T20_WDT_TIMER_BASE;
+
+	/*
+	 * The bootloader may leave the watchdog running. Take it over with
+	 * our own timeout and timer 1, and let the core ping it until user
+	 * space opens the device.
+	 */
+	if (readl(wdt->rst_source) & T20_RST_SOURCE_WDT_EN) {
+		dev_info(dev, "watchdog left running by the bootloader, taking it over (timeout %u sec)\n",
+			 wdt->wdd.timeout);
+		tegra20_wdt_start(&wdt->wdd);
+		set_bit(WDOG_HW_RUNNING, &wdt->wdd.status);
+	}
+
+	return 0;
+}
+
+static const struct tegra_wdt_soc tegra20_wdt_soc = {
+	.ops = &tegra20_wdt_ops,
+	.max_timeout = T20_MAX_WDT_TIMEOUT,
+	.init = tegra20_wdt_init,
+};
+
 static const struct tegra_wdt_soc tegra30_wdt_soc = {
 	.ops = &tegra_wdt_ops,
 	.max_timeout = MAX_WDT_TIMEOUT,
@@ -258,11 +408,16 @@ static int tegra_wdt_probe(struct platform_device *pdev)
 	return 0;
 }
 
+/*
+ * The watchdog may also run without user space having opened it, when it was
+ * taken over from the bootloader, so check whether the hardware runs. Calling
+ * the stop operation directly leaves WDOG_HW_RUNNING set for the resume.
+ */
 static int tegra_wdt_suspend(struct device *dev)
 {
 	struct tegra_wdt *wdt = dev_get_drvdata(dev);
 
-	if (watchdog_active(&wdt->wdd))
+	if (watchdog_hw_running(&wdt->wdd))
 		wdt->wdd.ops->stop(&wdt->wdd);
 
 	return 0;
@@ -272,14 +427,19 @@ static int tegra_wdt_resume(struct device *dev)
 {
 	struct tegra_wdt *wdt = dev_get_drvdata(dev);
 
-	if (watchdog_active(&wdt->wdd))
+	if (watchdog_hw_running(&wdt->wdd))
 		wdt->wdd.ops->start(&wdt->wdd);
 
 	return 0;
 }
 
+/*
+ * Tegra30 and later list "nvidia,tegra20-timer" as well, but match their more
+ * specific compatible string.
+ */
 static const struct of_device_id tegra_wdt_of_match[] = {
 	{ .compatible = "nvidia,tegra30-timer", .data = &tegra30_wdt_soc },
+	{ .compatible = "nvidia,tegra20-timer", .data = &tegra20_wdt_soc },
 	{ },
 };
 MODULE_DEVICE_TABLE(of, tegra_wdt_of_match);

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 4/6] soc/tegra: pmc: Add an option to keep the VDE partition on
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
                   ` (2 preceding siblings ...)
  2026-11-10 12:39 ` [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20 Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  2026-11-10 12:39 ` [PATCH 5/6] soc/tegra: pmc: Add a partition power off notifier Artur Kowalski
  2026-11-10 12:39 ` [PATCH 6/6] watchdog: tegra_wdt: Stop on Tegra20 VDE power off Artur Kowalski
  5 siblings, 0 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

After a warm reset, which doesn't reset the PMC, Tegra20's boot ROM
hangs if the VDE partition is powered off. The driver powers the
partition off when it registers it, and genpd powers it off whenever
the VDE is idle. The VDE driver powers it on again when the system
shuts down, since commit 8f0c714ad9be ("media: staging: tegra-vde: Turn
ON power domain on shutdown"), which covers regular reboots. A watchdog
reset doesn't go through the shutdown, so it still hangs the system
instead of rebooting it.

Add the vde_always_on module parameter, which keeps the partition
powered on, at the cost of its idle power. Instead of powering the
partition off when registering it, the driver runs the power up
sequence on it, which puts its clocks, resets and clamps into the same
known state without cutting its power, and registers its genpd as
always on. tegra_powergate_set() also doesn't power it off, which
covers the legacy power gating API.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 Documentation/admin-guide/kernel-parameters.txt | 12 ++++++
 drivers/soc/tegra/pmc.c                         | 54 +++++++++++++++++++++++--
 2 files changed, 62 insertions(+), 4 deletions(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 0b3c140babd9..81af8aa1482a 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -7571,6 +7571,18 @@ Kernel parameters
 
 	tdfx=		[HW,DRM]
 
+	tegra_pmc.vde_always_on=
+			[HW,ARM] Never power off the VDE power partition.
+			After a warm reset, which doesn't reset the PMC, the
+			boot ROM of Tegra20 hangs if the partition is off.
+			The VDE driver powers it on when the system shuts
+			down for a regular reboot, but a watchdog reset
+			doesn't go through the shutdown and hangs. Keeping
+			the partition on makes watchdog resets work, at the
+			cost of its idle power.
+			Format: <bool>
+			Default: false
+
 	test_suspend=	[SUSPEND]
 			Format: { "mem" | "standby" | "freeze" }[,N]
 			Specify "mem" (for Suspend-to-RAM) or "standby" (for
diff --git a/drivers/soc/tegra/pmc.c b/drivers/soc/tegra/pmc.c
index 2c70eedf514e..807977f52449 100644
--- a/drivers/soc/tegra/pmc.c
+++ b/drivers/soc/tegra/pmc.c
@@ -522,6 +522,28 @@ static struct tegra_pmc *early_pmc = &(struct tegra_pmc) {
 };
 #endif
 
+#ifdef MODULE_PARAM_PREFIX
+#undef MODULE_PARAM_PREFIX
+#endif
+#define MODULE_PARAM_PREFIX "tegra_pmc."
+
+/*
+ * After a warm reset, which doesn't reset the PMC, Tegra20's boot ROM hangs if
+ * the VDE partition is powered off. The VDE driver powers it on at shutdown,
+ * but a watchdog reset doesn't go through the shutdown and hangs. Keeping the
+ * partition powered on makes watchdog resets work, at the cost of its idle
+ * power.
+ */
+static bool vde_always_on;
+module_param(vde_always_on, bool, 0444);
+MODULE_PARM_DESC(vde_always_on,
+		 "Never power off the VDE partition (default: false)");
+
+static bool tegra_powergate_is_always_on(unsigned int id)
+{
+	return vde_always_on && id == TEGRA_POWERGATE_VDEC;
+}
+
 static inline struct tegra_powergate *
 to_powergate(struct generic_pm_domain *domain)
 {
@@ -697,6 +719,10 @@ static int tegra_powergate_set(struct tegra_pmc *pmc, unsigned int id,
 	if (id == TEGRA_POWERGATE_3D && pmc->soc->has_gpu_clamps)
 		return -EINVAL;
 
+	/* the partition is kept powered, also against the legacy API */
+	if (!new_state && tegra_powergate_is_always_on(id))
+		return 0;
+
 	mutex_lock(&pmc->powergates_lock);
 
 	if (tegra_powergate_state(pmc, id) == new_state) {
@@ -1319,6 +1345,7 @@ static int tegra_powergate_add(struct tegra_pmc *pmc, struct device_node *np)
 	struct device *dev = pmc->dev;
 	struct tegra_powergate *pg;
 	int id, err = 0;
+	bool always_on;
 	bool off;
 
 	pg = kzalloc_obj(*pg);
@@ -1346,6 +1373,7 @@ static int tegra_powergate_add(struct tegra_pmc *pmc, struct device_node *np)
 	pg->pmc = pmc;
 
 	off = !tegra_powergate_is_powered(pmc, pg->id);
+	always_on = tegra_powergate_is_always_on(pg->id);
 
 	err = tegra_powergate_of_get_clks(pg, np);
 	if (err < 0) {
@@ -1363,9 +1391,20 @@ static int tegra_powergate_add(struct tegra_pmc *pmc, struct device_node *np)
 	 * If the power-domain is off, then ensure the resets are asserted.
 	 * If the power-domain is on, then power down to ensure that when is
 	 * it turned on the power-domain, clocks and resets are all in the
-	 * expected state.
+	 * expected state. A power-domain that is kept on is powered up instead,
+	 * which puts it into the same state without powering it off.
 	 */
-	if (off) {
+	if (always_on) {
+		err = tegra_powergate_power_up(pg, true);
+		if (err) {
+			dev_err(dev, "failed to turn on PM domain %s: %d\n",
+				pg->genpd.name, err);
+			goto remove_resets;
+		}
+
+		dev_info(dev, "keeping PM domain %s powered on\n",
+			 pg->genpd.name);
+	} else if (off) {
 		err = reset_control_assert(pg->reset);
 		if (err) {
 			pr_err("failed to assert resets: %d\n", err);
@@ -1385,11 +1424,18 @@ static int tegra_powergate_add(struct tegra_pmc *pmc, struct device_node *np)
 	 * the domain and skip the genpd registration.
 	 */
 	if (!IS_ENABLED(CONFIG_PM_GENERIC_DOMAINS)) {
-		WARN_ON(tegra_powergate_power_up(pg, true));
+		if (!always_on)
+			WARN_ON(tegra_powergate_power_up(pg, true));
 		goto remove_resets;
 	}
 
-	err = pm_genpd_init(&pg->genpd, NULL, true);
+	if (always_on) {
+		/* as tegra_genpd_power_on() does after powering up */
+		reset_control_release(pg->reset);
+		pg->genpd.flags |= GENPD_FLAG_ALWAYS_ON;
+	}
+
+	err = pm_genpd_init(&pg->genpd, NULL, !always_on);
 	if (err < 0) {
 		dev_err(dev, "failed to initialise PM domain %pOFn: %d\n", np,
 		       err);

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 5/6] soc/tegra: pmc: Add a partition power off notifier
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
                   ` (3 preceding siblings ...)
  2026-11-10 12:39 ` [PATCH 4/6] soc/tegra: pmc: Add an option to keep the VDE partition on Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  2026-11-10 12:39 ` [PATCH 6/6] watchdog: tegra_wdt: Stop on Tegra20 VDE power off Artur Kowalski
  5 siblings, 0 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

After a watchdog reset, which doesn't reset the PMC, Tegra20's boot ROM
hangs if the VDE partition is powered off. Unless booted with
tegra_pmc.vde_always_on=1, the PMC driver powers the partition off when
it registers it, and genpd powers it off while the VDE is idle. The
Tegra watchdog driver will need to stop the watchdog before that
happens.

genpd notifiers can't be used for this: they only reach devices attached
to the domain, which the watchdog isn't, and the partitions are powered
off before their genpds are registered. Add a notifier that is called
before a partition is powered off, from tegra_powergate_set(), which all
power off paths go through.

When a notifier is registered, it is also called for every partition that
is already powered off, except one kept powered on with
tegra_pmc.vde_always_on, which the PMC driver powers on when it registers
it. A lock is held across both the notification and the power off, so a
partition can't be powered off unnoticed while a notifier is being
registered. Registration uses the early PMC, so it works before the PMC
driver has probed, which is when the partitions are first powered off.
The notifier, including the hook on the power off path, is therefore only
built on 32-bit ARM, the only architecture with an early PMC.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 drivers/soc/tegra/pmc.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++
 include/soc/tegra/pmc.h |  19 +++++++++
 2 files changed, 123 insertions(+)

diff --git a/drivers/soc/tegra/pmc.c b/drivers/soc/tegra/pmc.c
index 807977f52449..1d9a6aeb0e57 100644
--- a/drivers/soc/tegra/pmc.c
+++ b/drivers/soc/tegra/pmc.c
@@ -31,6 +31,7 @@
 #include <linux/irq.h>
 #include <linux/irq_work.h>
 #include <linux/kernel.h>
+#include <linux/notifier.h>
 #include <linux/of_address.h>
 #include <linux/of_clk.h>
 #include <linux/of.h>
@@ -520,6 +521,34 @@ static struct tegra_pmc *early_pmc = &(struct tegra_pmc) {
 	.base = NULL,
 	.suspend_mode = TEGRA_SUSPEND_NOT_READY,
 };
+
+/*
+ * Notified before a partition is powered off. The lock is held across the
+ * notification and the power off, so that a notifier being registered either
+ * sees the partition still powered or is notified.
+ */
+static DEFINE_MUTEX(tegra_powergate_nh_lock);
+static RAW_NOTIFIER_HEAD(tegra_powergate_nh);
+
+static void tegra_powergate_power_off_begin(unsigned int id)
+{
+	mutex_lock(&tegra_powergate_nh_lock);
+	raw_notifier_call_chain(&tegra_powergate_nh,
+				TEGRA_PMC_POWERGATE_POWER_OFF, &id);
+}
+
+static void tegra_powergate_power_off_end(void)
+{
+	mutex_unlock(&tegra_powergate_nh_lock);
+}
+#else
+static inline void tegra_powergate_power_off_begin(unsigned int id)
+{
+}
+
+static inline void tegra_powergate_power_off_end(void)
+{
+}
 #endif
 
 #ifdef MODULE_PARAM_PREFIX
@@ -730,8 +759,14 @@ static int tegra_powergate_set(struct tegra_pmc *pmc, unsigned int id,
 		return 0;
 	}
 
+	if (!new_state)
+		tegra_powergate_power_off_begin(id);
+
 	err = pmc->soc->powergate_set(pmc, id, new_state);
 
+	if (!new_state)
+		tegra_powergate_power_off_end();
+
 	mutex_unlock(&pmc->powergates_lock);
 
 	return err;
@@ -5166,6 +5201,75 @@ int tegra_pmc_cpu_remove_clamping(unsigned int cpuid)
 	return tegra_pmc_powergate_remove_clamping(early_pmc, id);
 }
 
+/**
+ * tegra_pmc_powergate_register_notifier() - get notified of partition power off
+ * @nb: notifier block
+ *
+ * The notifier is called with %TEGRA_PMC_POWERGATE_POWER_OFF and a pointer to
+ * the partition ID (unsigned int) as data before a partition is powered off,
+ * from a context that may sleep. Its return value is ignored. It is also
+ * called for every partition that is already powered off before this
+ * function returns, so a partition is either still powered once the function
+ * returns or the notifier has been called for it. A partition that is kept
+ * powered on (tegra_pmc.vde_always_on) is never reported, as the driver powers
+ * it on when it registers it.
+ *
+ * This works before the PMC driver has probed, as the PMC is set up early on
+ * 32-bit ARM, so it covers the partitions that the driver powers off when it
+ * registers them.
+ */
+int tegra_pmc_powergate_register_notifier(struct notifier_block *nb)
+{
+	unsigned int id;
+	int err;
+
+	mutex_lock(&tegra_powergate_nh_lock);
+
+	err = raw_notifier_chain_register(&tegra_powergate_nh, nb);
+	if (err)
+		goto unlock;
+
+	/* the base is switched to the driver's mapping under this lock */
+	mutex_lock(&early_pmc->powergates_lock);
+
+	if (early_pmc->base && early_pmc->soc) {
+		for (id = 0; id < early_pmc->soc->num_powergates; id++) {
+			/* powered on when the driver registers it */
+			if (tegra_powergate_is_always_on(id))
+				continue;
+
+			if (tegra_powergate_is_powered(early_pmc, id) == 0)
+				nb->notifier_call(nb,
+						  TEGRA_PMC_POWERGATE_POWER_OFF,
+						  &id);
+		}
+	}
+
+	mutex_unlock(&early_pmc->powergates_lock);
+
+unlock:
+	mutex_unlock(&tegra_powergate_nh_lock);
+
+	return err;
+}
+EXPORT_SYMBOL(tegra_pmc_powergate_register_notifier);
+
+/**
+ * tegra_pmc_powergate_unregister_notifier() - unregister a power off notifier
+ * @nb: notifier block registered with tegra_pmc_powergate_register_notifier()
+ */
+int tegra_pmc_powergate_unregister_notifier(struct notifier_block *nb)
+{
+	int err;
+
+	mutex_lock(&tegra_powergate_nh_lock);
+	err = raw_notifier_chain_unregister(&tegra_powergate_nh, nb);
+	mutex_unlock(&tegra_powergate_nh_lock);
+
+	return err;
+}
+EXPORT_SYMBOL(tegra_pmc_powergate_unregister_notifier);
+
 bool tegra_pmc_core_domain_state_synced(void)
 {
 	return early_pmc->core_domain_state_synced;
diff --git a/include/soc/tegra/pmc.h b/include/soc/tegra/pmc.h
index 53f6d02889db..c574a6e81a1a 100644
--- a/include/soc/tegra/pmc.h
+++ b/include/soc/tegra/pmc.h
@@ -231,14 +231,33 @@ static inline void tegra_pmc_enter_suspend_mode(enum tegra_suspend_mode mode)
 #endif
 #endif
 
+/* event of the partition power off notifier, data points to the partition ID */
+#define TEGRA_PMC_POWERGATE_POWER_OFF	0
+
+struct notifier_block;
+
 #if defined(CONFIG_ARM) && defined(CONFIG_SOC_TEGRA_PMC)
 bool tegra_pmc_core_domain_state_synced(void);
+int tegra_pmc_powergate_register_notifier(struct notifier_block *nb);
+int tegra_pmc_powergate_unregister_notifier(struct notifier_block *nb);
 #else
 /* needed for COMPILE_TEST */
 static inline bool tegra_pmc_core_domain_state_synced(void)
 {
 	return false;
 }
+
+static inline int
+tegra_pmc_powergate_register_notifier(struct notifier_block *nb)
+{
+	return -ENOSYS;
+}
+
+static inline int
+tegra_pmc_powergate_unregister_notifier(struct notifier_block *nb)
+{
+	return -ENOSYS;
+}
 #endif
 
 #endif /* __SOC_TEGRA_PMC_H__ */

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 6/6] watchdog: tegra_wdt: Stop on Tegra20 VDE power off
  2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
                   ` (4 preceding siblings ...)
  2026-11-10 12:39 ` [PATCH 5/6] soc/tegra: pmc: Add a partition power off notifier Artur Kowalski
@ 2026-11-10 12:39 ` Artur Kowalski
  5 siblings, 0 replies; 8+ messages in thread
From: Artur Kowalski @ 2026-11-10 12:39 UTC (permalink / raw)
  To: Daniel Lezcano, Thomas Gleixner, Thierry Reding, Jonathan Hunter,
	Wim Van Sebroeck, Guenter Roeck, Jonathan Corbet, Shuah Khan,
	Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95,
	Artur Kowalski

A watchdog reset doesn't reset the PMC, and Tegra20's boot ROM hangs
after it if the VDE power partition is off. Unless booted with
tegra_pmc.vde_always_on=1, the PMC driver powers the partition off when
it registers it, and genpd powers it off whenever the VDE is idle. A
watchdog started by the bootloader can still catch hangs early during
boot, before that happens.

Register a PMC partition power off notifier to stop the watchdog, and
the watchdog core's pings, before the VDE partition is powered off for
the first time. If the partition is already off, a watchdog left
running by the bootloader is stopped instead of taken over. Afterwards
the watchdog refuses to start with -EBUSY, as a reset would hang, even
if the partition is powered on again. If the notifier can't be
registered, the driver warns and keeps the watchdog running.

Stopping is the default. With tegra_pmc.vde_always_on=1, the partition
is never powered off, so the watchdog stays usable. Booting with
tegra_wdt.stop_on_vde_off=0 also keeps it usable, but at the risk of the
system hanging instead of rebooting.

The watchdog operations and the notifier share a mutex, as the notifier
runs outside of the watchdog core's locking.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Artur Kowalski <arturkow2000@gmail.com>
---
 Documentation/watchdog/watchdog-parameters.rst |   5 +
 drivers/watchdog/tegra_wdt.c                   | 143 +++++++++++++++++++++++--
 2 files changed, 137 insertions(+), 11 deletions(-)

diff --git a/Documentation/watchdog/watchdog-parameters.rst b/Documentation/watchdog/watchdog-parameters.rst
index 348229ffcb43..3f7a1831c5cb 100644
--- a/Documentation/watchdog/watchdog-parameters.rst
+++ b/Documentation/watchdog/watchdog-parameters.rst
@@ -600,6 +600,11 @@ tegra_wdt:
     nowayout:
 	Watchdog cannot be stopped once started
 	(default=kernel config parameter)
+    stop_on_vde_off:
+	Tegra20: stop the watchdog for good when the VDE partition is
+	powered off, as a reset would hang the boot ROM. Booting with
+	tegra_pmc.vde_always_on=1 keeps the partition, and with it the
+	watchdog, usable (default=1)
 
 -------------------------------------------------
 
diff --git a/drivers/watchdog/tegra_wdt.c b/drivers/watchdog/tegra_wdt.c
index 7c2c21f90de5..d36310c50f78 100644
--- a/drivers/watchdog/tegra_wdt.c
+++ b/drivers/watchdog/tegra_wdt.c
@@ -7,6 +7,8 @@
 #include <linux/module.h>
 #include <linux/interrupt.h>
 #include <linux/io.h>
+#include <linux/mutex.h>
+#include <linux/notifier.h>
 #include <linux/of.h>
 #include <linux/of_address.h>
 #include <linux/platform_device.h>
@@ -14,6 +16,8 @@
 #include <linux/stringify.h>
 #include <linux/watchdog.h>
 
+#include <soc/tegra/pmc.h>
+
 /* minimum and maximum watchdog trigger timeout, in seconds */
 #define MIN_WDT_TIMEOUT			1
 #define MAX_WDT_TIMEOUT			255
@@ -86,7 +90,12 @@ struct tegra_wdt {
 	struct watchdog_device	wdd;
 	void __iomem		*wdt_regs;
 	void __iomem		*tmr_regs;
-	void __iomem		*rst_source;	/* Tegra20 only */
+
+	/* Tegra20 only */
+	void __iomem		*rst_source;
+	struct notifier_block	powergate_nb;
+	struct mutex		lock;		/* hardware and vde_off */
+	bool			vde_off;
 };
 
 struct tegra_wdt_soc {
@@ -109,6 +118,21 @@ MODULE_PARM_DESC(nowayout,
 	"Watchdog cannot be stopped once started (default="
 	__stringify(WATCHDOG_NOWAYOUT) ")");
 
+/*
+ * After a watchdog reset, Tegra20's boot ROM hangs if the VDE partition is
+ * powered off, which the PMC driver does when it registers the partition,
+ * unless booted with tegra_pmc.vde_always_on=1. By default, the watchdog is
+ * therefore only used until the partition is powered off for the first time,
+ * which still allows a watchdog started by the bootloader to catch hangs early
+ * during boot. tegra_pmc.vde_always_on=1 keeps the watchdog usable for good.
+ * Clearing this keeps it usable as well, but at the risk of the system
+ * hanging instead of rebooting.
+ */
+static bool stop_on_vde_off = true;
+module_param(stop_on_vde_off, bool, 0);
+MODULE_PARM_DESC(stop_on_vde_off,
+	"Tegra20: stop the watchdog for good when the VDE partition is powered off (default=1)");
+
 static int tegra_wdt_start(struct watchdog_device *wdd)
 {
 	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
@@ -222,10 +246,9 @@ static int tegra30_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
 	return 0;
 }
 
-static int tegra20_wdt_start(struct watchdog_device *wdd)
+static void __tegra20_wdt_start(struct tegra_wdt *wdt)
 {
-	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
-	u32 period = wdd->timeout * USEC_PER_SEC / 2;
+	u32 period = wdt->wdd.timeout * USEC_PER_SEC / 2;
 	u32 val;
 
 	writel(TIMER_EN | TIMER_PERIODIC | (period - 1),
@@ -241,13 +264,10 @@ static int tegra20_wdt_start(struct watchdog_device *wdd)
 	val &= ~T20_RST_SOURCE_WDT_MASK;
 	val |= T20_RST_SOURCE_WDT_EN | T20_RST_SOURCE_WDT_SYS_RST_EN;
 	writel(val, wdt->rst_source);
-
-	return 0;
 }
 
-static int tegra20_wdt_stop(struct watchdog_device *wdd)
+static void __tegra20_wdt_stop(struct tegra_wdt *wdt)
 {
-	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
 	u32 val;
 
 	val = readl(wdt->rst_source);
@@ -256,6 +276,35 @@ static int tegra20_wdt_stop(struct watchdog_device *wdd)
 
 	writel(0, wdt->tmr_regs + TIMER_PTV);
 	writel(TIMER_PCR_INTR_CLR, wdt->tmr_regs + TIMER_PCR);
+}
+
+static int tegra20_wdt_start(struct watchdog_device *wdd)
+{
+	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
+	int ret = 0;
+
+	mutex_lock(&wdt->lock);
+
+	if (wdt->vde_off) {
+		dev_warn(wdd->parent,
+			 "not starting, a reset would hang with VDE powered off\n");
+		ret = -EBUSY;
+	} else {
+		__tegra20_wdt_start(wdt);
+	}
+
+	mutex_unlock(&wdt->lock);
+
+	return ret;
+}
+
+static int tegra20_wdt_stop(struct watchdog_device *wdd)
+{
+	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
+
+	mutex_lock(&wdt->lock);
+	__tegra20_wdt_stop(wdt);
+	mutex_unlock(&wdt->lock);
 
 	return 0;
 }
@@ -264,15 +313,20 @@ static int tegra20_wdt_ping(struct watchdog_device *wdd)
 {
 	struct tegra_wdt *wdt = watchdog_get_drvdata(wdd);
 
+	mutex_lock(&wdt->lock);
+
 	/*
 	 * Clearing the interrupt alone doesn't restart the timer's period, so
 	 * the reset could follow as soon as half the timeout after the ping.
 	 * Rewriting the timer setting restarts the period. The interrupt is
-	 * cleared afterwards, so one fired in between doesn't count.
+	 * cleared afterwards, so one fired in between doesn't count. While
+	 * stopped, the timer setting reads back as 0, so this doesn't start it.
 	 */
 	writel(readl(wdt->tmr_regs + TIMER_PTV), wdt->tmr_regs + TIMER_PTV);
 	writel(TIMER_PCR_INTR_CLR, wdt->tmr_regs + TIMER_PCR);
 
+	mutex_unlock(&wdt->lock);
+
 	return 0;
 }
 
@@ -296,6 +350,45 @@ static const struct watchdog_ops tegra20_wdt_ops = {
 	.set_timeout = tegra20_wdt_set_timeout,
 };
 
+static int tegra20_wdt_powergate_notify(struct notifier_block *nb,
+					unsigned long event, void *data)
+{
+	struct tegra_wdt *wdt = container_of(nb, struct tegra_wdt,
+					     powergate_nb);
+	unsigned int id = *(unsigned int *)data;
+
+	if (event != TEGRA_PMC_POWERGATE_POWER_OFF ||
+	    id != TEGRA_POWERGATE_VDEC)
+		return NOTIFY_DONE;
+
+	mutex_lock(&wdt->lock);
+
+	if (!wdt->vde_off) {
+		wdt->vde_off = true;
+		__tegra20_wdt_stop(wdt);
+
+		/*
+		 * Stop the core's pings. If user space has the device open,
+		 * its pings don't restart the watchdog either.
+		 */
+		clear_bit(WDOG_HW_RUNNING, &wdt->wdd.status);
+
+		dev_info(wdt->wdd.parent,
+			 "VDE powered off, watchdog stopped\n");
+	}
+
+	mutex_unlock(&wdt->lock);
+
+	return NOTIFY_OK;
+}
+
+static void tegra20_wdt_unregister_notifier(void *data)
+{
+	struct tegra_wdt *wdt = data;
+
+	tegra_pmc_powergate_unregister_notifier(&wdt->powergate_nb);
+}
+
 static int tegra20_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
 			    void __iomem *regs)
 {
@@ -326,18 +419,46 @@ static int tegra20_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
 
 	wdt->tmr_regs = regs + T20_WDT_TIMER_BASE;
 
+	ret = devm_mutex_init(dev, &wdt->lock);
+	if (ret)
+		return ret;
+
+	/*
+	 * If the VDE partition is already powered off, the notifier is called
+	 * right away and stops a watchdog left running by the bootloader.
+	 */
+	if (stop_on_vde_off) {
+		wdt->powergate_nb.notifier_call = tegra20_wdt_powergate_notify;
+
+		ret = tegra_pmc_powergate_register_notifier(&wdt->powergate_nb);
+		if (ret) {
+			dev_warn(dev, "failed to register VDE notifier: %d\n",
+				 ret);
+		} else {
+			ret = devm_add_action_or_reset(dev,
+					tegra20_wdt_unregister_notifier, wdt);
+			if (ret)
+				return ret;
+		}
+	}
+
 	/*
 	 * The bootloader may leave the watchdog running. Take it over with
 	 * our own timeout and timer 1, and let the core ping it until user
 	 * space opens the device.
 	 */
-	if (readl(wdt->rst_source) & T20_RST_SOURCE_WDT_EN) {
+	mutex_lock(&wdt->lock);
+
+	if (!wdt->vde_off &&
+	    (readl(wdt->rst_source) & T20_RST_SOURCE_WDT_EN)) {
 		dev_info(dev, "watchdog left running by the bootloader, taking it over (timeout %u sec)\n",
 			 wdt->wdd.timeout);
-		tegra20_wdt_start(&wdt->wdd);
+		__tegra20_wdt_start(wdt);
 		set_bit(WDOG_HW_RUNNING, &wdt->wdd.status);
 	}
 
+	mutex_unlock(&wdt->lock);
+
 	return 0;
 }
 

-- 
2.56.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20
  2026-11-10 12:39 ` [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20 Artur Kowalski
@ 2026-11-10 16:21   ` Krzysztof Kozlowski
  0 siblings, 0 replies; 8+ messages in thread
From: Krzysztof Kozlowski @ 2026-11-10 16:21 UTC (permalink / raw)
  To: Artur Kowalski, Daniel Lezcano, Thomas Gleixner, Thierry Reding,
	Jonathan Hunter, Wim Van Sebroeck, Guenter Roeck,
	Jonathan Corbet, Shuah Khan, Randy Dunlap, Philipp Zabel
  Cc: linux-kernel, linux-tegra, linux-watchdog, linux-doc, clamor95

On 10/11/2026 13:39, Artur Kowalski wrote:
> +static int tegra20_wdt_init(struct platform_device *pdev, struct tegra_wdt *wdt,
> +			    void __iomem *regs)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct device_node *np;
> +	struct resource res;
> +	int ret;
> +
> +	/*
> +	 * RST_SOURCE belongs to the clock and reset controller, whose driver
> +	 * doesn't use it. Map only this register, without requesting it, as
> +	 * the region belongs to the clock and reset controller.
> +	 */
> +	np = of_find_compatible_node(NULL, NULL, "nvidia,tegra20-car");
> +	if (!np)
> +		return dev_err_probe(dev, -ENODEV,
> +				     "clock and reset controller not found\n");
> +
> +	ret = of_address_to_resource(np, 0, &res);
> +	of_node_put(np);
> +	if (ret)

Looks like doing syscon here. This in general is more fragile.

...

> +/*
> + * Tegra30 and later list "nvidia,tegra20-timer" as well, but match their more
> + * specific compatible string.
> + */
>  static const struct of_device_id tegra_wdt_of_match[] = {
>  	{ .compatible = "nvidia,tegra30-timer", .data = &tegra30_wdt_soc },
> +	{ .compatible = "nvidia,tegra20-timer", .data = &tegra20_wdt_soc },

Why in reversed order?

>  	{ },
>  };
>  MODULE_DEVICE_TABLE(of, tegra_wdt_of_match);
> 


Best regards,
Krzysztof

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-11-10 16:21 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-10 12:39 [PATCH 0/6] watchdog: tegra_wdt: Add Tegra20 support Artur Kowalski
2026-11-10 12:39 ` [PATCH 1/6] clocksource/drivers/timer-tegra: Keep Tegra20's watchdog timers free Artur Kowalski
2026-11-10 12:39 ` [PATCH 2/6] watchdog: tegra_wdt: Move SoC specific parts into match data Artur Kowalski
2026-11-10 12:39 ` [PATCH 3/6] watchdog: tegra_wdt: Support Tegra20 Artur Kowalski
2026-11-10 16:21   ` Krzysztof Kozlowski
2026-11-10 12:39 ` [PATCH 4/6] soc/tegra: pmc: Add an option to keep the VDE partition on Artur Kowalski
2026-11-10 12:39 ` [PATCH 5/6] soc/tegra: pmc: Add a partition power off notifier Artur Kowalski
2026-11-10 12:39 ` [PATCH 6/6] watchdog: tegra_wdt: Stop on Tegra20 VDE power off Artur Kowalski

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®