Watchdog Timer in Verilog: Timeout & Kick Logic with Code

A watchdog timer is a free-running counter that forces a reset or raises an alarm when software (or another subsystem) fails to service it in time. The firmware must periodically kick the watchdog; if a kick does not arrive before the counter reaches its programmed interval, the timeout output fires. This page gives runnable, simulator-verified watchdog timer Verilog code for both a classic one-shot timer and a windowed variant.

Both modules below use an active-low asynchronous reset and a synchronous kick. Each example ships with a self-checking testbench that compares the design against an independent golden model, so the simulation log you see is real output from Icarus Verilog, not a hand-written transcript.

This is the clearest form of a watchdog timer in Verilog: a single counter that increments every cycle, reloads to zero on a kick, and asserts timeout once TIMEOUT cycles pass with no kick. The TIMEOUT parameter makes the interval reusable across clock domains.

// Parameterized watchdog timer
// Counts clock cycles since the last 'kick'. When the count reaches TIMEOUT
// (i.e. TIMEOUT cycles have elapsed with no kick), 'timeout' is asserted.
// A kick (or reset) reloads the counter to 0 and clears the alarm.
module watchdog #(
  parameter integer TIMEOUT = 8
)(
  input  wire clk,
  input  wire rst_n,   // active-low asynchronous reset
  input  wire kick,    // pulse high to pet the dog
  output wire timeout  // high once TIMEOUT cycles pass with no kick
);
  reg [31:0] count;
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      count <= 32'd0;
    else if (kick)
      count <= 32'd0;          // kick has priority: reload
    else if (count != TIMEOUT)
      count <= count + 32'd1;  // saturate at TIMEOUT
  end
  assign timeout = (count == TIMEOUT);
endmodule

