2026-01-08
Reading a Linux Kernel Watchdog Driver: s3c2410_wdt
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:
- Divide the source clock by 128 (fixed division).
- Compute the required ticks as
timeout * freq. - If that exceeds the counter width (
max_cnt), divide further with the prescaler so it fits in the counter. - Back-compute
wdd->timeoutfrom 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
nowayoutmodule 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.candDocumentation/watchdog/