</>PatchNote
Back to posts

2026-01-08

Reading a Linux Kernel Watchdog Driver: s3c2410_wdt

Linux KernelDevice DriverEmbedded

During my Samsung Electronics DS AP S/W internship I worked on BSP kernel drivers for embedded SoCs. The code I touched there can't be published, so instead I walk through a mainline Linux watchdog driver in the same family: drivers/watchdog/s3c2410_wdt.c, the Samsung SoC watchdog driver used from the S3C2410 up to recent Exynos parts. (All the code below is from the public mainline kernel source.)

What a watchdog does

A watchdog timer is hardware that resets the system unless you "kick" it within a set interval. If the kernel or a userspace process hangs and can't keep kicking it, the timer expires and force-reboots the SoC to recover. It's an essential safety net in embedded systems.

Linux abstracts this into a watchdog core (drivers/watchdog/watchdog_core.c, watchdog_dev.c). An individual SoC driver just fills in a struct watchdog_device and a struct watchdog_ops, registers with watchdog_register_device(), and the core provides the /dev/watchdog character device and the ioctl interface.

Hardware that fits in three registers

The s3c2410 watchdog's hardware interface is strikingly simple.

#define S3C2410_WTCON    0x00   // control
#define S3C2410_WTDAT    0x04   // reload count (value refilled on expiry)
#define S3C2410_WTCNT    0x08   // current count (the counter that actually decrements)
#define S3C2410_WTCLRINT 0x0c   // interrupt clear (newer SoCs)
  • WTDAT: the value to reload when the counter hits zero.
  • WTCNT: the live decrementing counter. Reaching zero triggers a reset/interrupt.
  • WTCON: the control register holding enable / reset-enable / clock divisors.

WTCON's bit definitions make it clear at a glance what the driver controls.

#define S3C2410_WTCON_RSTEN   BIT(0)   // reset the SoC on expiry
#define S3C2410_WTCON_INTEN   BIT(2)   // raise an interrupt on expiry
#define S3C2410_WTCON_ENABLE  BIT(5)   // run the watchdog
#define S3C2410_WTCON_DIV16   (0 << 3) // clock division (16/32/64/128)
#define S3C2410_WTCON_DIV128  (3 << 3)
#define S3C2410_WTCON_PRESCALE(x)  ((x) << 8)  // 8-bit prescaler

So the watchdog clock = PCLK / (prescaler+1) / division. Those two divider stages are how you dial in a timeout.

start / stop / keepalive

Here are the three core callbacks registered in watchdog_ops.

start — set the counter and turn the watchdog on.

static int s3c2410wdt_start(struct watchdog_device *wdd)
{
	unsigned long wtcon;
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	__s3c2410wdt_stop(wdt);                 // stop first, start from a clean state

	wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128;

	if (soft_noboot) {                      // debug: interrupt only, no reset
		wtcon |= S3C2410_WTCON_INTEN;
		wtcon &= ~S3C2410_WTCON_RSTEN;
	} else {                                // real: reset the SoC on expiry
		wtcon &= ~S3C2410_WTCON_INTEN;
		wtcon |= S3C2410_WTCON_RSTEN;
	}

	writel(wdt->count, wdt->reg_base + S3C2410_WTDAT);
	writel(wdt->count, wdt->reg_base + S3C2410_WTCNT);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);
	spin_unlock_irqrestore(&wdt->lock, flags);
	return 0;
}

The soft_noboot module parameter is a nice touch. During development you can't have the watchdog actually reboot the board — debugging would be impossible — so it sets INTEN instead of RSTEN, receiving an interrupt rather than a reset.

stop — just clear the ENABLE and RSTEN bits.

static void __s3c2410wdt_stop(struct s3c2410_wdt *wdt)
{
	unsigned long wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);
}

keepalive (ping) — the "kick." Rewrite the reload value into WTCNT to reset the counter.

static int s3c2410wdt_keepalive(struct watchdog_device *wdd)
{
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long flags;

	spin_lock_irqsave(&wdt->lock, flags);
	writel(wdt->count, wdt->reg_base + S3C2410_WTCNT);   // this one line is the "kick"
	spin_unlock_irqrestore(&wdt->lock, flags);
	return 0;
}

The essence of a watchdog lives in that single line. When userspace periodically writes to /dev/watchdog, the core calls this ping to refill WTCNT. Stop writing and the counter drains to zero, triggering a reset.

Translating a timeout into register values

set_heartbeat translates the user's "how many seconds" into the hardware's prescaler/count.

static int s3c2410wdt_set_heartbeat(struct watchdog_device *wdd,
				    unsigned int timeout)
{
	struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd);
	unsigned long freq = s3c2410wdt_get_freq(wdt);
	unsigned long count;
	unsigned int divisor = 1;
	unsigned long wtcon;

	if (timeout < 1)
		return -EINVAL;

	freq = DIV_ROUND_UP(freq, 128);        // account for the fixed DIV128
	count = timeout * freq;                // total ticks needed

	if (count > wdt->max_cnt) {             // beyond counter width? divide more via prescaler
		divisor = DIV_ROUND_UP(count, wdt->max_cnt);
		if (divisor > S3C2410_WTCON_PRESCALE_MAX + 1) {
			dev_err(wdt->dev, "timeout %d too big\n", timeout);
			return -EINVAL;
		}
	}

	count = DIV_ROUND_UP(count, divisor);
	wdt->count = count;

	wtcon = readl(wdt->reg_base + S3C2410_WTCON);
	wtcon &= ~S3C2410_WTCON_PRESCALE_MASK;
	wtcon |= S3C2410_WTCON_PRESCALE(divisor - 1);

	writel(count, wdt->reg_base + S3C2410_WTDAT);
	writel(wtcon, wdt->reg_base + S3C2410_WTCON);

	wdd->timeout = (count * divisor) / freq;  // back-compute the timeout actually applied
	return 0;
}

