Friday, March 18, 2022

More buttons on fewer pins

For an index to all my stories click this text

I am working at at larger project in which I need an ESP8266 to send certain data over wifi depending on buttons being pressed. Nothing exiting you might think. However in one particular case I might need a lot of buttons.

The ESP8266 boards like a NodeMCU module or a Wemos D1 have about 11 digital I/O ports available. So I could attach a button to each I/O pin giving me 11 buttons. However that leaves no room for attaching other items like control-leds or a display.

It gets worse when you are working with really tiny controllers like the Attiny85. This micro-controller has, when normally used, just 5 I/O pins.

Let's have a look how the commercially available keyboards work.

 
 
As you can see they are build in a matrix layout. The 4 x 4 matrix has 4 rows and 4 columns. In the software you make sure that one of the rows is set to LOW, the rest to HIGH. This way you can test which column gets the LOW signal and that intersection is the key being pressed. If none of the columns gets a LOW signal the next row is set to LOW and the test starts over.

This works great however has some flaws for me. First the software makes the controller load high. You actively have to set each row in its turn LOW and the rest HIGH and then test which column gets the LOW signal. This will keep your controller pretty busy.

Next to that for 12 buttons you will need 7 I/O pins. For 16 buttons you will need 8 pins. This leaves little room for control leds although I could use neopixels for that which only take one I/O port. And this is no option for working with an Attiny85 with its mere 5 I/O pins.

I needed something better. So started thinking.

The binary system.

Let's look at the binary system. If I have one wire it can be HIGH or LOW. When using two wires there are already more possibilities:

LOW   LOW
LOW   HIGH
HIGH  LOW
HIGH  HIGH


So how do I put that to use. Lets attach switches to the 2 wires using this principle.




Look at the schematic. The two pull up resistors make sure that if no key is pressed both lines are HIGH. That is the HIGH-HIGH state.
There are three buttons.
The first button is connected to the first line, second button to the second and third button to both.
The idea is ok but it just does not work. If you press button one the ground will indeed be connected to the first line, but it will flow through the wire connected to button three. And as this button is connected to both wires, so ground will be connected to both wires.
We have to prevent that ground will be connected to two wires at the same time and that is easy.




Just put diodes on the wires connected to the buttons.
If you press button 1 ground will be connected to line 1. Ground wants to flow to button three but is prevented by the diode so can not flow back to line 2.

That is it. So with two lines we can use three switches.




Above you can see the breadboard setup. And best part: it really works.

The binary system with 3 wires

Using 3 wires we have the following posibillities:

LOW   LOW   LOW
LOW   LOW   HIGH
LOW   HIGH  LOW
LOW   HIGH  HIGH
HIGH  LOW   LOW
HIGH  LOW   HIGH
HIGH  HIGH  LOW
HIGH  HIGH  HIGH


Leaving the last one out because that is when no button is pressed, we have 7 posibillities. So 7 buttons with just 3 wires.

Lets look at the schematics again.


Not a lot different from our fist setup with only two wires.
Does it look familiar somewhere ???
Well if you are a faithfull reader of this blog you might recognise this from my stories on Charlieplexing. The hardware setup is not quite the same but it has some resemblance. You can re-read that story here:
https://lucstechblog.blogspot.com/2017/09/charlieplexing.html or search my index page for all the Charlieplexing stories: http://lucstechblog.blogspot.com/p/index-of-my-stories.html

Adding visual feedback

I wanted a visual feedback for testing purposes. For that I added a string of 7 neopixels. The reason is obvious: attaching a bunch of neopixels can be done with just 1 I/O pin. For those not familiar with the term: neopixels are adressable RGB leds officially called WS2812. For basic information on Neopixels re-read this story: http://lucstechblog.blogspot.com/2015/10/neopixels-ws2812-intro.html

The software.

Let's start with the software in Arduino IDE (C++).



#include <Adafruit_NeoPixel.h>
#define PIXEL_PIN    D8    
#define PIXEL_COUNT 7

Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, NEO_GRB + NEO_KHZ800);

void setup() 
{
  pinMode(D5, INPUT);
  pinMode(D6, INPUT);
  pinMode(D7, INPUT);
  attachInterrupt(digitalPinToInterrupt(D5), buttonpress, FALLING);
  attachInterrupt(digitalPinToInterrupt(D6), buttonpress, FALLING);
  attachInterrupt(digitalPinToInterrupt(D7), buttonpress, FALLING);
  strip.begin();
  strip.show();

}

void loop() 
{

}

void buttonpress()
{
  // button 1 ==> 011
  if (digitalRead(D5)== LOW && digitalRead(D6)== HIGH && digitalRead(D7)== HIGH)
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,0,0);
    strip.setPixelColor(2, 0,0,0);
    strip.setPixelColor(3, 0,0,0);
    strip.setPixelColor(4, 0,0,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show(); 
    delay(500);
  }
  // button 2 ==> 101
  if (digitalRead(D5)== HIGH && digitalRead(D6)== LOW && digitalRead(D7)== HIGH)
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,0,0);
    strip.setPixelColor(3, 0,0,0);
    strip.setPixelColor(4, 0,0,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show(); 
    delay(500);
  }
  // button 3 ==> 110
  if (digitalRead(D5)== HIGH && digitalRead(D6)== HIGH && digitalRead(D7)== LOW)
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,0,0);
    strip.setPixelColor(4, 0,0,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0); 
    strip.show(); 
    delay(500);
  }
    // button 4 ==> 100
    if (digitalRead(D5)== HIGH && digitalRead(D6)== LOW && digitalRead(D7)== LOW) //
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,255,0);
    strip.setPixelColor(4, 0,0,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show(); 
    delay(500);
  }
    // button 5 ==> 001
    if (digitalRead(D5)== LOW && digitalRead(D6)== LOW && digitalRead(D7)== HIGH) //
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,255,0);
    strip.setPixelColor(4, 0,255,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show(); 
    delay(500);
  }
    // button 6 ==> 010
    if (digitalRead(D5)== LOW && digitalRead(D6)== HIGH && digitalRead(D7)== LOW) //
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,255,0);
    strip.setPixelColor(4, 0,255,0);
    strip.setPixelColor(5, 0,255,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show(); 
    delay(500);
  }
    // button 7 ==> 000
    if (digitalRead(D5)== LOW && digitalRead(D6)== LOW && digitalRead(D7)== LOW) //
  {
    strip.setPixelColor(0, 0,255,0); 
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,255,0);
    strip.setPixelColor(4, 0,255,0);
    strip.setPixelColor(5, 0,255,0);
    strip.setPixelColor(6, 0,255,0); 
    strip.show(); 
    delay(500);
  }
}


Before I get a load of comments in my mail on this code let me make it clear that this source code can be optimised a lot. That would make it a lot more complicated for beginning programmers to comprehend. So adjust it for your own purposes.

The program starts with initialising the Neopixel library and attaching the library to pin D8.

In the setup routine the input lines D5, D6 and D7 are defined, and we attach an interrupt to each of these lines. All the interrupts point to the same routine: buttonpress on a FALLING signal. This means that when a button is pressed (connected to ground) the program starts the interrupt routine.

This is a big advantage over the software for the commercial keyboards. Your software is not constant sending LOW signals to row-pins and polling the collumn-pins. We just wait till a key is pressed and in the mean time the micro-controller has loads of time for other tasks.

When a button is pressed the program jumps to the buttonpress() routine.

Here is a closer examination about what happens when button 4 is pressed:

    // button 4 ==> 100
    if (digitalRead(D5)== HIGH && digitalRead(D6)== LOW && digitalRead(D7)== LOW) //
  {
    strip.setPixelColor(0, 0,255,0);
    strip.setPixelColor(1, 0,255,0);
    strip.setPixelColor(2, 0,255,0);
    strip.setPixelColor(3, 0,255,0);
    strip.setPixelColor(4, 0,0,0);
    strip.setPixelColor(5, 0,0,0);
    strip.setPixelColor(6, 0,0,0);
    strip.show();
    delay(500);
  }

The diodes from button 4 are attached to D6 and D7. So when button 4 is pressed D5 will get no signal and the pull-up resistor makes sure the line is HIGH. Line D6 and D7 will be connected to ground. This is what the if statement analyses.

Next part is just setting 4 of the neopixels to green, so we know for sure button 4 is pressed. The delay makes sure that the neopixels are lit before we go on.

Simple and efficient !!!
We got lots of room and time for the processor to perform other tasks in stead of constant sending signals and polling lines and have the risk of missing a keypress.

You will notice that the only thing you have to do is to alter D5, D6, D7 and D8 and this will also work on the ESP32 or any of the Arduino family even on the humble Attiny85 !!!

Basic program

