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