Day 5 · Counters, Shifters & Sync

Button Debouncing

Video 4 of 4 · ~10 minutes

Dr. Mike Borowczak · Electrical & Computer Engineering · CECS · UCF

Counter VariationsShift RegistersMetastabilityDebouncing

🌍 Where This Lives

In Industry

Every device with mechanical input — keyboards, remote controls, industrial control panels, elevator buttons, car key fobs — has a debouncer. For a 10-key keypad, the firmware detects 10 distinct presses only because each button is debounced. Oscilloscope captures of raw switch closures show 5-20 ms of noise — thousands of transitions — that must be filtered.

In This Course

Every lab with a pushbutton input. Day 7 FSM input demo. Day 10 PPA comparison (is your debouncer efficient?). Your capstone Barcelona project will use this pattern verbatim on at least one input.

⚠️ Buttons Don't Just Close — They Ring

❌ Wrong Model

“I press the button, the signal goes from 0 to 1, stays at 1 until I release. My edge detector sees one rising edge per press.”

✓ Right Model

The mechanical contacts bounce on impact — making and breaking the circuit rapidly for 5–20 ms. Put a scope on a button: you'll see a burst of transitions before the signal settles. At 25 MHz, that's 125,000 to 500,000 false edges from a single press.

The receipt: Without debouncing, your “count button presses” FSM increments by hundreds per click. Your menu cursor skips dozens of positions. Your UART sends garbage. The fix is a 20 ms debounce window — long enough to outlast the bouncing, short enough that humans don't notice.

👁️ I Do — The Counter-Based Debouncer

module debounce #(parameter CLKS_STABLE = 500_000) (  // 20ms @ 25MHz
    input  wire i_clk, i_reset, i_noisy,
    output reg  o_clean
);
    reg [$clog2(CLKS_STABLE)-1:0] r_count;

    always @(posedge i_clk) begin
        if (i_reset) begin
            r_count <= 0;
            o_clean <= 1'b0;
        end else if (i_noisy != o_clean) begin
            // input differs from stable output — start/continue counting
            if (r_count == CLKS_STABLE - 1) begin
                o_clean <= i_noisy;        // persisted long enough, accept
                r_count <= 0;
            end else begin
                r_count <= r_count + 1'b1;
            end
        end else begin
            r_count <= 0;                   // bounced back — reset counter
        end
    end
endmodule
My thinking: The counter measures how long the input has persisted at a different value than the output. Only after CLKS_STABLE consecutive cycles of difference do we accept the new value. Bounces reset the counter — so any glitchy transition keeps the output stable.

🤝 We Do — The Complete Input Pipeline

//  i_btn  →  [2-FF sync]  →  [debouncer]  →  [edge detect]  →  i_pressed_pulse
//  (raw)      (metastable      (filter         (1-cycle
//              safety)          bounces)        pulse per press)

wire w_synced, w_clean;
reg  r_clean_d1;

sync_2ff  u_sync    (.i_clk(clk), .i_async(i_btn),   .o_synced(w_synced));
debounce  u_deb     (.i_clk(clk), .i_reset(rst), .i_noisy(w_synced), .o_clean(w_clean));

always @(posedge clk) r_clean_d1 <= w_clean;
wire pressed_pulse = w_clean & ~r_clean_d1;  // rising-edge detect
The three-stage pipeline: (1) synchronizer — metastability safety, (2) debouncer — bounce filter, (3) edge detector — one-cycle pulse. Any external button signal needs all three stages. Build it as a reusable module and instantiate it.

🧪 You Do — Size the Counter

You want a 10 ms debounce window on a 12 MHz clock. What's CLKS_STABLE and the required counter width?

Answer: CLKS_STABLE = 12,000,000 × 0.010 = 120,000. Width = $clog2(120,000) = 17 bits. Verify: 2^17 = 131,072 ≥ 120,000. ✓
Follow-up: Too short (1 ms)? You re-enable bounces. Too long (100 ms)? Humans perceive lag. 10–20 ms is the sweet spot; most production designs pick 20 ms.
▶ LIVE DEMO

Debouncer on the Go Board

~5 minutes — real button, real bounces

▸ COMMANDS

cd labs/week2_day05/ex4_debouncer/
make sim       # TB injects simulated bounces
make wave
make prog      # flash to Go Board
# Press SW1 — watch counter on LEDs

▸ EXPECTED BEHAVIOR

Without debounce:
  press SW1 once → counter +3, +7
  (bounces cause multiple inc)

With debounce:
  press SW1 once → counter +1
  (clean single increment)

▸ GTKWAVE

Signals: i_noisy · r_count · o_clean · pressed_pulse. Watch the counter climb during noise, reset on each bounce, finally hit terminal count and assert o_clean. pressed_pulse is a clean 1-cycle pulse — the “one press” event your FSM wants.

🔧 What Did the Tool Build?

$ yosys -p "read_verilog debounce.v; synth_ice40 -top debounce; stat" -q

=== debounce ===  (CLKS_STABLE = 500000, width = $clog2(500000) = 19)
   Number of wires:                 30
   Number of cells:                 37
     SB_CARRY                       18
     SB_DFFESR                      20    ← 19 counter + 1 clean output
     SB_LUT4                         9
Scaling: ~40 cells per debouncer. An 8-button input array = 320 cells = ~25% of an iCE40 HX1K. Pattern efficiency matters when you instantiate it many times. (Day 8 shows how to use generate to instantiate an array without copy-paste.)

🤖 Check the Machine

Ask AI: “Design a debouncer for a 100 Hz clock and 10 ms debounce. Then explain why we also need a synchronizer in front of it.”

TASK

Debouncer + justify sync upstream.

BEFORE

Predict: CLKS_STABLE=1. 1-bit counter. Sync still needed because button is async.

AFTER

AI should explain: debouncer ≠ synchronizer. Different problems, different fixes.

TAKEAWAY

A common AI error: conflating the two. Both are needed, in series.

Key Takeaways

 Mechanical switches bounce for 5–20 ms. Filter them out.

 Counter-based debouncer: accept new value only after persistent stability.

 Full input pipeline = sync → debounce → edge detect.

 Build once, reuse on every button input.

Every external button needs three stages. Build the pipeline. Keep it.

Pre-Class Self-Check

Q1: Why isn't a synchronizer alone enough for a button?

The synchronizer fixes metastability from the first transition. It does nothing about the many additional bounce transitions over the next 5-20 ms.

Q2: At 25 MHz with CLKS_STABLE = 250_000, what's the debounce window in ms?

250,000 / 25,000,000 = 10 ms.

Pre-Class Self-Check (cont.)

Q3: Where does the edge-detector go in the pipeline?

At the end: after sync and debounce. The goal is to produce a single 1-cycle pulse per distinct button press, which only makes sense on a clean signal.

Q4: Could you debounce with a FIFO of samples instead of a counter?

Yes — sampling at a lower rate (e.g. 1 kHz) and looking for N consecutive same-value samples is an alternative. Uses less logic for long windows but adds a sample-rate divider. Counter-based is the more common choice.

🔗 End of Day 5

Tomorrow: Testbenches

Day 6 · Writing the tests that protect your designs

▸ WHY THIS MATTERS NEXT

You've been running make sim all week and trusting PASS/FAIL reports. Someone wrote those testbenches for you. Day 6 is when you start writing them yourself. Verification is larger than design at most ASIC companies — you're about to learn the craft that makes real silicon work.