WaynePlexing Push Buttons (or, read 6 buttons using only 3 I/O pins)

I recently needed to create simple, microcontroller-based circuit that could scan a few push button switches and then output a code over a serial link when one was pressed.  I first wondered about using an ATTiny10, as they're cheap and, thanks to my custom, C-based IDE and progammer, easy to work with.  However, as the ATTiny10 only has 4 available I/O lines, this seemed like it would limit me to having, at most, three buttons connected, as I needed to devote one I/O line to act as the serial output.  So, I wondered if there was some clever way I could connect more than 3 pushbuttons to only 3 I/O lines.

Most engineers are familiar with Charlieplexing, which is a method which can drive, for example, 6 LEDs using only 3 I/O lines.  So, I wondered if there was some equivalent way to scan push button switches.  After some web searching, I found a few references, such as the one, that required the addition of a diode in seres with each switch, but that seemed like too much trouble to me.  I also found methods that used a resistor ladder in combination with a microcontroller's A/D converter to read push buttons.  However, I rejected taking that approach, having once purchased an Arduino LCD shield that used this technique, but which failed to work properly because the button contact resistance made deboucing very difficult and subject to change over time as the buttons aged.  Then, after pondering the problem over a long lunch, an idea popped into my head and I drew up the following diagram:

Note: the pattern of push buttons vs I/O pins follows a triangular number sequence, so it can be extended to handle 10, 15, 21, etc. pushbuttons using, respectively, 4, 5, or 6 I/O pins.  In a pinch, you can also read the state of 3 push buttons using only 2 I/O pins, but I'll let you work out the circuits for these other examples.

To test my scheme, I used a solderless breadboard to hack together a quick prototype, like this:

How to Detect Button Presses

The secret to reading out the button states without having to resort to adding a diode to each switch lies in how the I/O pins are used.  The first 3 buttons, buttons 1-3, can be read by configuring pins 1-3 as inputs, but then also adding a pull up resistor to each pin.  Fortunately, the AVR series of microcontrollers (as well as most other microcontroller) has a built in way to do this without needing any external parts.  This is done by first setting the pin as an input and then writing a value of '1' (one) to the output.  Using the Arduino IDE, this is done like this for a Pin 1:

pinMode(1, INPUT);

digitalWrite(1, HIGH);

This combination engages the internal pull up resistor (20-50K) and, as a result, will cause the voltage on the pin to rise to a HIGH state unless push button S1 is pressed to hold it LOW.  So, this means we can detect if S1 is pressed by reading back the true state of the pin, like this:

boolean state = digitalRead(1);

If the value read is LOW, it means the button is currently pressed.  Repeating this for all three pins allows us to read the state of the first 3 push buttons, S1-S3.  But, what about the other 3 buttons?  That requires a bit of bit twiddling, but it's not too hard to do.  We start with the assumption that our code has already tried to read buttons 1-3 and has found that none of them are pressed.  If so, the next step is to temporarily convert pin 2 into an output and change it's state to LOW.  However, to avoid shorting the outputs, you must do this in the reverse order.  That is, you first set the state of the pin 2 output to LOW, and then you change the mode of pin 2 so it becomes an output, like this:

digitalWrite(2, LOW);

pinMode(2, OUTPUT);

Then, after a short delay, you can read the state of pins 1 and 3 to determine the state of push buttons S4 and S5.  If pin 3 is LOW, that means S4 is pressed and, if pin 1 is LOW, that means S5 is pressed.  Then, we must undo the conversion of pin 2 into an output by converting it back into an input.  But, this must be done in the reverse order in which you converted it into an output., which is:

pinMode(2, INPUT);

digitalWrite(2, HIGH);

This guarantees that the pin doesn't momentarily become an output pin that's trying to actively drive pin 2 high (which would shunt a lot of current to ground should S2 happen to be pressed and might even cause the microcontroller to reset.)

Finally, to read the state of S6, we can follow the same sequence and convert either pin 1, or pin 3 into a LOW output state and then read the state from the pin not converted into an output.  But, let's put this all together into a simple example for the Arduino so you can see how to both read and detect only the closure of each button:

 

 #define  Q1  2  // D2

 #define  Q2  3  // D3

 #define  Q3  4  // D4

void setup() {

  Serial.begin(9600);

  pinMode(Q1, INPUT);

  pinMode(Q2, INPUT);

  pinMode(Q3, INPUT);

  digitalWrite(Q1, HIGH);

  digitalWrite(Q2, HIGH);

  digitalWrite(Q3, HIGH);

}

unsigned char lastState;

void loop() {

  unsigned char state = getStates();

  if (state != lastState) {

    if (state > 0) {

      Serial.print("Pressed: ");

      Serial.println(state, DEC);

    }

    lastState = state;

  }

  delay(100);

}

unsigned char getStates () {

  if (digitalRead(Q1) == LOW) {

    return 3;

  } else if (digitalRead(Q2) == LOW) {

    return 2;

  } else if (digitalRead(Q3) == LOW) {

    return 1;

  }

  digitalWrite(Q2, LOW);

  pinMode(Q2, OUTPUT);

  delay(1);

  boolean s1 = digitalRead(Q1);

  boolean s3 = digitalRead(Q3);

  pinMode(Q2, INPUT);

  digitalWrite(Q2, HIGH);

  if (s1 == LOW) {

    return 5;

  } else if (s3 == LOW) {

    return 4;

  } 

  digitalWrite(Q1, LOW);

  pinMode(Q1, OUTPUT);

  delay(1);

  s3 = digitalRead(Q3);

  pinMode(Q1, INPUT);

  digitalWrite(Q1, HIGH);

  if (s3 == LOW) {

    return 6;

  }

  return 0;

}

No Rollover, or Chording Support

It's important to understand the limitations of this approach.  First, you can only press one button at a time.  Pressing more than one button at a time can produce inconsistent results.  So, don't try to use this approach to build a QUERTY or piano-style keyboard.  It won't be suitable for those applications.  It's best suited to things like a simple, front panel interface for an instrument, or device that just needs buttons you press one at a time to operate the interface.

Note: On the off chance that I happen to have invented something original here, I decided to nickname this technique "WaynePlexing" in a vain attempt at Internet glory.  However, while I was unable to find another example of this technique in use, I suspect it must have been discovered before (perhaps multiple times) and that, so far, I've just missed finding a reference to it.  If not, then maybe I'll finally manage to get those 15 minutes of fame that Andy Warhol promised me.