Simple State Machine: Scanning a Keypad Grid

Reading from a Keypad

The Digilent Keypad PMOD is a simple grid of buttons:

Keypad module for Basys3 board

The keypad’s physical circuit consists of a switch matrix organized into four rows and four columns:

Keypad switch matrix.

The row signals are the keypad’s outputs. A set of pull-up resistors is used to hold all the row signals HIGH when no button is pressed.

Keypad switch matrix.

The col signals are the keypad’s inputs. At any time, a single col is pulled LOW, so that it can overpower the pull-up resistors on the row signals. Suppose col[0] is pulled LOW, and a key is pressed in that column:

Keypad with one key pressed.

The keypad’s interface module sets col[0] LOW and detects that row[1] is HIGH, so that the keypress is uniquely detected at position (1,0).

High-Impedance Signals

You may have noticed that col[1], col[2] and col[3] are all indicated as Z, which is not a logic value. Verilog supports four-value logic where any wire or reg can have one of these values:

In most situations, X and Z indicate design mistakes. But there are special gates called tri-state buffers that support Z outputs, allowing us to temporarily connect and disconnect wires within a circuit. The Xilinx FPGA uses tri-state buffers for its top-level outputs, so we can write Z values when needed to control PMOD devices.

Scanning Refresh

In order to detect a single keypress, we can pull down one col signal at a time. The other col signals need to be Z so that a single column is isolated. We scan rapidly across all the columns following this sequence:

  1. col = 0ZZZ
  2. col = Z0ZZ
  3. col = ZZ0Z
  4. col = ZZZ0

After scanning through each column, we repeat the sequence indefinitely to refresh the key detection. This loop can be modeled as a simple Finite State Machine (FSM). A Finite State Machine is visually represented using a state transition diagram like the one below. In this diagram style, each circle is a state, and the label within the circle is the state’s identifier (usually a name or integer value). The edges in the graph are transitions, and each transition is labeled with two parts: a condition, then a /, and then an assignment. The condition is the logical test that must be TRUE in order for the transition to occur. The assignment is the change in output signals due to the transition. For clarity, conditions are indicated in blue color.

State transition diagram for scanning refresh process.

In this diagram, the process is initialized in state 0 (indicated by the init condition). A state transition occurs whenever the scan signal is high. The frequency of scan dictates the speed for the keypad refresh process.

Active-Low Reset

Notice that the state diagram indicates !rst_l as a condition to enter state 0. In digital systems there is usually a global reset signal that uses active low logic, so the system is reset whenever rst_l is LOW. The suffix _l is used to indicate an active-low signal. It’s also common to see other suffixes like _b (for “bar”), or _inv (for “inverted”). Whenever a signal is active-low, it should be named with a _l suffix to clarify its behavior.

Why active-low reset? When a system is first powered up, all signals are effectively 0, and at different times their electrical levels may appear to be 0 or 1. By holding rst_l at 0, the system can be maintained in a reset condition until the supply voltage is stable.

In the state transition diagram, there is an implied transition from every state back to 0 whenever rst_l is 0. These transitions are hidden in the diagram, but are indicated in the init | !rst_l condition.

Clock Divider Timing

The refresh period is the time it takes to repeat from col 0 back to col 0 again. The refresh period should be much faster than the time it takes for a human finger to press and release a key. On the other hand, the refresh period should be much slower than the system clock, since the Basys3 electrical signals are slower than the internal FPGA clock.

A good refresh time might be around 12ms. To complete a scan of all four col wires in 12ms, each individual col needs to be scanned for (12ms)/4 = 3ms. To create the appropriate timing, we will use a clock divider module:

module clock_divider
#(
   parameter N=300_000
 )
 (
  input      clk,
  input      rst_l,
  output reg div_clk
  );
   
   integer   clk_count;
   initial begin
      clk_count = 0;
      div_clk   = 0;
   end
   
   always @(posedge clk, negedge rst_l) begin
      // RESET BEHAVIOR:
      if (!rst_l) begin
         clk_count <= 0;
         div_clk   <= 0;
      end
      // NORMAL BEHAVIOR:
      // Pulse div_clk every N clock cycles.
      else begin
         if (clk_count == N) begin
            clk_count <= 0;
            div_clk   <= 1;
         end 
         else begin
            clk_count <= clk_count + 1;
            div_clk   <= 0;
         end 
      end 
   end // always @ (posedge clk, negedge rst_l)
   
endmodule // clock_divider

The clock_divider module sets clk_div HIGH every N clock cycles. On the Basys3, the system clock frequency is fc= 100MHz, with a period of Tc= 10ns. To obtain a 3ms scanning period, we need Tc × N=(10ns)*N = 3ms, or N=300000. This is set as the default value for parameter N in the module declaration.

Assigned Tasks

In the src subdirectory, create a file named clock_divider.v and enter the code shown above. It is prefered for you to enter the code manually so that you fully understand the module.

Prepare the keypad Module

Create a file named src/keypad.v to implement the keypad interface. Use this declaration template:

