Switch Debounce Verilog Code: Counter-Based Debouncer
A mechanical push-button or slide switch never makes a clean 0-to-1 edge. Its metal contacts bounce for anywhere from a few hundred microseconds to several milliseconds, so a naive edge detector reads one physical press as a burst of pulses. The debounce Verilog code below fixes that in hardware: a parameterized, counter-based debouncer that accepts a new level only after the input has held steady for a programmable number of clock cycles, feeding it through a two-flop synchronizer first so an asynchronous switch cannot cause metastability.
// debounce_tb.v -- counter-based switch debouncer + bounce-injecting bench
`timescale 1ns/1ns
module debounce #(
parameter integer STABLE_COUNT = 16 // clk cycles the input must stay stable
)(
input wire clk,
input wire rst_n, // active-low reset
input wire noisy_in, // raw, bouncing switch line
output reg clean_out // debounced, glitch-free level
);
// counter width: enough bits to reach STABLE_COUNT-1
localparam integer CW = (STABLE_COUNT < 2) ? 1 : $clog2(STABLE_COUNT);
reg sync0, sync1; // two-FF synchronizer
reg [CW-1:0] count;
// 1) bring the asynchronous switch line into the clk domain
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sync0 <= 1'b0;
sync1 <= 1'b0;
end else begin
sync0 <= noisy_in;
sync1 <= sync0;
end
end
// 2) require the synced input to hold a new value for STABLE_COUNT cycles
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= {CW{1'b0}};
clean_out <= 1'b0;
end else if (sync1 == clean_out) begin
count <= {CW{1'b0}}; // input agrees with output: reset timer
end else begin
count <= count + 1'b1; // input differs: keep timing it
if (count == STABLE_COUNT - 1) begin
clean_out <= sync1; // stable long enough -> accept it
count <= {CW{1'b0}};
end
end
end
endmodule
module tb_debounce;
reg clk, rst_n, noisy_in;
wire clean_out;
// small STABLE_COUNT so the simulation finishes quickly
debounce #(.STABLE_COUNT(4)) dut (
.clk(clk), .rst_n(rst_n),
.noisy_in(noisy_in), .clean_out(clean_out)
);
initial clk = 1'b0;
always #5 clk = ~clk; // 100 MHz, 10 ns period
// print every time the debounced output changes
always @(clean_out)
$display("[%4t ns] clean_out = %b", $time, clean_out);
// drive 6 chattering samples, then settle on final_val
task inject_bounce(input final_val);
integer k;
begin
for (k = 0; k < 6; k = k + 1) begin
noisy_in = $random; // random contact chatter
#8;
end
noisy_in = final_val; // mechanical contact settles
end
endtask
initial begin
rst_n = 1'b0; noisy_in = 1'b0;
#13 rst_n = 1'b1;
#20 $display("[%4t ns] PRESS : bouncing, will settle HIGH", $time);
inject_bounce(1'b1);
#120;
$display("[%4t ns] RELEASE: bouncing, will settle LOW", $time);
inject_bounce(1'b0);
#120;
$display("[%4t ns] done, final clean_out = %b", $time, clean_out);
$finish;
end
endmodule[ 0 ns] clean_out = 0 [ 33 ns] PRESS : bouncing, will settle HIGH [ 95 ns] clean_out = 1 [ 201 ns] RELEASE: bouncing, will settle LOW [ 305 ns] clean_out = 0 [ 369 ns] done, final clean_out = 0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpr0lnp7p0/debounce_tb.v:89: $finish called at 369 (1ns)
How the counter-based debouncer works
The design has two always blocks. The first is a classic two-flip-flop synchronizer (sync0 then sync1). A switch is asynchronous to your clock, so sampling it directly into logic risks metastability; the two stages let any metastable value settle before it reaches the counter.
The second block is the debounce timer. On every clock it compares the synchronized input sync1 with the current output clean_out:
- If they agree, nothing has changed, so the counter is held at zero.
- If they differ, the input is proposing a new level, so the counter increments. The moment it reaches
STABLE_COUNT - 1, the new level has survived a full stable window, soclean_outlatchessync1and the counter clears.
The key property: any glitch back to the old value during the wait resets the counter to zero, so the input must hold its new state for STABLE_COUNT consecutive clocks before it is believed. That is exactly what filters contact bounce.
| Signal | Direction | Role |
|---|---|---|
clk | input | System clock; the whole filter is synchronous to it. |
rst_n | input | Active-low reset; clears the synchronizer, counter and output. |
noisy_in | input | Raw, bouncing line straight from the switch. |
clean_out | output | Registered, glitch-free level, one clock-stable window behind the switch. |
STABLE_COUNT | parameter | Number of clocks the input must stay stable before it is accepted. |
The counter width is computed, not hard-coded: localparam CW = $clog2(STABLE_COUNT) sizes the register to exactly the bits needed, so you change one parameter and the hardware follows.
Choosing STABLE_COUNT for real hardware
Pick the stable window from your worst-case bounce time and clock frequency: STABLE_COUNT = t_stable × f_clk. A common, conservative target is a 10 ms guard. The table shows the counter size that implies — note how quickly the register grows, which is why computing the width matters.
| Clock | Stable window | STABLE_COUNT | Counter bits |
|---|---|---|---|
| 1 kHz tick | 10 ms | 10 | 4 |
| 1 MHz | 10 ms | 10,000 | 14 |
| 100 MHz | 10 ms | 1,000,000 | 20 |
The testbench instantiates the module with STABLE_COUNT = 4 purely so the simulation finishes in a few hundred nanoseconds. In a real FPGA you would feed the module a slow enable pulse or a large count, never four clocks at 100 MHz.
Seeing the problem: naive vs debounced
Why bother at all? The second bench drives a single button press whose contact chatters three times before settling, then counts rising edges two ways: once on the raw line and once on the debounced line. Read the simulation log — the raw count registers every bounce, while the debounced count is exactly one clean press.
// press_counter_tb.v -- why debouncing matters: naive vs debounced press count
`timescale 1ns/1ns
module debounce #(
parameter integer STABLE_COUNT = 4 // clk cycles the input must stay stable
)(
input wire clk,
input wire rst_n,
input wire noisy_in,
output reg clean_out
);
localparam integer CW = (STABLE_COUNT < 2) ? 1 : $clog2(STABLE_COUNT);
reg sync0, sync1;
reg [CW-1:0] count;
always @(posedge clk or negedge rst_n)
if (!rst_n) {sync1, sync0} <= 2'b00;
else {sync1, sync0} <= {sync0, noisy_in};
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
count <= {CW{1'b0}};
clean_out <= 1'b0;
end else if (sync1 == clean_out) begin
count <= {CW{1'b0}};
end else begin
count <= count + 1'b1;
if (count == STABLE_COUNT - 1) begin
clean_out <= sync1;
count <= {CW{1'b0}};
end
end
endmodule
module tb_press_counter;
reg clk, rst_n, noisy_in;
wire clean;
reg naive_q, clean_q;
reg [7:0] naive_cnt, clean_cnt;
integer k;
debounce #(.STABLE_COUNT(4)) dut (
.clk(clk), .rst_n(rst_n), .noisy_in(noisy_in), .clean_out(clean));
initial clk = 1'b0;
always #5 clk = ~clk;
// count rising edges on the RAW line (naive) and on the CLEAN line
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
naive_q <= 1'b0; clean_q <= 1'b0;
naive_cnt <= 8'd0; clean_cnt <= 8'd0;
end else begin
naive_q <= noisy_in;
clean_q <= clean;
if ( noisy_in & ~naive_q) naive_cnt <= naive_cnt + 1'b1;
if ( clean & ~clean_q) clean_cnt <= clean_cnt + 1'b1;
end
// one make/break bounce: HIGH then LOW, each 12 ns (well under the
// 40 ns debounce window), so the raw line never settles mid-bounce
task one_bounce;
begin
noisy_in = 1'b1; #12;
noisy_in = 1'b0; #12;
end
endtask
initial begin
rst_n = 1'b0; noisy_in = 1'b0;
#13 rst_n = 1'b1;
// ONE physical button press, but the contact chatters several times
#20;
for (k = 0; k < 3; k = k + 1) one_bounce;
noisy_in = 1'b1; // finally settles pressed
#120;
$display("one physical press with contact bounce:");
$display(" naive rising-edge count = %0d", naive_cnt);
$display(" debounced rising-edge count = %0d", clean_cnt);
$finish;
end
endmoduleone physical press with contact bounce: naive rising-edge count = 4 debounced rising-edge count = 1 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpc25qg5u8/press_counter_tb.v:85: $finish called at 225 (1ns)
That gap is the entire reason this debounce Verilog code exists. Wire the raw line into a menu counter, a state machine, or an interrupt and every physical press fires several times; route it through clean_out and you get one event per press.
Adapting it to your design
Two tweaks cover most real uses. First, if a million-cycle counter feels wasteful, drive the module from a slow enable instead: generate a tick every, say, 1 ms and gate both always blocks with it, so a tiny STABLE_COUNT still spans several milliseconds. Second, when downstream logic wants an event rather than a level, register clean_out once more and AND the new value with the inverted old one; the result is a one-clock-wide pulse on each press. The second bench already does exactly this to count presses, so you can lift that rising-edge detector straight into your own module.
Everything stays on a single clock and nothing is hard-coded, which keeps the block easy to reuse across boards: change STABLE_COUNT, or the enable rate, and the same source retargets from a 1 kHz housekeeping tick to a 100 MHz fabric clock without edits.
Common mistakes
- Skipping the synchronizer. The switch is asynchronous to
clk. Samplingnoisy_instraight into the counter invites metastability; always keep the twosyncflops. - Shipping the simulation value.
STABLE_COUNT = 4is for a fast sim only. At 100 MHz that is 40 ns of filtering — thousands of times too short for real bounce. Size it from your clock and worst-case bounce time. - Driving a clock with the switch. Never use
noisy_in(or evenclean_out) as a clock. Keep everything on one clock and treat the button as data. - Making the output combinational. If
clean_outis a wire of the raw input, glitches pass straight through. It must be a register updated only after the stable window. - Off-by-one on the window. Compare against
STABLE_COUNT - 1and reset the counter when the input matches the output. Resetting on the wrong condition lets a mid-settle glitch restart or prematurely accept the level. - Forgetting the one-shot.
clean_outis a clean level, not a pulse. If you need a single-cycle “pressed” event, add a rising-edge detector onclean_out, as the second bench does.
Quick check
Why does a mechanical switch need debouncing at all?
Its metal contacts physically bounce as they open or close, making and breaking the circuit many times over a few milliseconds. Each make/break is a real electrical edge, so one press looks like many to downstream logic.
What does STABLE_COUNT control, and how do you turn it into real time?
It is the number of clock cycles the input must stay unchanged before the output updates. In time that is STABLE_COUNT / f_clk (equivalently STABLE_COUNT × clock period). For a 10 ms guard at 100 MHz you need 1,000,000 cycles.
Why are there two flip-flops before the counter?
The switch is asynchronous to the clock, so a direct sample can land in a metastable state. The two-flop synchronizer gives any metastable value a full clock to resolve before the counter uses it.
In this design, exactly when does clean_out change?
Only after the synchronized input has differed from clean_out for STABLE_COUNT consecutive clocks (the counter reaches STABLE_COUNT - 1). Any return to the old value before then resets the counter, so a lone glitch is ignored.
Share your question in comments or talk to our mentor team for batch guidance.
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