module tb;
  localparam integer TIMEOUT = 6;
  reg clk, rst_n, kick;
  wire timeout;
  integer errors;
  integer exp_count;   // independent golden model (plain integer)
  integer i;

  watchdog #(.TIMEOUT(TIMEOUT)) dut (
    .clk(clk), .rst_n(rst_n), .kick(kick), .timeout(timeout)
  );

  // clock
  initial clk = 1'b0;
  always #5 clk = ~clk;

  // Independent golden reference, updated on the same clock edge as the DUT
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)            exp_count = 0;
    else if (kick)         exp_count = 0;
    else if (exp_count != TIMEOUT) exp_count = exp_count + 1;
  end

  // Check DUT against the golden model one time-step after each posedge
  task check;
    begin
      if (timeout !== (exp_count == TIMEOUT)) begin
        errors = errors + 1;
        $display("FAIL: exp_count=%0d dut_timeout=%b expected_timeout=%b",
                 exp_count, timeout, (exp_count == TIMEOUT));
      end
    end
  endtask

  initial begin
    errors = 0;
    kick   = 1'b0;
    rst_n  = 1'b0;
    // hold reset for two cycles
    @(posedge clk); @(posedge clk);
    #1 check;
    rst_n = 1'b1;

    // ---- Case (a): kicking before timeout keeps 'timeout' low ----
    // Pattern: two idle cycles then a kick, repeated. With TIMEOUT=6 the
    // count never climbs past 2, so the alarm must never fire.
    for (i = 0; i < 18; i = i + 1) begin
      @(negedge clk);
      kick = (i % 3 == 2) ? 1'b1 : 1'b0;  // kick every 3rd cycle
      @(posedge clk);
      #1 check;
      if (timeout !== 1'b0) begin
        errors = errors + 1;
        $display("FAIL: timeout asserted while being kicked (i=%0d)", i);
      end
    end
    if (errors == 0) $display("PASS: periodic kick holds timeout low for 18 cycles");

    // ---- Case (b): withholding the kick asserts timeout at cycle TIMEOUT ----
    @(negedge clk); kick = 1'b0;   // last kick is now in the past; count resets below
    @(negedge clk); kick = 1'b1;   // one clean kick -> count reloads to 0 on next posedge
    @(posedge clk); #1 check;      // count becomes 0 here
    @(negedge clk); kick = 1'b0;   // stop kicking from now on
    // Now count increments each posedge. timeout must be 0 for the first
    // TIMEOUT-1 cycles and exactly 1 on the TIMEOUT-th cycle.
    for (i = 1; i <= TIMEOUT; i = i + 1) begin
      @(posedge clk); #1 check;
      if (i < TIMEOUT && timeout !== 1'b0) begin
        errors = errors + 1;
        $display("FAIL: early timeout at cycle %0d", i);
      end
      if (i == TIMEOUT && timeout !== 1'b1) begin
        errors = errors + 1;
        $display("FAIL: timeout did not assert at cycle %0d", i);
      end
    end
    if (errors == 0)
      $display("PASS: timeout fires exactly %0d cycles after the last kick", TIMEOUT);

    // ---- Case (c): a kick clears an already-asserted alarm ----
    @(negedge clk); kick = 1'b1;
    @(posedge clk); #1 check;
    if (timeout !== 1'b0) begin
      errors = errors + 1;
      $display("FAIL: kick did not clear the alarm");
    end else begin
      $display("PASS: kick clears an asserted timeout");
    end

    if (errors == 0) $display("ALL TESTS PASSED");
    else             $display("TESTS FAILED: %0d error(s)", errors);
    $finish;
  end
endmodule
PASS: periodic kick holds timeout low for 18 cycles
PASS: timeout fires exactly 6 cycles after the last kick
PASS: kick clears an asserted timeout
ALL TESTS PASSED

The log shows three scenarios. First, a kick every third cycle keeps the counter below the threshold, so timeout stays low for 18 cycles. Second, once the kicks stop, timeout asserts on exactly the TIMEOUT-th cycle after the last kick, confirmed by cycle counting against an independent integer reference. Third, a fresh kick clears the alarm. Any off-by-one error or missed reload would print a FAIL line instead of PASS.

A windowed watchdog (WWDT) is stricter: the kick is only accepted inside an open window [OPEN, TIMEOUT-1]. Kicking too early (while the window is closed) is treated as a fault, just like kicking too late. This catches a stuck or runaway task that pets the dog at the wrong rate, not merely one that stops entirely.

// Windowed watchdog timer (WWDT).
// The counter increments every cycle. A kick is only valid inside the OPEN
// window [OPEN, TIMEOUT]: a kick that arrives too early (count < OPEN) is a
// fault, and letting the count reach TIMEOUT with no kick is also a fault.
// 'fault' is sticky and stays asserted until an asynchronous reset.
module wwdt #(
  parameter integer OPEN    = 4,   // window opens when count reaches OPEN
  parameter integer TIMEOUT = 8    // latest allowed kick / alarm point
)(
  input  wire clk,
  input  wire rst_n,
  input  wire kick,
  output reg  fault
);
  reg [31:0] count;
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      count <= 32'd0;
      fault <= 1'b0;
    end else if (fault) begin
      // latched: once faulted, stay faulted until reset
      count <= count;
      fault <= 1'b1;
    end else if (kick) begin
      if (count < OPEN)
        fault <= 1'b1;        // kicked during the closed (early) window
      else
        count <= 32'd0;       // valid kick inside the open window
    end else if (count == TIMEOUT-1) begin
      fault <= 1'b1;          // too late: deadline reached with no kick
    end else begin
      count <= count + 32'd1;
    end
  end
endmodule

module tb;
  localparam integer OPEN    = 4;
  localparam integer TIMEOUT = 8;
  reg clk, rst_n, kick;
  wire fault;
  integer errors;
  integer i;

  // independent golden model (plain integers)
  integer g_count;
  integer g_fault;

  wwdt #(.OPEN(OPEN), .TIMEOUT(TIMEOUT)) dut (
    .clk(clk), .rst_n(rst_n), .kick(kick), .fault(fault)
  );

  initial clk = 1'b0;
  always #5 clk = ~clk;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      g_count = 0; g_fault = 0;
    end else if (g_fault) begin
      g_fault = 1;
    end else if (kick) begin
      if (g_count < OPEN) g_fault = 1;
      else                g_count = 0;
    end else if (g_count == TIMEOUT-1) begin
      g_fault = 1;
    end else begin
      g_count = g_count + 1;
    end
  end

  task check;
    begin
      if (fault !== (g_fault ? 1'b1 : 1'b0)) begin
        errors = errors + 1;
        $display("FAIL: dut_fault=%b golden_fault=%0d (g_count=%0d)",
                 fault, g_fault, g_count);
      end
    end
  endtask

  task do_reset;
    begin
      rst_n = 1'b0; kick = 1'b0;
      @(posedge clk); @(posedge clk);
      #1 check;
      @(negedge clk); rst_n = 1'b1;
    end
  endtask

  initial begin
    errors = 0;

    // ---- Case (a): a valid kick inside the window keeps fault low ----
    do_reset;
    // let count climb into the open window, then kick each time at count==5
    for (i = 0; i < 20; i = i + 1) begin
      @(negedge clk);
      // kick once the count has reached a value safely inside [OPEN,TIMEOUT]
      kick = (dut.count == 5) ? 1'b1 : 1'b0;
      @(posedge clk); #1 check;
    end
    if (errors == 0) $display("PASS: valid in-window kicks keep fault deasserted");

    // ---- Case (b): kicking too early raises a fault ----
    do_reset;
    @(negedge clk); kick = 1'b0;
    @(posedge clk); #1 check;   // count = 1
    @(posedge clk); #1 check;   // count = 2  (still < OPEN=4)
    @(negedge clk); kick = 1'b1;   // early kick at count=2
    @(posedge clk); #1 check;
    @(negedge clk); kick = 1'b0;
    @(posedge clk); #1 check;
    if (fault !== 1'b1) begin
      errors = errors + 1;
      $display("FAIL: early kick did not raise fault");
    end else begin
      $display("PASS: early kick (count<OPEN) raises a fault");
    end

    // ---- Case (c): no kick -> timeout fault exactly at count==TIMEOUT ----
    do_reset;
    kick = 1'b0;
    // count goes 1,2,...; fault must stay low until count reaches TIMEOUT
    for (i = 1; i <= TIMEOUT; i = i + 1) begin
      @(posedge clk); #1 check;
      if (i < TIMEOUT && fault !== 1'b0) begin
        errors = errors + 1;
        $display("FAIL: premature timeout fault at count=%0d", i);
      end
      if (i == TIMEOUT && fault !== 1'b1) begin
        errors = errors + 1;
        $display("FAIL: timeout fault missing at count=%0d", i);
      end
    end
    if (errors == 0)
      $display("PASS: missing kick raises timeout fault at count==%0d", TIMEOUT);

    if (errors == 0) $display("ALL TESTS PASSED");
    else             $display("TESTS FAILED: %0d error(s)", errors);
    $finish;
  end
endmodule
PASS: valid in-window kicks keep fault deasserted
PASS: early kick (count<OPEN) raises a fault
PASS: missing kick raises timeout fault at count==8
ALL TESTS PASSED

The log verifies all three paths: a valid in-window kick keeps fault deasserted across 20 cycles; an early kick (counter still below OPEN) latches fault high; and withholding the kick entirely latches fault at the deadline. The sticky fault register is compared every cycle against an independent golden model, so a wrong window boundary would surface as a FAIL.

Why MCUs and SoCs use watchdog timers

Embedded firmware can hang: an infinite loop, a deadlock between tasks, a corrupted stack, or a peripheral that never returns. A watchdog timer is an independent hardware block that assumes the worst unless the running software proves it is still alive. Each healthy pass through the main loop (or a dedicated supervisor task) issues a kick that reloads the counter. If the software stops making progress, the kicks stop, the counter expires, and the watchdog pulls a reset line or raises a non-maskable interrupt to recover the system.

Because the watchdog lives outside the CPU it is guarding, it keeps counting even when the core is wedged. That independence is the whole point: it is the last line of defense in automotive, medical, industrial, and aerospace SoCs where an unattended lock-up is unacceptable.

Choosing the timeout interval

The TIMEOUT value is a trade-off. Too short and legitimate but slow operations (a long flash erase, a burst of interrupts) trip the watchdog and cause spurious resets. Too long and the system sits hung for an unacceptable time before recovering.

  • Pick an interval comfortably longer than the worst-case time between two consecutive kicks under normal load, with margin for interrupt latency and jitter.
  • Express the interval in cycles of the watchdog's own clock. If the watchdog runs from a slow always-on oscillator (common for low-power designs), convert the desired milliseconds into that clock's cycles.
  • Making TIMEOUT a parameter (as above) lets the same RTL serve a fast and a slow domain without edits.

In the examples the counter is 32 bits wide so any reasonable TIMEOUT fits; for area-critical blocks size the register to $clog2(TIMEOUT+1) bits instead.

The kick handshake

The kick input is a single-cycle pulse. In the simple timer any kick reloads the counter; in the windowed timer the kick is only valid inside the open window. Two design rules matter:

  • Kick has priority. In the simple watchdog the kick branch is evaluated before the expiry comparison, so a kick landing on the very last cycle still services the dog rather than being lost to a simultaneous timeout.
  • One kick, one pulse. Firmware should assert kick for exactly one clock. A level held high forever would permanently disable the watchdog and defeat its purpose — real silicon usually requires writing a specific key value to a register so a stuck bus cannot accidentally keep petting the dog.

Windowed handshake

The windowed variant adds a minimum spacing: software must wait until the window opens before kicking. This detects a task that is running too fast or re-entering unexpectedly, which a plain watchdog would happily accept.

Metastability note on an asynchronous kick

If the kick originates in a different clock domain from the watchdog — for example a slow always-on watchdog clock being kicked by a fast CPU domain — the kick signal must be synchronized before it enters the counter logic. Sampling an asynchronous pulse directly can violate setup/hold and push the first flop metastable, which may corrupt the reload or, worse, miss the kick entirely.

Use a standard two-flop synchronizer in the watchdog clock domain, and because a fast single-cycle pulse can be missed by a slower sampler, convert it to a level with a toggle/handshake (toggle a bit in the source domain, synchronize it, and detect the edge) so no kick is dropped. The RTL above assumes kick is already synchronous to clk.

Synthesis notes

  • Both modules use a single always @(posedge clk or negedge rst_n) block, inferring flip-flops with an asynchronous active-low reset — the common ASIC/FPGA style.
  • In the simple timer timeout is a combinational decode of the counter. If you need a glitch-free, fully registered output, add a flop and expect the alarm one cycle later; adjust the expiry compare accordingly.
  • The windowed timer's fault is intentionally sticky (latched until reset) so a transient violation cannot be silently cleared — a safety requirement in many standards.
  • Size the counter register to the real interval; a 32-bit counter is convenient for simulation but wasteful in silicon when TIMEOUT is small.

FAQ

What happens if the kick and the timeout occur on the same cycle?

In the simple watchdog the kick wins, because its branch is checked first; the counter reloads and the alarm never asserts that cycle. This is deliberate so a just-in-time kick is honored.

Why does the windowed watchdog fault on an early kick?

An early kick signals that the software is not running at its expected industry-standard EDA tools — it may be looping too fast or re-entering the service routine. A plain watchdog cannot see this; the window lower bound makes it a detectable fault.

How is this different from a simple timer?

An ordinary timer just counts and interrupts. A watchdog's defining feature is that software must actively and repeatedly service it, so the absence of action — not the presence — is what triggers recovery.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD