Debouncing Buttons and Keypresses

Buttons, Keys and Asynchronous Signals

In the time it takes for a user to push a button or type a key, millions of clocks cycles may pass. Usually the user’s intention is to trigger a single event, not millions of them. To make things more complicated, buttons and keys tend to “bounce” when switching between 0 and 1. Instead of a clean transition, the signal may flutter back and forth before settling into a clear logic level. So a good Human Interface Device (HID) should reject the bouncing and reduce a long keypress to a single clean pulse lasting one clock cycle.

The debouncing problem also introduces the concept of asynchronous interfaces. A human user is not synchronized to the digital system’s clock, so the system needs to do some work to detect the information. The same problem occurs whenever signals needs to be exchanged between different digital systems that are on separate clocks. Such systems are not synchronized, hence they are called asynchronous.

Simple Strategies

The simplest debouncing strategy is to make a register assignment, like this:

module debouncer
 (
   input       clk,
   input       btn,
   output reg  btn_db // debounced output
 );

  always @(posedge clk) begin
     btn_db <= btn;
  end
  
endmodule

This always code is sensitive to posedge clk, so the value of btn is evaluated only at the rising edge of the clock. That should take care of any “fast” bouncing. Slower bouncing can be resolved using a clock divider to make the assignments less frequent.

This method doesn’t resolve the long button press problem. What we really want is to trigger a single clock cycle pulse regardless of how long the button is held down. One way to do this is by adding delay registers as in the code below.

module debouncer
 (
   input       clk,
   input       btn,
   output reg  btn_db // debounced output
 );

  reg btn_d; // button delay by one clock cycle
 
  always @(posedge clk) begin
     btn_d  <= btn;          
     btn_db <= btn & ~btn_d;  // true only when btn rises from 0 to 1
  end
  
endmodule

Here there are two concurrent non-blocking assignments. These assignments take effect in the next (future) clock cycle. That means the btn_db assignment compares the value of btn now to its value in the previous cycle. The btn_db output is only 1 for a single clock cycle, when btn changes from 0 to 1.

This style of debouncer is a good start, but still misses a bounce from time to time, resulting in unintended double-press errors.

Debouncing with a Timer

A more robust debouncing approach is to ensure that the btn signal is 1 continuously for some number of clock cycles, and then settles to zero for a number of cycles. A standard way to count clock cycles is to use an alarm timer. An example is given in src/tcounter.v, listed here:

module tcounter
  #(
     parameter N=100
    )
   (
     input      clk,
     input      rst_l,
     input      clear,
     output reg done
   );

   integer     clk_count;

   always @(posedge clk, negedge rst_l) begin
      if (!rst_l) begin
        done      <= 0;
        clk_count <= 0;        
      end
      else begin
        if (clear) begin           
           clk_count <= 0;
           done      <= 0;
        end
        else if (clk_count == N) 
           done <= 1;
        else 
           clk_count <= clk_count + 1;                  
      end
   end   
endmodule // tcounter

In this module, the alarm is reset when clear is 1. When clear drops from 1 to 0, the timer starts counting. Once N clock cycles have elapsed, done rises from 0 to 1 and stays high until another clear signal is given.

Debouncer State Machine

Using this alarm timer, we can design a debouncer state machine with three states:

State transition diagram for the debouncer.

Let’s analyze the behavior in each of these states:

  1. WAIT state. The machine is waiting for a btn event. There are two outgoing transitions:
  2. PRESS state. The btn has gone to 1, and clear is 0 so the tcounter is running. There are three outgoing transitions:
  3. RELEASE state. Now btn_db is set to 0 so that the output pulse lasts a single clock cycle. There are two outgoing transitions:

Using localparam for Descriptive State Names

Numerical state values can be confusing when analyzing a complex or high-level FSM. Many designers prefer to use Verilog constants with local scope (i.e. defined only within the module). This is done using the localparam syntax.

Instead of using states 0, 1 and 2, we give them names:

localparam WAIT    = 0;
localparam PRESS   = 1;
localparam RELEASE = 2;

Later in the code, these names can be used as if they were numerical constants.

Assigned Tasks

Implement the Debouncer State Machine

Create a file named src/debouncer.v and declare a module named debouncer. Use the code fragment shown below as a template. The first state is already done.

module debouncer
  (
   input clk,
   input rst_l,
   input btn,
   output reg btn_db
   );

   reg [1:0]  state;
   reg              clear;

   wire       t; // tcounter timer alarm
   
   localparam WAIT    = 0;
   localparam PRESS   = 1;
   localparam RELEASE = 2;

   // tcounter instance   
   tcounter T1
     (
      .clk(clk),
      .rst_l(rst_l),
      .clear(clear),
      .done(t)
      );

   initial begin
      state  = WAIT;
      clear  = 1;
      btn_db = 0;      
   end

   always @(posedge clk, negedge rst_l) begin
      if (!rst_l) begin
        state  <= WAIT;
        clear  <= 1;
        btn_db <= 0;        
      end
      else begin
        case (state)
          WAIT:
            begin
              // btn_db is set to zero for all transitions
              // from this state
              btn_db <= 0;
              if (btn) begin
                 state <= PRESS;  // change state
                 clear <= 0;      // start the timer
              end
              else
                clear <= 1;              
            end
          PRESS:
            begin
               // YOU DO THIS
            end
          RELEASE:
            begin
              // YOU DO THIS
            end
          default:
            // DEFAULT behavior is necessary since we aren't
            // using state 2'b11 ('d3). If the module somehow
            // arrives in state 3, we need to give it a way
            // back to normal functioning so it can recover
            // gracefully from faults. 
            begin
               btn_db <= 0;
              clear  <= 1;
              state  <= WAIT;
            end
        endcase
      end
   end
   
endmodule // debouncer

Make a top Module

We will use a top-level wrapper module to do the following:

module top
  (
   input      clk,
   input      btn,
   input      rst,     
   output reg led
   );

   wire       btn_db;
   wire       rst_l;

   assign rst_l = ~rst;
   
   initial begin
      led = 0;
   end
   
   debouncer db1
     (
      .clk(clk),
      .rst_l(rst_l),
      .btn(btn),
      .btn_db(btn_db)
      );

   always @(posedge clk, negedge rst_l) begin
      if (!rst_l) begin
        led <= 0;
      end
      // Toggle the LED if the button is pressed:
      else begin
        if (btn_db)
          led <= ~led;        
      end
   end

endmodule // top

Testbench Simulation

Open src/testbench.v and find the lines below. These create a stimulus pattern to simulate a bouncing button press. The btn signal bounces from 0 to 1 starting at cycle 10, then remains stable at 1 until cycle 200, then bounces back to 0 until it becomes stable again at cycle 235:

        case (clk_count)
          10: // start button press
            btn <= 1;
          20: // bounce
            btn <= 0;
          24: //bounce
            btn <= 1;
          30: //bounce
            btn <= 0;
          31: // now sustain
            btn <= 1;
          200: // release
            btn <= 0;
          210: // bounce
            btn <= 1;
          215: // bounce
            btn <= 0;
          230: //bounce
            btn <= 1;
          235: //sustain
            btn <= 0;
                    
        endcase // case (clk_count)

The simulation is run until 500 clock cycles in order to verify that the state returns to zero after the button stabilizes:

   // DEFINE WHEN TO TERMINATE SIMULATION:
   always @(posedge clk) begin
      clk_count <= clk_count + 1;
      if (clk_count == 500) begin
        $fclose(fid);
        $finish;
      end
   end

Run the simulation using make and you should see output like this:

clk:    11 clk diff:    11 state: 0 clear: 1 t: 0 btn: 1 led:  0
clk:    12 clk diff:     1 state: 1 clear: 0 t: 0 btn: 1 led:  0
clk:    21 clk diff:     9 state: 1 clear: 0 t: 0 btn: 0 led:  0
clk:    22 clk diff:     1 state: 0 clear: 1 t: 0 btn: 0 led:  0
clk:    25 clk diff:     3 state: 0 clear: 1 t: 0 btn: 1 led:  0
clk:    26 clk diff:     1 state: 1 clear: 0 t: 0 btn: 1 led:  0
clk:    31 clk diff:     5 state: 1 clear: 0 t: 0 btn: 0 led:  0
clk:    32 clk diff:     1 state: 0 clear: 1 t: 0 btn: 1 led:  0
clk:    33 clk diff:     1 state: 1 clear: 0 t: 0 btn: 1 led:  0
clk:   134 clk diff:   101 state: 1 clear: 0 t: 1 btn: 1 led:  0
clk:   201 clk diff:    67 state: 1 clear: 0 t: 1 btn: 0 led:  0
clk:   202 clk diff:     1 state: 2 clear: 1 t: 1 btn: 0 led:  0
clk:   203 clk diff:     1 state: 2 clear: 0 t: 0 btn: 0 led:  1
clk:   211 clk diff:     8 state: 2 clear: 0 t: 0 btn: 1 led:  1
clk:   216 clk diff:     5 state: 2 clear: 0 t: 0 btn: 0 led:  1
clk:   231 clk diff:    15 state: 2 clear: 0 t: 0 btn: 1 led:  1
clk:   236 clk diff:     5 state: 2 clear: 0 t: 0 btn: 0 led:  1
clk:   304 clk diff:    68 state: 2 clear: 0 t: 1 btn: 0 led:  1
clk:   305 clk diff:     1 state: 0 clear: 1 t: 1 btn: 0 led:  1
clk:   306 clk diff:     1 state: 0 clear: 1 t: 0 btn: 0 led:  1

