We all have been there. We ran out of GPIO pins on our favorite microcontroller. Switching to a type with more pins wasn’t possible. Perhaps we were space- or cost-constrained. Or perhaps we didn’t want to port the code to a new microcontroller. Or perhaps we wanted the engineering challenge to make it work with fewer GPIO pins.
I recently encountered this situation. I had six switches but only two GPIO pins left. Another requirement was that I wanted to be able to detect not just single button presses but also two buttons pressed simultaneously. It was for a handheld device. Since we have two thumbs, it is very easy to press two buttons at the same time. For example, in a D-Pad with four buttons, up, down, left, and right, one can input the diagonal directions by pressing two adjacent buttons at the same time. I did not need to detect more than two buttons being pressed at the same time.
I found a new solution that I haven’t seen published before. In the hope that it may help some, here is a review of the available options, and then my novel solution for it.
One important aspect of my application was how much flash memory the multiplexing scheme required. I work predominantly with small microcontrollers, for example, the ATtiny series, where we may only have 1kB or 2kB of flash memory available. A solution that requires less flash memory, everything else being equal, is preferable.
Existing Solutions
I2C
You can use an I2C port expander chip, such as PCF8574 or MCP23008. You will need two pins on the microcontroller to establish an I2C interface. These port expanders provide 8 additional GPIO pins. There are also chips with 16 ports, for example, the MCP23017. Additionally, you can hook several of these chips to the same I2C lines by configuring different addresses for them.
This means this solution is great if you need a lot of additional GPIO pins. However, it comes at a price. You need an additional chip; the speed of the I2C bus is typically just 100kHz or 400kHz, and you need to use an I2C library, which requires a good amount of flash memory. I measured on my system that it required 190 bytes of additional flash memory to read an 8-bit port expander. I am using the software I2C library in BASCOM AVR. If your microcontroller has a hardware I2C, this may look different. Or if you already use the I2C library for other functionality in your system, you would not even need two pins. You could connect the port expander to an existing I2C bus, and the incremental flash memory overhead may be much smaller.
For my case, this did not apply. The added flash memory just to service an I2C port expander was not worth it, considering also that it is relatively slow, and it complicates the hardware with an additional chip.
Shift Register
Another solution requiring an additional chip is to use shift registers. Common chips used are 74HC165 (8 additional inputs) or 74HC595 (8 additional outputs). This already reveals one disadvantage of this solution. You cannot mix input and output pins as you can with an I2C expander chip. Depending on the chip, you either get 8 additional input pins or 8 additional output pins. Not both. You can daisy-chain the same type of chip if you need more than 8 input pins or more than 8 output pins.
A shift register needs 3 pins of the microcontroller. One pin is to latch the values of the inputs into the internal register. The second pin is the clock signal with which one shifts the register out, one bit at a time. And the third pin is the pin for the data, where one reads the bit values of the shift register.
There are techniques to reduce this to 2 pins and even just 1 pin. To reduce it to 2 pins, the clock and latch pins are combined with an RC-delay element. Fast clock pulses reach the clock input but not the latch input. Longer pulses do reach the latch input. To reduce it to just 1 pin, one connects serial data out of the shift register with a resistor to the clock/latch line. The pin that drives the clock/latch is for each step switched from output to input to read the data line, then switched back to output to advance the clock by one step, switched back to input to read, etc.
The flash memory requirements of a shift register are less than those of an I2C port expander because there is no overhead of the I2C protocol. No need to address a chip on the bus, etc. Simply send the latch signal and then a loop to clock the 8 bits out of the shift register. It is also considerably faster than the I2C solution. The 1-pin solution is a bit slower and requires more flash memory. But this solution still requires an additional chip, which I wanted to avoid, if possible.
Matrix
To avoid having to use an additional chip, one can arrange switches in a matrix and then scan rows or columns to determine which switches are closed. The problem with a matrix is that for a relatively small number of switches, the pin savings are minor. For example, for my 6 switches, I could use a 2×3 matrix, which requires 5 pins on the microcontroller instead of 6 pins if the switches were directly connected to the microcontroller. That is a saving of just 1 pin. For my situation, not enough. I only had 2 pins available.
A matrix is a good option if you have a lot more switches, say 16. Then a 4×4 matrix will save you 8 pins. A matrix can reliably detect two simultaneous button presses, but for more simultaneous presses, diodes are necessary to disallow parasitic paths through the matrix.
Charlieplexing
Charlieplexing is often used to multiplex LEDs, but it can also be used on the input side with switches. The pin savings are bigger for Charlieplexing than for a regular matrix. With 3 pins, one could Charlieplex 6 switches. The relationship is n(n-1), with n the number of pins available.
I only have 2 pins available, and thus, Charlieplexing wasn’t a viable solution for me. Another problem with Charlieplexing is that one needs to add diodes in series with the switches, and it is not possible to reliably detect simultaneous button presses. This is why Charlieplexing isn’t often used on the input side.
Digital-to-Analog
If the available pins on the microcontroller can be configured as analog input pins, then a different way to multiplex inputs can be deployed. Each switch and switch combination is associated with a particular voltage level. The microcontroller reads that voltage with an ADC, and the ADC value gets converted to determine which buttons were pressed. One advantage of this solution is that only one pin is necessary to service several switches.
There are different ways to generate the switch-dependent analog voltage. One is to use a digital-to-analog converter chip, such as the DAC0808. One advantage of using a DAC is that each bit of the DAC input can be directly associated with a switch, and thus, the decoding on the microcontroller side is trivial. However, this only works for the most significant bits. The least significant bits can easily be altered by noise. It also requires an additional chip.
But there are simpler ways to produce a switch-dependent voltage.
Resistive Ladder
A common way is to set up a resistive voltage divider (two resistors in series between power and ground) and then have the switches alter one of the resistors in the divider by, for example, connecting additional resistors in parallel and thus altering the resistive divider, changing the output voltage accordingly.
For three switches, this can be done with 4 resistors, as shown below, and thus it is hardware-wise a simple solution not requiring an additional chip.

| Switches Closed | ADC [V] |
|---|---|
| none | 0 |
| S3 | 0.612 |
| S2 | 0.968 |
| S2, S3 | 1.268 |
| S1 | 1.500 |
| S1, S3 | 1.670 |
| S1, S2, S3 | 1.788 |
If no switch is closed, the ADC pin is pulled down by R1 to ground or zero voltage. Closing S1 produces 1.5V on the ADC pin, etc. The voltages produced are shown in the table below:
The smallest gap is between 1.670V and 1.788V. We would place the boundary in the center, which would produce a noise margin of just 59mV. You can try to find better resistor values, staying within the E12 number series. I didn’t exhaustively search all possibilities, but I think the values I found are close to optimal.
Resistor Values
Resistors are manufactured with particular values only. The most popular values are given by the E12 series: 10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82. That means one can purchase resistors with 10kΩ, 12kΩ, etc. For each decade, up or down, the values repeat, multiplied or divided by multiples of 10: 1kΩ, 1.2kΩ, etc.
The flash memory required was 162 bytes, which was less than the 190 bytes of the I2C solution. It is also faster, requiring just one ADC read and then an IF-ELSEIF-ELSEIF-…-ELSE sequence to separate 7 value ranges. For three switches, we need seven intervals, one for no buttons pressed, 3 for each of the three buttons individually, and then another 3 for the three possible combinations of two simultaneous buttons. With two such analog inputs, I can service six switches. One can do this with more than 3 switches per pin, but then the noise margins become progressively smaller. For three buttons on one pin, the noise margin I could achieve was 60mV with values from the E12 number series.
I like this solution, but I wonder if this could be improved. Could this be made to require less flash memory? Could the noise margin be improved? The inherent problem of such a parallel resistive ladder is that the output voltage changes non-linearly. This requires setting up custom intervals in the code to detect the various switch positions, and it is non-ideal in terms of noise margins.
If we assume a 3V system, then we need 6 equally spaced intervals to distinguish between 7 values. This would result in 250mV noise margins, not the 60mV we have with the voltage divider solution. Equally spaced intervals would also make the decoding simpler because the most significant bits of the ADC output value would directly define the buttons pressed.
So why not do something like this:

Using equal resistor values in series, fed by a current source, will produce equally large gaps between voltages. This configuration is used in old analog music keyboards. But it has a disadvantage that makes this solution unusable for my application. One cannot detect simultaneous button presses. If multiple buttons are pressed, the highest button will set the output voltage.
Novel Resistive Solution
There is a way to achieve both an improvement in the noise margin and an easier decoding of switch positions with the use of one or two more resistors. With one additional resistor, I get a noise margin of 124mV and with two additional resistors, meaning 6 resistors for 3 switches, I get 150mV noise margins. They both require just 94 bytes of flash memory to be decoded, which is a good amount less than the 162 bytes of the resistive ladder above. For just one more resistor, I call this a win. The circuit diagram of the 5-resistor solution is this:

| Switches Closed | ADC [V] |
|---|---|
| none | 0 |
| S2 | 0.634 |
| S2, S3 | 0.882 |
| S3 | 1.395 |
| S1, S2 | 1.746 |
| S1, S3 | 2.289 |
| S1 | 2.842 |
Why is this better? It is better because we are changing the resistances on both sides of the voltage divider. This allows us to create much more uniform intervals, increasing the noise margin, as well as simplifying decoding.
When no switch is closed, R1 pulls down the ADC to ground. When S1 is closed, we do essentially the same as before; we set up a two-resistor voltage divider, and the voltage at ADC will be 2.842V. But for S2 and S3, we connect another voltage divider (two resistors instead of just one) in parallel to the first, which gives us more ways to alter the resulting voltage.
The smallest gap is between 0.634V and 0.882V. If we put the boundary in the center, it results in 124mV worst-case noise margin, which is more than twice the 59mV from the common resistive ladder. I am ignoring resistor tolerances, but using 1% tolerance resistors will only reduce the worst case by a few millivolts.
We also get a decoding benefit. Let us assume we have a 10-bit ADC. The three most significant bits change at the following ADC values and voltages, assuming a 3V power supply.
| 3 MSB bits of ADC | ADC decimal | Voltage [V] | Divider [V] | Switches |
| 000 | 0 | 0 | 0 | none |
| 127 | 0.372 | |||
| 001 | 128 | 0.375 | 0.634 | S2 |
| 255 | 0.748 | |||
| 010 | 256 | 0.751 | 0.882 | S2, S3 |
| 383 | 1.123 | |||
| 011 | 384 | 1.126 | 1.395 | S3 |
| 511 | 1.499 | |||
| 100 | 512 | 1.501 | 1.746 | S1, S2 |
| 639 | 1.874 | |||
| 101 | 640 | 1.877 | 2.289 | S1, S3 |
| 767 | 2.249 | |||
| 110 | 768 | 2.252 | ||
| 895 | 2.625 | |||
| 111 | 896 | 2.628 | 2.842 | S1 |
| 1023 | 1.000 |
You can see that the voltages that define the bit patterns of the 3 most significant bits nicely separate the voltages produced by the switches. Here, we cannot arbitrarily define the boundaries of the buckets. They are given by the bit patterns. This reduces the worst-case noise margin a little bit to 117mV.
To deduce from the ADC value which switches are closed only requires us to consider the 3 most significant bits. We can do this by shifting the ADC value to the right by 7 bits. Bit shifting is an efficient task in a microcontroller.
Unfortunately, these 3 bits do not directly correspond to the three switches. But we can fix this with a small 8-byte-long lookup table that translates these 3-bit ADC values to the switch bit pattern.
I used this configuration twice, for two pins, to make my six switches only use 2 GPIO pins of the microcontroller. It is possible to reuse resistors R2-R5 for the second pin and only use a new R1 for the second analog input pin, which means this solution will require only 6 resistors for 6 switches. Reusing resistors this way will reduce the noise margins a bit, but everything else continues to work as intended.
This means that, for my six switches, two GPIO pins, and the requirement to distinguish between single and dual button presses, my novel resistive solution requires only one resistor per switch, it requires less flash memory, and it has a larger noise margin by a factor of two. To put the theory to the test, I quickly connected resistors and switches using the miniTesla prototyping system to verify that it was working.

Novel Diode Ladder
We can do even better. I was not entirely happy with the fact that one has to decode the 3-bit pattern into the actual switch states with a lookup table. With a diode ladder, we can create binary-weighted voltage ranges with more or less fixed values. See the circuit diagram below.

The forward voltages of the diodes define voltages that get added to the ADC output when a button is pressed, and a diode is shorted. The BAS40 Schottky diode has a forward voltage of about 0.37V at half a milliamp. The 1N4148 silicon diode has a forward voltage of above 0.7V, which is about twice as large. And two of these diodes in series produce a forward voltage drop of around 1.5V. This is a binary-scaled ladder. The 3 most significant bits of the ADC value will directly correspond to the switch states.
While this works, there are a few things to consider. The forward voltages of diodes are temperature dependent with -2mV/°C. If this needs to work over a wide temperature range, it is not a good option because the changes can result in misidentification of the switch states. Also, changes in supply voltage add to this problem. However, AVR microcontrollers typically stop working properly at 2.7V. Two fresh alkaline batteries produce about 3.1V. Thus, the supply voltage range, from 3.1V to 2.7V, is less of a problem than the temperature drift. One could add logic to the code to account for some of these variations, but that defeats the purpose of creating a solution with minimal flash memory usage. Another improvement can be achieved by adding a negative temperature coefficient (NTC) resistor parallel to R. An NTC with 1kΩ at room temperature should work. We can then increase R to 470Ω to get closer to the 330Ω without NTC at room temperature. When the temperature increases and the forward voltages get smaller, the NTC lowers its resistance, which will cause the current to increase, which will increase the forward voltages of the diodes. The compensation will not be perfect, but it will help to extend the temperature range at which it is working.
Even if your requirements are different, I have laid out the various options you have to multiplex inputs, including their advantages and disadvantages. This information will allow you to choose the scheme most suitable for your situation.
ADVERTISEMENT



