Keypad display

Pmod Keypad Peripheral First Look

For this next project I am going to take some time to learn and explore another peripheral. Specifically the Pmod Keypad. If you are a reader of this blog you may remember that earlier I discussed the Basys 3 Pmod pack. The Pmod pack includes a total of five peripherals to explore. I purchased the Pmod pack along with my Basys 3 so that I would have plenty of peripherals to keep me busy.

Image of Basys 3 Pmod Pack
Basys 3 Pmod Pack

The keypad is just one of the five peripherals the come with the Basys 3 Pmod pack. I chose it as the first Pmod to explore because it uses what Digilent calls a GPIO interface. Which is a fancy way of saying that we don’t need to use complex communication protocols in order to interface with it. Only simple FPGA inputs and outputs, and a little glue logic.

Image of Pmod keypad
Pmod Keypad

Understanding the Basic Keypad Hardware Design

The keypad is a simple peripheral. And conveniently Digilent provides both a datasheet and schematic for the keypad. I like to start by reading datasheets and schematics. It helps me understand the hardware better and that makes conceptualizing the interface requirements and approach a little bit easier.

As with all of the Pmod add-ons, Digilent also provides a Resource Library with sample code in Verilog, VHDL and Arduino. Using sample code is too easy. And making it too easy on myself means I won’t truly learn. So I won’t use the sample code, other than to refer to it if necessary for troubleshooting.

The keypad module consists of 16 momentary pushbutton switches wired in a matrix of 4 columns and 4 rows. Internally, the row lines are pulled up by a large (10K ohm) resistor to logic high. The column lines each have a 470 ohm current limiting resistor to protect the Artix-7 inputs from overload.

The first column line is wired in parallel to each of the four switches in the first column – 1, 4, 7 and 0. Pressing a button connects the corresponding column and a row. That is, pressing the switch labeled 1 connects column 1 and row 1 together. By using an Artix-7 output to pull one of the column lines to logic low, a circuit can tell which button(s) are pressed by sampling which row(s) are now also at logic low.

How does this work? Remember that the row lines are normally pulled high by the 10K resistor connected to the voltage supply. With a column line pulled low, a button press effectively connects the corresponding row line to ground. Since the column line has only a 470 ohm resistor the column can sink more current than the row can source through the 10K resistor. The result is that the row line for the button is pulled to logic low.

Remember that the row lines are normally pulled high by the 10K resistor connected to the voltage supply. With a column line pulled low, a button press effectively connects the corresponding row line to ground.

Repeat that process for each of the remaining columns and a successful scan of the keypad is now complete!

Starting The Verilog Design

OK, with the hardware side understood let’s move on to the interface circuit. I want to design a simple circuit for interfacing to the keypad. As with all of the circuits I’ve designed so far I hope to reuse this circuit in future projects.

For constraints this project will use the following signals that we’ve seen before: clk, seg, and an. We will also use a new block of signals, the JA port. The JA port represents the Pmod header labeled JA. Of the 12 pins on the JA port there are only 8 that we can use, meaning that are not Vcc or GND. Those 8 pins are mapped via constraints to JA[7:0]. The complete constraints file is below.

## This file is a general .xdc for the Basys3 rev B board
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property PACKAGE_PIN W5 [get_ports clk]
	set_property IOSTANDARD LVCMOS33 [get_ports clk]
	create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk]

