Table of Contents
Introduction
A while back, I attached a 4x4 keypad matrix to a microcontroller, and it took up eight GPIO inputs. This time around, I noticed I had a load of buttons from an unused project, and decided to rethink things, and ended up building a little custom keypad for the Pi Pico (it can be done with any microcontroller). This could be useful for robotics, gaming, all sorts of things really.
It was a quick weekend project, and I used the parts I had.
The usual problem to solve, as hinted above, is that many buttons can consume a lot of GPIO! Various solutions exist, and (in my opinion) one of the best approaches is to use a GPIO expander chip, because it also eliminates the need for repeated keyboard scanning. Many GPIO expanders have an interrupt output, so all the microcontroller needs to do is enable a GPIO interrupt and in the event that there’s a button change, simply read in the inputs using I2C or SPI.
In the absence of a GPIO expander, there are a few reduced-GPIO key scanning options, and one method is to use a parallel-input-to-serial-output shift register, and clock in all the inputs.
I didn’t have such a shift register, but I had the complete opposite; a serial-input-to-parallel output shift register. I decided to use that, and learned along the way that it’s actually not a bad solution if scanning is acceptable (but there’s a way to use the same circuit and partially eliminate scanning too, if desired). The '595 is a useful problem-solver.

74HC595 Explained
There are three parts to the ‘595. The blue part has three inputs, which are initially set low by the microcontroller. Set pin 10 high to keep the shift register out of reset. The other two inouts are a clock and data pair; set the SER data line, and pulse SRCLK high and low to push the bit into the shift register. On the right side, you could attach another shift register if desired.

Once the shift register contains the desired logic levels, the middle portion of the ‘595 can be used, by pulsing RCLK high and low. It will store the data into output registers, and you’ll see that on pins QA to QH, provided pin 13 (output enable) is wired low.
When joining up multiple ‘595s in a chain, usually pins 10, 11 and 12 are all wired together across all chain.
Circuit
The result of the circuit and code is to apply a voltage to each push-switch, one at a time, and let the microcontroller sense when the common wire, SW_DATA, goes high. That will only occur if a user presses a button at the time when a voltage is being applied to the switch, therefore rapid scanning is needed, which is not always a major issue with microcontrollers, but it depends on the project if this is convenient or not.
To perform the scanning, three microcontroller output signals are needed for the shift register, but I used four to simplify the coding a little bit.
A weakness in this circuit is that there is nothing to pull the SW_DATA signal sharply down, so a low-value resistor (such as 2.2k as I’ve used) is needed, otherwise the settling time is quite long. If you see erroneous simultaneous button-presses whenever a button is pressed, then the cause is most likely too high a resistor value there.

Code
I wrote a very short Pi Pico demo program, that scans the buttons every 100 msec (in practise you’d want to set it to a faster rate), and displays via USB UART two 8-bit hexadecimal values, indicating button(s) pressed.
The way the code works, is that it sets the SER_DAT line high, and then pulses the shift register clock once, then sets SER_DAT low for the rest of the entire scan. That single high value will pulse its way through the shift register on 15 more clock pulses. After each clock pulse, the R_CLK signal is pulsed too; that registers the shift register value to the outputs, therefore driving just one switch line high. After a very short delay (couple of hundred nsec) the microcontroller reads the common switch line, and then the cycle repeats for the remainder switches.
LED Bonus
One of the key things about parallel-output registers, is that they can be used as outputs : ) I decided to append another 74HC595 (these ICs cost about $0.10) and use it for up to 8 LEDs.

The code (viewable/downloadable here) doesn’t change much. After all the keys have been scanned, the desired LED settings are clocked in, and then padded with enough zero bits to push past the first shift registers.
Because all the 595’s are chained and I’d joined all the RCLK signals together, it is not possible to solely do a key scan without affecting the LED outputs. However, in practical terms, it makes no difference, because the key scan happens within microseconds, thus leaving plenty of time until the next key scan, for the LED output registers to hold their desired levels.
Building It
I used PB86 switches that I had purchased a while back, they are really convenient because they fit 0.1” perfboard if a couple of tiny plastic locator pegs are snipped off. The odd button in the photo is there for a purpose (I will use it to map different key layouts, if the user holds down that button and presses another).

This project is very easy to build because triple ‘595 boards are available:

(Image source: Google Images)
I didn’t have such a board, so took the longer route. The underside is very messy.

Anyway, this was just supposed to be a quick experiment, and it seems to work fine. The code is in the simple_keypad project repository, and it uses the Pi Pico C/C++ SDK, but it's so simple, that it would be easy to port to Arduino or other platforms.
Thanks for reading!