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 PASSEDThe log shows three scenarios. First, a kick every third cycle keeps the counter below the threshold, so
timeoutstays low for 18 cycles. Second, once the kicks stop,timeoutasserts on exactly theTIMEOUT-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 endmodulePASS: 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 PASSEDThe log verifies all three paths: a valid in-window kick keeps
faultdeasserted across 20 cycles; an early kick (counter still belowOPEN) latchesfaulthigh; and withholding the kick entirely latchesfaultat the deadline. The stickyfaultregister 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
TIMEOUTvalue 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
TIMEOUTa 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
TIMEOUTfits; for area-critical blocks size the register to$clog2(TIMEOUT+1)bits instead.The kick handshake
The
kickinput 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
kickbranch 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
kickfor 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
kicksignal 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
kickis already synchronous toclk.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
timeoutis 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
faultis 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
TIMEOUTis 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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres