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 = 0ZZZcol = Z0ZZcol = ZZ0Zcol = ZZZ0After 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_dividerThe 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;
endmoduleThe 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
endcol 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
endWhenever 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 // topkeypad.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.
gitTurn in your work using git:
git add src/*.v *.v *.rpt *.txt *.bit
git commit . -m "Complete"
git push origin main