The output lines are only printed when there is an interesting signal change (see explanation in the next section). In the first line of output at clock 11, we see btn go high, then the FSM state goes to state 1 at clock 12. The btn bounces a few times causing state to cycle between 0 and 1.

The button is initially pressed and bounces a few times.

The btn signal stabilizes at clock 33, and the alarm t is finished at clock 134. At this point the FSM sits in state 1 until btn goes low at clock 201. At this point the FSM transitions to state 2, and btn_db is pulsed causing led to toggle.

The button is released, and the debounced output is pulsed.

The FSM then loiters in state 2 until clock 304 when t fires again. At clock 305 the btn has stabilized, and the FSM is back in state 0, ready to detect a new button press.

Data Visualization Methods

In this project we explore several new options for data visualization. Instead of straining over a raw cycle-by-cycle text dump, we can use several methods to make the data more interpretable.

Text output

To make the text output readable, we only print an output line when an interesting change occurs. To detect these changes, we use a set of delay registers and use XOR operations to detect when something has gone from 0-to-1 or 1-to-0:

   reg btn_d;   // delayed button (to detect changes)
   reg led_d;   // delayed led
   reg state_d; // delayed debouncer state
   reg clear_d; // delayed clear signal
   reg t_d;     // delayed timer alarm
   
   reg value_change; // 1 if something changes, 0 otherwise

   always @(*) begin
      value_change = btn ^ btn_d;
      value_change = value_change | (led ^ led_d);
      value_change = value_change | (DUT.db1.clear ^ clear_d);
      value_change = value_change | (DUT.db1.state ^ state_d);
      value_change = value_change | (DUT.db1.t ^ t_d);      
   end

   always @(posedge clk) begin
     // print data summary only if there is a change:
     if (value_change) begin
        clk_count_d <= clk_count;
        
        $write("clk:  %4d", clk_count);      
        $write(" clk diff:  %4d", clk_count-clk_count_d);
        $write(" state: %1d", DUT.db1.state);
        $write(" clear: %b", DUT.db1.clear);

        // ... and so on ... //
     end
  end

Tab-Delimited Raw Data

These lines produce a tab delimited data file in test_result.txt:

        $fwrite(fid,"%4d", clk_count);      
        $fwrite(fid,"\t%4d", clk_count-clk_count_d);
        $fwrite(fid,"\t%1d", DUT.db1.state);
        $fwrite(fid,"\t%b", DUT.db1.clear);
        $fwrite(fid,"\t%b", DUT.db1.t);
        $fwrite(fid,"\t%b", btn);
        $fwrite(fid,"\t%d", led);
        $fwrite(fid,"\n");

A tab-delimited file can be imported by many software applications. In Matlab, you can use the importdata command:

d = importdata('test_result.txt');

The data is loaded into a matrix named d. You can plot columns from the data using the plot, stem, or stairs commands, for example:

stairs(d(:,1),d(:,6))
xlabel('Clock Cycle')
ylabel('btn')

Run xsim in GUI mode

The Vivado graphical interface can be used by running the simulation in GUI mode:

make gui

This allows using the graphical interface to zoom and view signals side-by-side:

Vivado xsim in GUI mode.

Use gtkwave to view vcd file

These lines in src/testbench.v direct the simulator to create a Value Change Dump (VCD) file for the indicated module. VCD files are used to exchange simulation data between a variety of analysis tools.

   // INITIAL SIGNAL CONFIGURATION:
   initial begin

      $dumpfile("debounce.vcd");
      $dumpvars(1,DUT.db1);
      
   end

The simulator will save all the signal changes in the db1 module and dump them into debounce.vcd. There are a number of lightweight programs for viewing VCD files. One of the more popular tools is gtkwave, which is a multi-platform free option.

Viewing the VCD file in gtkwave

Implement, Program and Test

Copy the master XDC file, Basys3_Master.xdc to a new one named debouncer.xdc. In debouncer.xdc, locate all the lines corresponding to these ports, and move them to the top of the file:

Then delete all the other lines since we aren’t using any other resources.

Run make implement to build your design, and program it onto the Basys3 board. Record a short video showing yourself pressing the button a few times. The LED should toggle ON/OFF each time you press and release the button.

Turn in your work using git:

git add src/*.v *.v *.rpt *.txt *.tcl *.bit *.xdc
git commit . -m "Complete"
git push origin master

Upload your video on Canvas to indicate that your assignment is done.