Introduction

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.


missing
Fig. 1: 3x4 keypad

Keypad Layout

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.


missing
Fig. 2: Internal layout for keypad. **The resistors are external!!**

Reading the keypad

Configuring GPIO's

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)) ;

Scanning for presses

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 #).

Debouncing

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.

missing
Fig. 3: Debouncing state machine

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.