Button debouncer (Verilog)
A mechanical button bounces for a few milliseconds; feed it straight into logic and you
get dozens of spurious edges. This module synchronizes the async input (two flops),
requires the level to be stable for a full debounce window before accepting it, and
emits a clean one-cycle pressed pulse — perfect for incrementing a counter or triggering
an FSM exactly once per press.
module debouncer #(
parameter integer CLK_HZ = 50_000_000,
parameter integer MS = 10
)(
input wire clk,
input wire rst_n,
input wire btn, // raw, async, active-high
output reg level, // debounced level
output reg pressed // 1-cycle pulse on a clean press
);
localparam integer TICKS = (CLK_HZ / 1000) * MS;
// two-flop synchronizer for the async pin
reg meta, sync;
always @(posedge clk) begin
meta <= btn;
sync <= meta;
end
reg [$clog2(TICKS + 1) - 1 : 0] count;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
count <= 0;
level <= 1'b0;
pressed <= 1'b0;
end
else begin
pressed <= 1'b0;
if (sync == level)
count <= 0; // stable: hold
else if (count == TICKS) begin
level <= sync; // accept the new level
pressed <= sync & ~level; // pulse on 0 -> 1 only
count <= 0;
end
else
count <= count + 1'b1;
end
endmodule
How it works
The counter only advances while the synchronized input differs from the current
debounced level; any glitch resets it. Only after MS milliseconds of a steady new level
does level flip, and pressed fires for exactly one clock on a press. The two-flop
synchronizer protects against metastability on the async pin.
Usage
- Set
CLK_HZto your fabric clock andMSto taste (5–20 ms is typical). - Use
pressedto clock-enable downstream logic — never use a button as a clock. - For an active-low button, invert
btnat the port or flip the pulse condition.