The Digilent Keypad PMOD is a simple grid of buttons:
The keypad’s physical circuit consists of a switch matrix organized into four rows and four columns:
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.
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:
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).
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:
1
– logic TRUE, electrical HIGH0
– logic FALSE, electrical LOWX
– logic “invalid”, electrical unkownZ
– logic “undefined”, electrical high-impedance, i.e. disconnectedIn 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.
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:
col = 0ZZZ
col = Z0ZZ
col = ZZ0Z
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.
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.
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.
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.
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.
keypad
ModuleCreate 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
.
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
.
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
col
assignmentsTo 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.
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.
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
.
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.
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.
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.
top
ModuleWe’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
keypad.xdc
FileThe 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:
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.
After verifying correct function, take a short video demonstrating keys 0 through F, and upload it in Canvas to indicate that you are done.
git
Turn in your work using git
: