Introduction
It is very easy to start an Arduino project. To light some LEDs. However, very quickly, you will eat up the available pins. You can see the article, linked to the original 'Summer Fun with Arduino', challenge 1 here: Connecting an Arduino to a Breadboard to Light Up LEDs[^].
As you can see, it lights a sequence of eight LEDs according to the bits of a number. But what if you want to show a 16 bit number? You have only 14 digital pins on the Arduino UNO used in the sample...
While you can go with other boards (like MEGA), you can hit a size limit whenever you go for larger board/controller, and pay more too. So after all, it can be very helpful to know how to use additional chips to maximize the potential of the microcontroller...
Starting Point
The final project will contain three seven-segment displays and a piezo (buzzer). To demonstrate the problem, I created (tried at least) a diagram of those components connected to the board...
While the display shows perfectly the "answer to the Ultimate Question of Life, the Universe, and Everything", it has a problem... Not a single pin left to connect the piezo (not to mention the third seven segment display). It also ate rx/tx line so could be problem to initiate serial communication (it is a very cool option to hook the Arduino to your PC/Tablet and configure the running application via serial communication - a kind of command-line interpreter).
The project I intend to create is not a big one (like driving a printer or washing machine) and even so, I managed to break the boundaries of the Arduino...
Shift Register
To resolve the problem, I want you to meet the 74HC595 shif-register chip. A shift register is a serial-in parallel-out (it can be serial-out too, but that's not really relevant now :-)) gate between the microcontroller and the device. The idea is that you use only 3 pins to write an 8-bit value. It happens in a bit-by-bit fashion, where the shift register will receive a bit with every clock pulse and push the values into the some internal storage (which overflows when full). When the shift register gets the 'end-of-transmittion' signal it pushes the internal storage to the pins and lights the display...
A nice addition of the 595, that it has a 'overflow' pin, that enables chaining multiple shift registers and save even more Arduino pins. So when you write out 16 bits (in two steps of 8 bits each), the first 8 pulses will set the 8 storage pins of the 595 and the pulses from 9 to 19 will pushe them out to the 'overflow' pin - one by one, and take their places. And you can go on to 24 or 32 or more bits, writing them in groups of 8 but within the same loop. If you connect the 'overflow' pin to the data pin of another 595, you actually write both chips via the very same Arduino pin.
Let's see it on the board...
Now the white and yellow and turquoise lines are connected to the shift registers. All shift registers' clock and latch lines are connected to pin 8 and 9 as we want to synchronize the writing and timing for all displays. Pin 13 is connected to the data line of the first shift register (the right one) and the 'overflow' pin of that is connected to the data line of the second shift register (the middle one), and the 'overflow' pin of that is connected to the data line of the last shift register (the left one).
Now I had the free pin to connect the piezo (purple) and still 10 free pins left, including the rx/tx pair for serial communication... What is actually happening here is that I can drive 3 seven-segment displays (that is 21 LEDs!) using only 3 pins of my Arduino (and it can go on, even to the crazy thing of driving shift registers with shift registers)...
Let's Code It
Driving the Shift Register
const byte ledValues[10] = {
0b01111110, 0b00001100, 0b10110110, 0b10011110, 0b11001100, 0b11011010, 0b11111010, 0b00001110, 0b11111110, 0b11011110, };
void registerWrite(byte left, byte middle, byte right) {
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, left);
shiftOut(dataPin, clockPin, MSBFIRST, middle);
shiftOut(dataPin, clockPin, MSBFIRST, right);
digitalWrite(latchPin, HIGH);
}
ledValues
- it is a simple mapping of the digits to the 7 LEDs in the display (the first bit position unused because of technical reasons, I do not use the Q0 pin of the shift register). Because I push the bits starting from the most significant bit, the order of A-G is flipped here...
If you look at the registerWrite
method, you can realize that I write out the leftmost byte first (the one holds the value for the hundreds), then the middle (holds the value for the tens) and finally the rightmost (that holds the value for the ones). So if I write 42 (that is 042) to the display, I send this bit sequence (in this order):
0,1,1,1,1,1,1,0,1,1,0,0,1,1,0,0,1,0,1,1,0,1,1,0
You may wonder while the seemingly reversed order? You may be thinking that the first write should be the first led (A) of the first display (rightmost), but remember that we are using shift registers!!! When a new value comes in on the data line, it pushes the previous value on (and probably out to the overflow bit). So the first bit I have to write is the value of LED G of that last display in the chain (the leftmost).
The other interesting thing is that the shiftOut
method (not mine - it is native to the Arduino) can write only 8 bits - a byte - so I need three calls to write out three digits. The trick is that as long as the latch line of the shift register is low, it will be addressed as a single input. The moment I set the latch line to high, the data that came in will be sent out on the Q pins... And that also explains why I connected all the latch lines of all the shift registers to the same pin - that enables me to control their states as one. The same of course is true for the clock line, that will synchronize the writings using the same frequency for all the shift registers...
Counting Seconds
Arduino has no builtin clock to trace time, but it has a feature (common for microcontrollers) called watchdog. This feature is a ticking clock - parallel to the execution of the main loop, that is mainly used to break out from hangs. The watchdog can be programmed to fire an interrupt periodically, if the main program did not reset the watchdog before the interrupt finished, it will initiate a reboot of the controller.
In my case, I set the watchdog timer to kick in every second, then I reduce the counter and reset the watchdog to come back again after one second...
#include <avr/wdt.h> // you need this for easier access of the watchdog timer
ISR(WDT_vect) {
if(counter > 0) {
counter--;
wdt_start();
}
else {
alarm = true;
wdt_stop();
}
}
void wdt_start() {
wdt_disable();
WDTCSR |= 0b00011000; WDTCSR = 0b01001000 | WDTO_1S;
wdt_reset();
}
void wdt_stop() {
wdt_reset();
wdt_disable();
}
void setup() {
wdt_stop();
}
void loop() {
wdt_start();
}
As watchdog is a low level thing (and I do not want to load more libraries and bloat my Arduino), I initialize the timer using some bit settings...
Probably the most important thing is to disable the watchdog in the setup
method, so it will not interfere with the software/hardware initialization, that may take more than 1 second, in which case we will hit an infinite loop of restarting...
The ISR
method is the interrupt handler and that's the place the actual counting is done... After each interrupt, I restart the cycle or stop the timer if the counter hits zero.
Setting and Resetting
Then the next major part is starting and stopping the timer. For it to be totally autonomous, I would have add some buttons to set/start/stop the timer. However, I decided to use the rx/tx line (after all, I worked hard to save them) to hook the Arduino to my PC and configure it from there...
void loop() {
bool endRead = false;
while (Serial.available())
{
byte val = Serial.read();
if (val > 47 && val < 58)
{
value = value * 10 + (val - 48);
}
else if (val == 's') {
endRead = true;
}
else if (val == 'r' || val == 'R')
{
reset();
}
}
if (endRead && value > 0 && value < 1000)
{
setValue(value);
wdt_start();
value = 0;
}
}
This code reads in everything sent on the rx/tx line (on the PCs end, it will probably show as a COM over USB connection). If you look closely, you will notice that this code is built to work in an environment where IO is not blocking. In most of the developer world (desktop or web or mobile), the IO is blocking by default and you have to do special efforts to overcome it. In Arduino however, Serial.read
is non-blocking by definition. What it means in practice is that not all the information sent will be read in the same spring of the loop
method.
My solution for that is using single characters to flag the end of input. For my need, I picked r
(or R
) for reset the timer, and s
(seconds) to flag the end of the numeric input. Sending for instance 456s will set the timer 456 seconds and start counting. On the other hand, sending 23 will not do anything, except wait for more input.
Alarmingly Funny
When the timer runs out, it sets a flag to start the alarm phase. A piezo is not much for music, as all it can do is playing a note of specific frequency for a specific duration, and not in fantastic quality... However, that is enough to play a melody as alarm instead of simple buzzing, and that's exactly the last part of what the code does...
const int o = 0;
const int a = 440;
const int f = 349;
const int cH = 523;
const int eH = 659;
const int fH = 698;
const int gS = 415;
const int len = 19;
const int notes[] = {a, a, a, f, cH, a, f, cH, a, o, eH, eH, fH, cH, gS, f, cH, a, o};
const int beats[] = {8, 8, 8, 6, 2, 8, 6, 2, 16, 8, 8, 8, 8, 6, 2, 8, 6, 2, 16, 8};
const int tempo = 70;
const int piezo = 2;
void play(int note, int duration)
{
tone(piezo, note, duration);
delay(duration);
noTone(piezo);
delay(tempo / 2); }
void setup() {
pinMode(piezo, OUTPUT);
}
void loop() {
if(alarm) {
for (int i = 0; i < len; i++) {
play(notes[i], beats[i] * tempo);
}
}
}
As I know nothing of music notes, I used the values from here (with some modification to make it more efficient): Arduino Star Wars Song for Piezo.
It will play you the Imperial March theme from Star Wars in a very irritating way, over and over again, until you reset the timer...
Hardware
Only to complete it.
Quantity | Component |
1 | Arduino UNO |
3 | Seven Segment Display (cathode but you can switch) |
3 | 8-Bit Shift Register (74HC595) |
4 | 100 Ω Resistor |
1 | Piezo |
| A nice amount of wire of several colors |
Summary
Working with modern computers, or even with mobile devices may have left us with the feeling that there is no limit of resource in our hands. When starting to work with electronics and using microcontrollers, we can easily hit walls and break down. This project shows how to re-think and resolve problems of shortage of resources. Spending small but still thinking big...