This webpage describes a method for interfacing a 3x4 matrix keypad, like the one pictured in Fig. 1, with an RP2040. Other methods for building this interface exist (this would be a nice application for the PIO coprocessors, for instance). The particular method described on this page is the one that I think is most educationally useful in ECE 4760.
This webpage explains what is happening in this example code.
The 3x4 keypad has 7 pinouts. Four of these pins are connected to each row of the keypad, and the remaining three pins are connected to each column of the keypad. Pressing a key shorts one of these pins to another of the pins. For example, pressing the 1 key shorts the pin attached to row 1 to the pin attached to column 1.
Fig. 2 illustrates this internal layout, and indicates the particular RP2040 GPIO pins to which each pin is attached by default in the keypad demo code.
To scan the keypad for button presses, we will configure each of the GPIO's connected to the rows of the keypad (9-12, as shown in Fig. 2) as outputs, and we'll configure each of the GPIO's connected to the columns of the keypad (13-15, as shown in Fig. 2) as inputs. For each of the input pins, we will enable internal pullup resistors. To start, we'll set all the output GPIO's attached low.
Using the C SDK for the RP2040, all of these configurations look like the following:
#define BASE_KEYPAD_PIN 9
// Initialize the keypad GPIO's
gpio_init_mask((0x7F << BASE_KEYPAD_PIN)) ;
// Set row-pins to output
gpio_set_dir_out_masked((0xF << BASE_KEYPAD_PIN)) ;
// Set all output pins to low
gpio_put_masked((0xF << BASE_KEYPAD_PIN), (0x0 << BASE_KEYPAD_PIN)) ;
// Turn on pulldown resistors for column pins (on by default)
gpio_pull_down((BASE_KEYPAD_PIN + 4)) ;
gpio_pull_down((BASE_KEYPAD_PIN + 5)) ;
gpio_pull_down((BASE_KEYPAD_PIN + 6)) ;
The RP2040 allows for us to get the state of all of the GPIO ports using gpio_get_all()
. This returns a 32-bit unsigned int, each bit of which represents the state (high or low) of the associated GPIO port (bit 0 is GPIO 0, bit 1 is GPIO 1, etc.).
We scan the keypad by setting the GPIO's attached to the columns of the matrix high one at a time, and then reading the states of all 7 GPIO's that are attached to the keypad. For convenience, we'll call gpio_get_all()
, then shift the result right by 9 (so that the state of GPIO 9 is in bit 0), and then mask with 0x7f. This way, we're left with a 7-bit number, where each bit in that number represents one of the pins of the keypad. The first four are attached to the rows, and the next 3 are attached to the columns.
keypad = ((gpio_get_all() >> BASE_KEYPAD_PIN) & 0x7F) ;
Suppose that we set GPIO 9, and we are not presssing any keys. Then none of the column pins are shorted to the row pins, and they'd all remain pulled low. The bit associated with GPIO 9 will be high (since we set it high), but none of the others. So, when we scan the keypad, we get a value of 0b0000001, or 0x01. This is the scancode that corresponds to "no press."
Suppose instead that we set GPIO 9 high, and we press key 1. What value would we expect for the keypad? The bit associated with GPIO 9 will be high (since we set it high), and the bit associated with GPIO 13 will also be high, since pressing key 1 shorted it to GPIO 9. So, the binary value for keypad
will be 0b0010001, or 0x11 in hex.
Suppose that we had instead pressed 2. Then the bits of keypad
associated with GPIO's 9 and 14 would be high. We would get a scancode of 0b0100001, or 0x21.
And lastly, suppose that we had pressed 3. Then the bits of keypad
associated with GPIO's 9 and 15 would be high. We would get a scancode of 0b1000001, or 0x41.
If we had gotten any scancode other than 0x01, 0x11, 0x21, or 0x41, then that means the user had two keys pressed simultaneously. You could expand your keycodes to accomodate those cases, but for now we'll treat these as "invalid presses."
The table below enumerates these cases for row 0, and expands to include the scancodes for the other three rows of the keypad.
GPIO 15 | GPIO 14 | GPIO 13 | GPIO 12 | GPIO 11 | GPIO 10 | GPIO 9 | Control code | Scancode | Interpretation | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | 0x1 | 0x01 | No press | |
0 | 0 | 1 | 0 | 0 | 0 | 1 | 0x11 | Pressed 1 | ||
0 | 1 | 0 | 0 | 0 | 0 | 1 | 0x21 | Pressed 2 | ||
1 | 0 | 0 | 0 | 0 | 0 | 1 | 0x41 | Pressed 3 | ||
0 | 0 | 0 | 1 | else | Invalid. | |||||
0 | 0 | 0 | 0 | 0 | 1 | 0 | 0x2 | 0x02 | No press | |
0 | 0 | 1 | 0 | 0 | 1 | 0 | 0x12 | Pressed 4 | ||
0 | 1 | 0 | 0 | 0 | 1 | 0 | 0x22 | Pressed 5 | ||
1 | 0 | 0 | 0 | 0 | 1 | 0 | 0x42 | Pressed 6 | ||
0 | 0 | 1 | 0 | else | Invalid. | |||||
0 | 0 | 0 | 0 | 1 | 0 | 0 | 0x4 | 0x04 | No press | |
0 | 0 | 1 | 0 | 1 | 0 | 0 | 0x14 | Pressed 7 | ||
0 | 1 | 0 | 0 | 1 | 0 | 0 | 0x24 | Pressed 8 | ||
1 | 0 | 0 | 0 | 1 | 0 | 0 | 0x44 | Pressed 9 | ||
0 | 1 | 0 | 0 | else | Invalid. | |||||
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0x8 | 0x08 | Pressed * | |
0 | 0 | 1 | 1 | 0 | 0 | 0 | 0x18 | Pressed * | ||
0 | 1 | 0 | 1 | 0 | 0 | 0 | 0x28 | Pressed 0 | ||
1 | 0 | 0 | 1 | 0 | 0 | 0 | 0x48 | Pressed # | ||
1 | 0 | 0 | 0 | else | Invalid. |
Note that we can do a quick check to see whether or not any button is pushed by masking keypad
with 0x70, and checking if the result is nonzero. If the result is nonzero, it means one or more of the column GPIO's (15, 14, and/or 13) is pressed. We can then compare the full scancode against valid scancodes to determine which button was pressed.
Encoded, this sequence of steps looks like the following:
#define BASE_KEYPAD_PIN 9
#define KEYROWS 4
#define NUMKEYS 12
unsigned int keycodes[NUMKEYS] = { 0x28, 0x11, 0x21, 0x41, 0x12,
0x22, 0x42, 0x14, 0x24, 0x44,
0x18, 0x48} ;
unsigned int scancodes[KEYROWS] = { 0x01, 0x02, 0x04, 0x08} ;
unsigned int button = 0x70 ;
// Scan the keypad!
for (i=0; i<KEYROWS; i++) {
// Set a row high
gpio_put_masked((0xF << BASE_KEYPAD_PIN),
(scancodes[i] << BASE_KEYPAD_PIN)) ;
// Small delay required
sleep_us(1) ;
// Read the keycode
keypad = ((gpio_get_all() >> BASE_KEYPAD_PIN) & 0x7F) ;
// Break if button(s) are pressed
if (keypad & button) break ;
}
// If we found a button . . .
if (keypad & button) {
// Look for a valid keycode.
for (i=0; i<NUMKEYS; i++) {
if (keypad == keycodes[i]) break ;
}
// If we don't find one, report invalid keycode
if (i==NUMKEYS) (i = -1) ;
}
// Otherwise, indicate invalid/non-pressed buttons
else (i=-1) ;
After the above code executes, the value of i
will be -1 for no press or invalid press, and otherwise it will have the value of the key pressed (with 10 for * and 11 for #).
Each button on the keypad can be modeled as a spring-mass-damper system. When you press a key, you are closing a mechanical switch, and mechanical things bounce! As a consequence, the microcontroller might detect multiple button presses each time you press/release a key. In order to prevent this, you must implement a debouncing state machine. An example of one such state machine is illustrated in Fig. 3.
We start in the Not pressed
state and repeatedly scan the keypad for a button press. For as long as that scan returns no press or invalid press, we remain in the Not pressed
state.
When the scan returns a valid keypress, we transition to the Maybe pressed
state and store the value of that keypress in a variable called possible
. We then scan the keypad again, and compare the keycode to the value of possible
. If they differ, then the key has bounced to a different state and we transition back to Not pressed
. If they are the same, then we assume the key state to be stable and we transition from Maybe pressed
to Pressed
. If you want for a single event to occur each time a button is pressed, then that event should be triggered on this transition.
Once we're in the Pressed
state, we continue to scan the keypad. For as long as the keycode matches the value that we stored in possible
, we remain in this state. If the value differs, we transition to Maybe not pressed
.
In Maybe not pressed
, we scan the keypad again. If the keycode matches possible
, we transition back to Pressed
. Otherwise, we transition to Not pressed
.