No I am not forgetting ESP-Basic, - my used to be - favorite rapid devellopment environment (that's a mouth full). In fact the hardware setup is so efficient that writing the software in Basic is very easy. It looks a lot like the C++ program.



interrupt d5, [CHANGE]
interrupt d6, [CHANGE]
interrupt d7, [CHANGE]

neo.setup(d8)
neo.cls()

wait

[CHANGE] 

interrupt d5
interrupt d7
interrupt d6

if ((io(pi,d5) = 0) and (io(pi,d6) = 1) and (io(pi,d7) = 1)) then
neo.cls()
neo(0,250,250,250)
endif
 
if ((io(pi,d5) = 1) and (io(pi,d6) = 0) and (io(pi,d7) = 1)) then
neo.cls()
neo(0,250,250,250)
neo(1,250,250,250)
endif

if ((io(pi,d5) = 1) and (io(pi,d6) = 1) and (io(pi,d7) = 0)) then
neo.cls()
neo(0,250,250,250)
neo(1,250,250,250)
neo(2,250,250,250)
endif

if ((io(pi,d5) = 1) and (io(pi,d6) = 0) and (io(pi,d7) = 0)) then
neo.cls()
neo(0,0,0,250)
neo(1,0,0,250)
neo(2,0,0,250)
neo(3,0,0,250)
endif

if ((io(pi,d5) = 0) and (io(pi,d6) = 0) and (io(pi,d7) = 1)) then
neo.cls()
neo(0,250,0,0)
neo(1,250,0,0)
neo(2,250,0,0)
neo(3,250,0,0)
neo(4,250,0,0)
endif


if ((io(pi,d5) = 0) and (io(pi,d6) = 1) and (io(pi,d7) = 0)) then
neo.cls()
neo(0,0,250,0)
neo(1,0,250,0)
neo(2,0,250,0)
neo(3,0,250,0)
neo(4,0,250,0)
neo(5,0,250,0)
endif


if ((io(pi,d5) = 0) and (io(pi,d6) = 0) and (io(pi,d7) = 0)) then
neo.cls()
neo(0,250,0,250)
neo(1,250,0,250)
neo(2,250,0,250)
neo(3,250,0,250)
neo(4,250,0,250)
neo(5,250,0,250)
neo(6,250,0,250)
endif

delay (500)

interrupt d6, [CHANGE]
interrupt d7, [CHANGE]
interrupt d5, [CHANGE]

delay (500)
cls
wait 

end


A quick examination of the program:

interrupt d5, [CHANGE]
interrupt d6, [CHANGE]
interrupt d7, [CHANGE]

neo.setup(d8)
neo.cls()

wait

This is the main part where the interrupts are defined and the neopixels are attached to IO pin D8

[CHANGE]

interrupt d5
interrupt d7
interrupt d6

This is a tricky part.

If the [CHANGE] routine is called (as a button is pressed) we do not want that the routine is accidentally (think bouncing buttons) called again. So these commands disable the interrupts.

if ((io(pi,d5) = 1) and (io(pi,d6) = 0) and (io(pi,d7) = 0)) then
neo.cls()
neo(0,0,0,250)
neo(1,0,0,250)
neo(2,0,0,250)
neo(3,0,0,250)
endif

This is almost the same as in the C++ routine and is the part where button 4 is processed.
As you can see D5 gets no signal so that will be pulled HIGH by the pull-up resistors. D6 and D7 should both get a LOW signal. If this is true the first 4 neopixels (0 to 3) will be set to blue.

interrupt d6, [CHANGE]
interrupt d7, [CHANGE]
interrupt d5, [CHANGE]

These lines will activate the interrupts anew when the routine has finished.

As there are two delay (500) statements the program will register a button-press once every second. That should be fast enough for most purposes.

Expanding

Using 4 I/O pins you can attach 15 buttons which is enough for a simple calculator keyboard. And with 5 I/O pins you can attach 31 buttons !!
I am sure you can work the schematics out for yourselves.

Let's elaborate on this.
With 5 I/O pins you can attach 31 buttons. Now suppose you use one of these 31 buttons as a Shift key and another one as a Second Function key you could build a simple ASCII keyboard using an Arduino Pro Micro which has a real USB interface.

Pros and cons.

So why isn't this used more often, and why are all the commercial keyboards build as a matrix ssytem and not in this way.
The answer is simple: costs.
Using the matrix layout you only need buttons. In this setup you need buttons and diodes which is more expensive and makes the PCB a bit more complicated.

The pros for me weigh much more as the cons.
For the 12 button commercial version you will need to use 7 I/O pins on your controller. I only need 4 and can use 15 buttons (3 more).
The additional costs are so small that they are neglectable. At the moment of this writing you can buy 100 diodes for 50 cents !!!
Next to that the software does not put a heavy load on my controller and is much easier to program.

And last but not least: on an Attiny85 I can attach 15 buttons and a string of neopixels at the same time !!!

Real world setup




Above you can see my real world setup and test. The only difference with the schematics in this story is that I attached a seperate power supply for the Neopixels.

That's it for now.

Till next time and have fun.

Luc Volders