This project describes a 4-digit LED display made from a minimal parts count. The intention of this project was:
- Display a 4-digit number
- Use as few pins on the Arduino as possible (to keep them free for other uses)
- Have as little soldering and wiring as possible
- Use minimal extra parts like resistors etc.
- Be easy to use from your program
- Be cheap
The finished board looks like this:
Apart from the LED display itself there are 2 x 74HC595 shift registers, one on each side. There are a couple of decoupling capacitors on the 5V power line, and a pull-down resistor on the data line. The 74HC595 registers cost under $US 1 each, and the LED display cost about $US 3.
One 3 wires are needed between the board and the Arduino (plus Gnd and +5V).
This is how it looks connected up, with the 5 wires between the board and the Arduino:
Getting started
To get started I took a bit of prototyping board, like this:
By carefully orienting the two shift register chips so that the "output" pins were adjacent to the LED pins, it was possible to avoid having to run any wires from the shift registers to the LED.
Schematic
[EDIT] Despite the simplicity of this scheme, there should be current-limiting resistors in series with the LEDs. See further down for further comment about that.
Note that one shift register is turned around 180 degrees, so that both of them have their data pins facing the LED pins:
This rear view shows how things line up:
Program code
Note the absence of current-limiting resistors. Potentially we could damage both the shift register chip and the LEDs by sending unlimited current through them. We'll try to not do that by multiplexing the output, and having "off" periods so that things can cool down.
[EDIT] See further comments below about this. (April 2012)
The demo program below shows how we can address the LEDs. The main work is done by Timer 2, which is set up to fire every 1 mS (every thousandth of a second).
The main part of the program just puts into the variable array "ledOutput" the digits it wants displayed. Then the timer calls an interrupt service routine (ISR) which sequences through each digit and displays it. For example you might do this:
ledOutput [0] = 5;
ledOutput [1] = 6;
ledOutput [2] = 7;
ledOutput [3] = 8;
That would display "5678".
The code has an array "digitPatterns" which has the various segments defined which require to be on to show each letter, as per this diagram:
#include <SPI.h>
const byte LATCH = 10; // pin for latch (slave select)
const byte MAX_DIGITS = 4;
const byte TIME_BETWEEN_DIGITS = 8; // blanking period between digits in loop iterations
// which bit to turn on to make a particular pin of the LED go high
const unsigned int DIG1 = 0x0200; // Q1 of second IC
const unsigned int DIG2 = 0x1000; // Q4 of second IC
const unsigned int DIG3 = 0x2000; // Q5 of second IC
const unsigned int DIG4 = 0x0002; // Q1 of first IC
const unsigned int SEGA = 0x0400; // Q2 of second IC
const unsigned int SEGB = 0x4000; // Q6 of second IC
const unsigned int SEGC = 0x0008; // Q3 of first IC
const unsigned int SEGD = 0x0020; // Q5 of first IC
const unsigned int SEGE = 0x0040; // Q6 of first IC
const unsigned int SEGF = 0x0800; // Q3 of second IC
const unsigned int SEGG = 0x0004; // Q2 of first IC
const unsigned int SEGDP = 0x0010; // Q4 of first IC
// which segments to light for each digit
const unsigned int digitPatterns [15] = {
SEGA | SEGB | SEGC | SEGD | SEGE | SEGF, // 0
SEGB | SEGC, // 1
SEGA | SEGB | SEGD | SEGE | SEGG, // 2
SEGA | SEGB | SEGC | SEGD | SEGG, // 3
SEGB | SEGC | SEGF | SEGG, // 4
SEGA | SEGC | SEGD | SEGF | SEGG, // 5
SEGA | SEGC | SEGD | SEGE | SEGF | SEGG, // 6
SEGA | SEGB | SEGC, // 7
SEGA | SEGB | SEGC | SEGD | SEGE | SEGF | SEGG, // 8
SEGA | SEGB | SEGC | SEGD | SEGF | SEGG, // 9
0, // space
SEGG, // hyphen
SEGA | SEGB | SEGC| SEGE | SEGF | SEGG, // A
SEGA | SEGE | SEGF | SEGG, // F
SEGD | SEGE | SEGF, // L
};
// how long to leave the digit on for
const byte digitOnTimes [15] = { 3, 1, 3, 3, 2, 3, 3, 2, 3, 3, 0, 0, 3, 3, 2 };
// index into digits
const unsigned int digits [4] = { DIG1, DIG2, DIG3, DIG4 };
// 0 to 9 are themselves in the patterns array
// there are the other entries:
#define PAT_SPACE 10
#define PAT_HYPHEN 11
#define PAT_A 12
#define PAT_F 13
#define PAT_L 14
// what to display (0 to 15, plus 0x10 to light the decimal point)
// see above (eg. 1 displays 1)
volatile byte ledOutput [MAX_DIGITS];
// global variables used by the ISR
unsigned int countdown;
byte digit;
//******************************************************************
// Timer2 Interrupt Service is invoked by hardware Timer 2 every 1ms = 1000 Hz
// 16Mhz / 128 / 125 = 1000 Hz
ISR (TIMER2_COMPA_vect)
{
// keep doing what we were doing before?
if (countdown-- > 0)
return;
if (digit >= MAX_DIGITS)
{
digit = 0;
countdown = TIME_BETWEEN_DIGITS;
// blanking period to allow output chip and LEDs to cool down
digitalWrite (LATCH, LOW);
SPI.transfer (0);
SPI.transfer (0);
digitalWrite (LATCH, HIGH);
return;
} // end of done all digits
// make all digits high
unsigned int output = DIG1 | DIG2 | DIG3 | DIG4;
// bring low the wanted digit (this will sink)
output &= ~digits [digit];
// turn on wanted segments (this will source)
output |= digitPatterns [ledOutput [digit] & 0xF];
// add decimal place if required
if (ledOutput [digit] & 0x10)
output |= SEGDP;
// send to shift registers
digitalWrite (LATCH, LOW);
SPI.transfer (highByte (output));
SPI.transfer (lowByte (output));
digitalWrite (LATCH, HIGH);
// adjust time to allow for different numbers of segments
countdown = digitOnTimes [ledOutput [digit] & 0xF] ;
// onto next digit next time
digit++;
} // end of TIMER2_COMPA_vect
void setup ()
{
SPI.begin ();
// set up Timer 2
// Timer 2 - gives us our 1 mS counting interval
// 16 MHz clock (62.5 nS per tick) - prescaled by 128
// counter increments every 8 uS.
// So we count 125 of them, giving exactly 1000 uS (1 mS)
TCCR2A = 0; // stop timer 2
TCCR2B = 0;
TCCR2A = _BV (WGM21) ; // CTC mode
OCR2A = 124; // count up to 125 (zero relative!!!!)
TIMSK2 = _BV (OCIE2A); // enable Timer2 Interrupt (ie. every 1 mS)
TCNT2 = 0; // reset counter
TCCR2B = _BV (CS20) | _BV (CS22) ; // start Timer with a prescaler of 128
} // end of setup
void loop ()
{
unsigned long now = millis () / 100;
noInterrupts ();
for (int i = 0; i < 4; i++)
{
ledOutput [3 - i] = now % 10;
now /= 10;
}
// turn leading zeroes into spaces
for (int i = 0; i < 4; i++)
{
if (ledOutput [i] == 0)
ledOutput [i] = PAT_SPACE;
else
break;
}
// decimal point before 10ths of a second
ledOutput [2] |= 0x10;
interrupts ();
delay (10);
} // end of loop
To get each digit to display in turn the display routine sets the appropriate "digit bit" for the LED low (effectively sinking current). Then it consults the table of patterns for that particular digit and turns the bit for those segments high (sourcing current). Those segments that are on at one end, and off at the other, conduct current and light up. Those that are either on at both ends, off at both ends, or off at one end "the wrong way around" will be dark.
The example code just displays the counter from millis() (to the nearest tenth of a second). Note that you don't have to call any special code to make the number appear as the ISR is always displaying the current contents of ledOutput.
I turned off interrupts while refreshing the count, otherwise leading zeroes might flicker into view if the interrupt fired between putting the zeroes into the array, and removing them a moment later.
Video of it in operation:
Current limiting resistors
In my original design I omitted current limiting resistors because the LEDs are being pulsed for brief intervals.
Mike Cook has pointed out that this is a flawed design:
http://www.thebox.myzen.co.uk/Tutorial/LEDs.html
Based on Mike's advice I suggest you incorporate suitable LEDs (say 220 ohms) in series with the A-G/DP pins on the LED matrix. |