The core logic:

  1. Divide the source clock by 128 (fixed division).
  2. Compute the required ticks as timeout * freq.
  3. If that exceeds the counter width (max_cnt), divide further with the prescaler so it fits in the counter.
  4. Back-compute wdd->timeout from what actually got programmed, so the value reported to the user is honest.

Where does freq come from?

static inline unsigned long s3c2410wdt_get_freq(struct s3c2410_wdt *wdt)
{
	return clk_get_rate(wdt->src_clk ? wdt->src_clk : wdt->bus_clk);
}

Not hardcoded — it reads the actual operating frequency from the clock framework. PCLK differs per board, so this is what makes the driver portable.

Where it meets the core: watchdog_ops

The interface the driver fills in comes down to this struct.

static const struct watchdog_ops s3c2410wdt_ops = {
	.owner       = THIS_MODULE,
	.start       = s3c2410wdt_start,
	.stop        = s3c2410wdt_stop,
	.ping        = s3c2410wdt_keepalive,
	.set_timeout = s3c2410wdt_set_heartbeat,
	.restart     = s3c2410wdt_restart,
};

#define OPTIONS (WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE)
  • WDIOF_MAGICCLOSE — if the user just closes /dev/watchdog, the watchdog must not stop. To stop it, they must write the magic character 'V' before closing. This keeps the watchdog running even if a process dies and its fd is closed by accident.
  • If the nowayout module parameter is set, a watchdog once started can never be stopped — foreclosing the accident of the watchdog being switched off in production.

restart is the callback that performs a deliberate system reset via the watchdog: set the timeout to a minimum and let it expire, reusing the SoC's reset path to reboot.

Exynos: PMU and quirks

This is where real SoC integration gets messy. On newer Exynos parts, the watchdog reset goes through the PMU (Power Management Unit). So the driver absorbs per-SoC differences with a quirks flag set and a variant struct.

#define QUIRK_HAS_WTCLRINT_REG     BIT(0)
#define QUIRK_HAS_PMU_MASK_RESET   BIT(1)
#define QUIRK_HAS_PMU_RST_STAT     BIT(2)
#define QUIRK_HAS_PMU_AUTO_DISABLE BIT(3)
#define QUIRK_HAS_PMU_CNT_EN       BIT(4)

struct s3c2410_wdt_variant {
	int disable_reg;
	int mask_reset_reg;
	bool mask_reset_inv;
	int mask_bit;
	int rst_stat_reg;
	int rst_stat_bit;
	int cnt_en_reg;
	int cnt_en_bit;
	u32 quirks;
};

PMU register access is abstracted through regmap (syscon). For example, the function that masks/unmasks the watchdog reset:

static int s3c2410wdt_mask_wdt_reset(struct s3c2410_wdt *wdt, bool mask)
{
	const u32 mask_val = BIT(wdt->drv_data->mask_bit);
	const bool val_inv = wdt->drv_data->mask_reset_inv;
	const u32 val = (mask ^ val_inv) ? mask_val : 0;

	return regmap_update_bits(wdt->pmureg, wdt->drv_data->mask_reset_reg,
				  mask_val, val);
}

The reason a field like mask_reset_inv exists is delightful. Some SoCs mask by writing 1, others mask by writing 0 — opposite polarity. The (mask ^ val_inv) XOR absorbs that polarity inversion in one line. For a single driver to support many SoC generations, this kind of pulling the fine differences out into data is essential.

IRQ handler

Handling the interrupt that arrives under soft_noboot or a pretimeout is simple.

static irqreturn_t s3c2410wdt_irq(int irqno, void *param)
{
	struct s3c2410_wdt *wdt = platform_get_drvdata(param);

	dev_info(wdt->dev, "watchdog timer expired (irq)\n");
	s3c2410wdt_keepalive(&wdt->wdt_device);   // kick to defer the reset for now

	if (wdt->drv_data->quirks & QUIRK_HAS_WTCLRINT_REG)
		writel(0x1, wdt->reg_base + S3C2410_WTCLRINT);  // newer SoCs need an interrupt clear

	return IRQ_HANDLED;
}

Takeaways from this driver

  • Hardware abstraction layer: fill in only the thin interface the core defines (watchdog_ops), and policy like /dev/watchdog, ioctl, nowayout, and magic-close is all handled by the core. The driver only knows registers.
  • Separation of policy and mechanism: "when to kick" belongs to userspace, "how to kick" to the driver, and "which safety policy to enforce" to the core.
  • The variant/quirk pattern: generational SoC differences are absorbed as data structures rather than code branches, so one driver covers everything from S3C2410 to Exynos — handling even polarity inversion in a single field.

The code I worked on during the internship was a SoC-specific BSP sharing a lineage with this public driver. I can't disclose the specifics, but the instinct of "touch registers, but strictly within the subsystem contract the kernel defines" came straight from this structure.

Source: mainline drivers/watchdog/s3c2410_wdt.c and Documentation/watchdog/