##7 segment display
set_property PACKAGE_PIN W7 [get_ports {seg[0]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}]
set_property PACKAGE_PIN W6 [get_ports {seg[1]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[1]}]
set_property PACKAGE_PIN U8 [get_ports {seg[2]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[2]}]
set_property PACKAGE_PIN V8 [get_ports {seg[3]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[3]}]
set_property PACKAGE_PIN U5 [get_ports {seg[4]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[4]}]
set_property PACKAGE_PIN V5 [get_ports {seg[5]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[5]}]
set_property PACKAGE_PIN U7 [get_ports {seg[6]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {seg[6]}]

set_property PACKAGE_PIN U2 [get_ports {an[0]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {an[0]}]
set_property PACKAGE_PIN U4 [get_ports {an[1]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {an[1]}]
set_property PACKAGE_PIN V4 [get_ports {an[2]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {an[2]}]
set_property PACKAGE_PIN W4 [get_ports {an[3]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {an[3]}]

##Pmod Header JA
##Sch name = JA1
set_property PACKAGE_PIN J1 [get_ports {JA[0]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[0]}]
#Sch name = JA2
set_property PACKAGE_PIN L2 [get_ports {JA[1]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[1]}]
#Sch name = JA3
set_property PACKAGE_PIN J2 [get_ports {JA[2]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[2]}]
#Sch name = JA4
set_property PACKAGE_PIN G2 [get_ports {JA[3]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[3]}]
#Sch name = JA7
set_property PACKAGE_PIN H1 [get_ports {JA[4]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[4]}]
#Sch name = JA8
set_property PACKAGE_PIN K2 [get_ports {JA[5]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[5]}]
#Sch name = JA9
set_property PACKAGE_PIN H2 [get_ports {JA[6]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[6]}]
#Sch name = JA10
set_property PACKAGE_PIN G3 [get_ports {JA[7]}]
	set_property IOSTANDARD LVCMOS33 [get_ports {JA[7]}]


## Configuration options, can be used for all designs
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]

The top-level module for this project is going to be intentionally simple. The top-level module takes our input/output signals and connects them to the keypad circuit and from there to the seven-segment display.

You may notice in the code below that I am using a new keyword inout in the module signal declarations on line 4. The inout keyword lets Verilog know that the signals in the JA port act as both inputs and outputs. Why both inputs and outputs? Within the JA port, pins 0 through 3 are column pins which we will use as an output. Pins 4 through 7 are row pins which we will use as inputs.

// top-level module
module keypad_top(
    input clk,
    inout [7:0] JA,
    output [6:0] seg,
    output [3:0] an
    );

    // interconnect wiring
    wire [3:0] key;
    
    // only using 1 digit of sseg display
    assign an = 4'b1110;
        
    // instantiate the keypad circuit
    pmod_keypad keypad(
        .clk(clk), 
        .col(JA[3:0]), 
        .row(JA[7:4]), 
        .key(key)
    );

    // instantiate the display circuit     
    sseg_display display(
        .hex(key),
        .seg(seg)
    );
    
endmodule

On line 13, you can see that I set an[0] to logic low to enable only the first digit of the seven-segment display. Remember from the post on the seven-segment display that the inputs to all four digits connect in parallel. If I enabled all 4 digits at the same time it could exceed the current draw limits of the Artix-7 outputs and cause damage to the hardware.

Defining the Keypad Interface

Then starting on line 16 you see the instantiation of the circuit for this project, called pmod_keypad. The pmod_keypad module takes as input the clk and JA signals. Notice on lines 18 and 19 how I slice the bits of the JA signal with some bits going to col and others to row. Finally, it outputs a 4-bit scan code, key, that represents the button press.

Notice on lines 18 and 19 how I slice the bits of the JA signal with some bits going to col and others to row.

You might reasonably ask why I am doing that. I have not yet written the pmod_keypad module. How can I write the code to instantiate a module that I have not yet written?

This is a software development pattern that I frequently use. I want the interface to be clear and easy to use. So I will start by writing the definition of a sensible interface, thinking about the required inputs and the outputs. I look at how that interface connects and interacts with other interfaces at the top level. Once that is complete, then I flesh out and implement to that design.

Is this outside-in approach better than an inside-out approach? I cannot say for certain, and reasonable minds may disagree. But in the long run I find that you, and more importantly others, spend much more time working with the interface than on the implementation. Given that, I find it valuable to ensure that I start by thinking carefully about the former.

On line 24 you can see that we are re-using the sseg_display circuit from an earlier post. The input to sseg_display is our key code from pmod_keypad and the output connects directly to the seven-segment output seg.

Implementing the Keypad Module

Ok, now we have to proceed to actually implement pmod_keypad. The code is lengthy but not very difficult. Let’s take a look. As we saw earlier the inputs are clk , row and col. And the output is key.

We don’t want to sample the pushbuttons on every clock tick. Why is that? Momentary pushbuttons are electrically noisy due to something called contact bounce. In a future post I will talk about ways to manage bounce. For now the plan is to sample each column only once every few milliseconds. And I will wait 1 micro second after pulling down a column line. That should provide enough time for the electrical signal on the row line to settle to a steady state. If not, since we are using localparam values I can easily tune the delays.

Having a millisecond resolution delay sounds like we need a counter/timer circuit. As it happens we created one of those in creating a configurable counter. We will use that. At line 10 I define a constant for the number of bits in the counter and at line 22 I instantiate the counter.

The wire (techically a bus) I call key_counter is just an interconnect between the counter and the rest of the circuit. The register rst resets our counter as needed. The output of the counter connects to key_counter.

// circuit for interfacing with Pmod keypad
module pmod_keypad(
    input clk,
    input [3:0] row,
    output reg [3:0] col,
    output reg [3:0] key
    );

    // counter bits
    localparam BITS = 20;
    
    // number of clk ticks for 1ms: 100Mhz / 1000
    localparam ONE_MS_TICKS = 100000000 / 1000;
    
    // settle time of 1 us = 100Mhz / 1000000
    localparam SETTLE_TIME = 100000000 / 1000000;
    
    wire [BITS - 1 : 0] key_counter;
    reg rst = 1'b0;
    
    // instantiate a 20-bit counter circuit
    counter_n #(.BITS(BITS)) counter(
        .clk(clk),
        .rst(rst),
        .q(key_counter)
    );
    
    // check on each clock
    always @ (posedge clk)
    begin
        case (key_counter)
            0:
                rst <= 1'b0;
                
            ONE_MS_TICKS:
                col <= 4'b0111;

            ONE_MS_TICKS + SETTLE_TIME:
            begin
                case (row)
                    4'b0111:
                        key <= 4'b0001; // 1
                    4'b1011:
                        key <= 4'b0100; // 4
                    4'b1101:
                        key <= 4'b0111; // 7
                    4'b1110:
                        key <= 4'b0000; // 0
                endcase
            end
            
            2 * ONE_MS_TICKS:
                col <= 4'b1011;
            
            2 * ONE_MS_TICKS + SETTLE_TIME:
            begin
                case (row)
                    4'b0111:
                        key <= 4'b0010; // 2
                    4'b1011:
                        key <= 4'b0101; // 5
                    4'b1101:
                        key <= 4'b1000; // 8
                    4'b1110:
                        key <= 4'b1111; // F
                endcase
            end

            // 3ms
            3 * ONE_MS_TICKS:
                col <= 4'b1101;

            3 * ONE_MS_TICKS + SETTLE_TIME:
            begin
                case (row)
                    4'b0111:
                        key <= 4'b0011; // 3
                    4'b1011:
                        key <= 4'b0110; // 6
                    4'b1101:
                        key <= 4'b1001; // 9
                    4'b1110:
                        key <= 4'b1110; // E
                endcase
            end
            
            // 4ms
            4 * ONE_MS_TICKS:
                col <= 4'b1110;
            
            4 * ONE_MS_TICKS + SETTLE_TIME:
            begin
                case (row)
                    4'b0111:
                        key <= 4'b1010; // A
                    4'b1011:
                        key <= 4'b1011; // B
                    4'b1101:
                        key <= 4'b1100; // C
                    4'b1110:
                        key <= 4'b1101; // D
                endcase

                // reset the counter                
                rst <= 1'b1;
            end     
        endcase
    end
    
endmodule

The always block is where it gets interesting. The always block activates on every clk tick. On each tick I look at the value of key_counter. When key_counter is 0 I set rst to 0. It will be clear why that is important later.

When the key_counter is 100,000 (equivalent to 100,000,000Mhz / 100,000 ticks or 1ms) I set the col bits to 4’b0111. This sets col 1 (bit 4) to logic low and leaves the other columns at logic high. Then a short time later at 100,008 I use a case statement to check the states of the row bits. If the first bit of row is low (4’b0111) then the key code is set to 1. That is because the pushbutton at column 1 and row 1 on the kepad is labeled 1. Repeat this procedure for each of the remaining 3 rows, decoding keypresses appropriately, and the scan of column 1 is complete.

This process is repeated for columns 2 through 4 at time values of 200, 300 and 400,000 respectively. And finally, note that on line 105 after the last row scan of column 4, we reset counter to 0 by setting rst to logic high.

Wrapping it up

I fine the code a little cumbersome. For example, the clauses in the case statement are very similar. It would be nice to collapse that logic down to just two states. Wait for 1ms, then pull down a column, then check the rows. Update the column and repeat.

I will plan to look that code over to see if I can find a cleaner and simpler solution. But sometimes it is important to first make it work, then make it work better/faster. And as long as our interface is reasonable we can optimize/re-implement the internals without breaking circuits that use the module.

“First make it work, then make it faster”

–Brian Kernighan

For now this circuit does the job, behaving as expected. When you press a button on the keypad the corresponding button label appears on the seven segment display.

Keypad display
Keypad display

That’s it for today! The source code for this project is available on github. If you have feedback, a question or a suggestion, please leave a comment!

Discover more from FPGA Coding

Subscribe now to keep reading and get access to the full archive.

Continue reading