module keypad
  #(
     parameter N=300_000
   )
   (
     input             clk,
     input             rst_l,
     input      [3:0]  row,
     output     [3:0]  col,
     output reg [15:0] keys
   );

   wire          scan;
   reg   [1:0]   state;

endmodule

The module has a col output to drive the keypad columns, and a row input to receive signals from the keypad rows. The detected keypress(es) are indicated in an output named keys. There are 16 keys; if a key is pressed then the corresponding bit position in keys is set to 1.

Submodules and Initial Conditions

Declare a clock_divider submodule and initialize the state and keys signals:

   initial begin
      state = 0;
      keys  = 0;
   end
     
   clock_divider clkdiv(.clk(clk),.rst_l(rst_l),.div_clk(scan));

The global signals clk and rst_l are “passed through” from the top to submodules. The clock_divider output div_clk is connected to the scan signal. Since scan is defined in a submodule, it is considered a wire within the scope of keypad.

Define the State Machine Behavior

Next, implement the state transition diagram using a case statement as shown below. The behavior for state 0 is already completed, you should fill in the remaining states. In the code below, the reset behavior is implemented using if/else if statements. Since the state transitions only occur when scan is high, it is used as a condition for
the entire case block:

   always @(posedge clk, negedge rst_l) begin
      if (!rst_l) begin         
         state <= 0;
         keys  <= 0;
      end
      else if (scan) begin
         case (state)
           0:
             begin
                // Set the key values for this column:
                keys[4'h1] <= ~row[0];
                keys[4'h4] <= ~row[1];
                keys[4'h7] <= ~row[2];
                keys[4'h0] <= ~row[3];

                // Proceed to the next state:
                state    <= 1;                
             end
           1:
             begin
               // You do this
             end
           2: 
             begin
               // You do this
              end
           3:
             begin
               // You do this
             end
         endcase
      end
   end

Define col assignments

To complete the keypad design, the col signals need to be appropriately assigned. In each of the module’s states, only one of the col signals is assigned 0, while the others are all Z. This is achieved using assign statements with the conditional ? operator:

   assign col[0] = (state == 0) ? 0 : 1'bz;
   assign col[1] = (state == 1) ? 0 : 1'bz;
   assign col[2] = (state == 2) ? 0 : 1'bz;
   assign col[3] = (state == 3) ? 0 : 1'bz;

Since the col signals use Z values, they can only be assigned as top-level outputs. They can’t be safely assigned as registers in the clocked always block. The best practice is to use assign statements to define tri-state outputs separately from the clocked logic, as is done here.

Enter the lines above to complete the keypad module definition.

Testbench Design

A testbench is provided in the file src/testbench.v. Open the file and study its contents. The testbench simulates a succession of keypress events in the order: no key, then col 0: row 0, 1, 2, 3; then no key, followed by col 1: row 0, 1, 2, 3; and so on.

Simulating the Electrical Key Signals

To simulate these keypresses, we first define the depressed key position using variables row_pressed and col_pressed. When the keypad module scans across the columns, the electrical row signals should be 1111 except when the pressed column is being scanned. To simulate this electrical activity, we make conditonal assignments to row_wire as shown here:

   // Simulated row/col for keypress position:
   reg [3:0]  row_pressed;
   reg [3:0]  col_pressed;
   
   // Interface wires:
   wire [3:0]  row_wire;
   wire [3:0]  col_wire;
   wire [15:0] keys;
   wire        refresh;
   
   // The row wire can be pulled down only if the column wire matches
   // the column where the button is pressed:
   assign row_wire[0] = &(col_wire===col_pressed) ? row_pressed[0] : 1 ;
   assign row_wire[1] = &(col_wire===col_pressed) ? row_pressed[1] : 1 ;
   assign row_wire[2] = &(col_wire===col_pressed) ? row_pressed[2] : 1 ;
   assign row_wire[3] = &(col_wire===col_pressed) ? row_pressed[3] : 1 ;

Here, the triple-equal operator === is used to compare 4-value logic vectors. If the keypad output col_wire matches col_pressed in all positions, then row_wire is assigned to equal row_pressed. Otherwise row_wire is assigned 1111.

Keypress Timing

For each simulated keypress, we need to give the keypad module enough time to scan through all the columns and complete a full refresh. To achieve the required timing, we use a clock_divider in the testbench to generate a timing signal called refresh:

   // Timing to change key value every
   // refresh interval
   clock_divider #(.N(1_200_000)) clkdiv
       (.clk(clk),.rst_l(rst_l),.div_clk(refresh));
   

This clock divider’s timing parameter is set 1,200,000, equal to four times the keypad scan period. This means we’ll change the keypress after the keypad completes its scan of all four columns.

Key Sequence

Further down in the testbench, we see a nested pair of case statements:

       if (refresh) begin
          case (row_pressed)
            4'b1111: row_pressed <= 4'b1110; // first row_pressed
            4'b1110: row_pressed <= 4'b1101; // second row_pressed
            4'b1101: row_pressed <= 4'b1011; // third row_pressed
            4'b1011: row_pressed <= 4'b0111; // fourth row_pressed
            4'b0111: // After the last row, we change column and 
                begin  // start going through the rows again:
                   row_pressed <= 4'b1111; 
               
                   case (col_pressed)
                     4'bZZZ0 : col_pressed <= 4'bZZ0Z;
                     4'bZZ0Z : col_pressed <= 4'bZ0ZZ;
                     4'bZ0ZZ : col_pressed <= 4'b0ZZZ;
                     4'b0ZZZ : col_pressed <= 4'bZZZ0;
                   endcase // case (col_pressed)
                end
          endcase
       end

Whenever the refresh signal is 1, this code block is evaluated. It scans through each of the four row_pressed values. On the final row, it reverts to a no-keypress state, and advances col_pressed to the next column. Then the sequence of row_pressed states is repeated.

Notice that this simple state-machine uses Z values in its registers. This was not allowed in the keypad module design. Why? The answer is that four-value register assignments are allowed in simulation, but are not synthesizeable. In the keypad module, we could have used Z assignments within the state machine in the always block, and it may have simulated correctly, but in the end the implemented design would not work properly on the Basys3 board, all without any clear error messages.

Simulate the Design

Once you have completed the clock_divider and keypad modules, and understand the testbench design, run make to compile and simulate your design. In the file results.txt, you should see an output like this:

clk:       300002       col: zzz0       row: 1111       keys: 0000000000000000
clk:       600003       col: zz0z       row: 1111       keys: 0000000000000000
clk:       900004       col: z0zz       row: 1111       keys: 0000000000000000
clk:      1200005       col: 0zzz       row: 1111       keys: 0000000000000000
clk:      1500006       col: zzz0       row: 1110       keys: 0000000000000000
clk:      1800007       col: zz0z       row: 1111       keys: 0000000000000010
clk:      2100008       col: z0zz       row: 1111       keys: 0000000000000010
clk:      2400009       col: 0zzz       row: 1111       keys: 0000000000000010
clk:      2700010       col: zzz0       row: 1101       keys: 0000000000000010
clk:      3000011       col: zz0z       row: 1111       keys: 0000000000010000
clk:      3300012       col: z0zz       row: 1111       keys: 0000000000010000
clk:      3600013       col: 0zzz       row: 1111       keys: 0000000000010000
clk:      3900014       col: zzz0       row: 1011       keys: 0000000000010000
clk:      4200015       col: zz0z       row: 1111       keys: 0000000010000000
clk:      4500016       col: z0zz       row: 1111       keys: 0000000010000000
clk:      4800017       col: 0zzz       row: 1111       keys: 0000000010000000
clk:      5100018       col: zzz0       row: 0111       keys: 0000000010000000
clk:      5400019       col: zz0z       row: 1111       keys: 0000000000000001
clk:      5700020       col: z0zz       row: 1111       keys: 0000000000000001
clk:      6000021       col: 0zzz       row: 1111       keys: 0000000000000001
clk:      6300022       col: zzz0       row: 1111       keys: 0000000000000001
clk:      6600023       col: zz0z       row: 1111       keys: 0000000000000000
clk:      6900024       col: z0zz       row: 1111       keys: 0000000000000000
clk:      7200025       col: 0zzz       row: 1111       keys: 0000000000000000
clk:      7500026       col: zzz0       row: 1111       keys: 0000000000000000
clk:      7800027       col: zz0z       row: 1110       keys: 0000000000000000

(The console output is more detailed and could be useful for debugging if your design has a problem). Each line of output represents one period of scan, and you can see the col signal rotate through each of the four columns. After every four lines, the row signal changes. In the output shown above, we see “no press” followed by keys 1, 4, 7, and 0, corresponding to the left-most column on the keypad.

Synthesize, Implement and Program

Create a top Module

We’re going to use the Basys3 board’s center button as the reset for this design. Since the buttons are active-high, we need a top module to invert it and make an active-low reset signal. This type of interface layer is sometimes called a “wrapper” module, since it performs a minimal logic function.

module top
  (
   input         clk,
   input         rst,
   input  [3:0]  row,
   output [3:0]  col,
   output [15:0] keys
   );

   wire       rst_l;
   assign rst_l = ~rst;

   keypad #(.N(300_000)) kypd
     (
      .clk(clk),
      .rst_l(rst_l),
      .row(row),
      .col(col),
      .keys(keys)
      );
   

endmodule // top

Inspect the keypad.xdc File

The top-level I/O assignments are specified in the XDC file. Open it and observe the usual clk definition, and notice that the keys output vector is mapped to the 16 LEDs on the Basys3 board. The rst signal is mapped to the center button.

To interface with the PMod hardware, the row and col signals are mapped to the JA Header located on the upper left of the board. To test the keypad interface, you need to plug the keypad into the JA header as shown:

Header position for the keypad module.

Build and Test

Run make implement to synthesize, place-and-route, and generate a bitstream. Program the bitstream onto your Basys3 board and verify that each key lights up the corresponding LED on the board.

Turn in Your Work

Make a Video

After verifying correct function, take a short video demonstrating keys 0 through F, and upload it in Canvas to indicate that you are done.

Submit Design Files in Using git

Turn in your work using git:

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