7 Segment Multiplexing With ULN2003 & PNP Transistors

The reason a started my electronics hobby was that I wanted to build a chess clock. Lacking a proper LCD display, I chose to multiplex several 7-segment displays. Most sources in the net did not specify hardware at all, and those that did were driving the segments with a 74HC595 shift register and using NPN transistors to enable one common cathode display at a time. However, if you look at 74HC595 specs you’ll notice that it’s not designed to source the amount of current that is required to drive several multiplexed 7-segment displays. It might work, but no one can say for how long!

It took me a while to find a good, inexpensive and readily available alternative. I finally found it in ULN2003, which is inexpensive darlington array that can drive 500 mA from each of its pins. So I decided to write a little tutorial on 7 segment multiplexing that walks through all the needed hardware and software in detail. Here’s what we’ll build (click for a larger image):

For this tutorial I assume you know how to connect ATtiny2313 to a programmer and flash it with custom software. You’ll learn as much in IMakeProjects.com’s AVR tutorial. You’ll also need the following components:

  • ATtiny2313 (actually, any AVR chip with 10 output pins will do)
  • ULN2003, 7-channel darlington array (NPN, i.e. current sinks)
  • 7 resistors, 330 ohm
  • 2 seven segment displays, common anode
  • 2 PNP transistors, e.g. BC558B, but almost any will do
  • 2 resistors, 2.2 kohm
  • 4.7 kohm pullup resistor(or similar value)
  • 6-pin programming header
  • A breadboard and jumper wires
  • 5V DC voltage source (USB is one option)

Seven segment display basics

A seven segment display is basically just 8 LEDs (most include a decimal point) wired so that they share a common cathode or anode. Like normal LEDs, you need current-limiting resistors in series. Each segment needs its own resistor, otherwise LEDs get dimmer the more segments are lit. Number segments are labelled a-g and connected to “side pins”, whereas common cathode/anode occupy the center pin on top and bottom of the display – they are internally connected so we only need to wire one of them.

From the datasheet picture above, you can see how the pins are numbered (top left schematic), and which pin is connected to which segment (bottom right diagram).

ULN2003

The chips I got from Partco had the exact model of ULN2003APG. The datasheet tells that the chip has 2.7 kohm resisors on input side, so I1-I7 can be directly wired to PD0-PD6. Here’s how the pinout looks like:

In this tutorial, we’ll be wiring the “a” segments of both seven segment displays to a 330 ohm current-limiting resistor connected to O1 (only one display is on at a time so two displays can share a resistor even if two segments cannot). “b” segments are wired via another 330 ohm resistor to O2, and so on. Pretty simple!

Multiplexing common anode displays

Multiplexing means that you are actually switching only one display on at a time, but you do it so fast (at least 60 Hz) that the eye does not notice it. Easiest way to switch on a display is with a transistor, so the microcontroller does not need to sink/source all the current flowing through LEDs – even with only two displays it would most likely exceed MCU specs.

Most examples on the net use common cathode displays, in which case NPN transistor is called for, but the ULN2003 which we are using here for each segment is a current sink, which means individual segments need to be wired to it on “minus side” – so we need a common anode type of display. Because the “multiplex switch transistor” is now on VCC side instead of GND side, NPN transistor would not work. So we are using PNP transistor instead. See this article for more details on PNP switching (the previous page of that link shows NPN switching, by the way).

Note that for a PNP transistor, you wire the emitter to VCC (in this image it’s pin 3) and collector to the common anode of the display.

Hardware setup and schematic

For simplicity, I chose to the 7-pin port D of ATtiny2313 for selecting the segments, and PB0+PB1 for first and second digit display, respectively. Here’s the full schematic – the wiring of the 6-pin programming is omitted for clarity:

Update: Transistor symbol had the arrow pointing the wrong way (i.e. NPN, not PNP). Fixed.

Good order to wire these is the following:

  1. Start with the ATtiny2313 and it’s VCC, GND and RESET pullup resistor
  2. Add the 6-pin header and wire it to ATtiny, VCC and GND
  3. Add power and you can now test if you can program the ATtiny
  4. Add ULN2003 and 330 ohm resistors, connect the ground pin and wire ATtiny PD0..PD6 to I1..I7
  5. Add the first 7 segment display, and wire segments a-g to the 330 ohm resistors
  6. Tip: Program ATtiny so it outputs VCC from all port D pins (DDRD=0x7F; PORTD=0x7F;) and temporarily wire seven segment display’s common anode straight to VCC – now it’s easy to start wiring segments to the correct resistors, as a segment lights up as soon as you plug a jumper wire in!
  7. Add the second 7 segment display and connect the “a” segments of both displays, then “b” segments and so on
  8. At this point you can test if everything except the transistor switching works by wiring the anodes straight to VCC and blinking the LEDs with ATtiny
  9. Finally, add the transistors, connect their emitters to VCC, bases to PB0 & PB1 and collectors to display 1 & 2, respectively

Software

First thing to tackle is the display of numbers 0 to 9 using PORTD. With a few defines it’s easy to construct the necassary bitmasks:

#define SEG_ALL 0x7F
#define SEG_A 1
#define SEG_B 2
#define SEG_C 4
#define SEG_D 8
#define SEG_E 16
#define SEG_F 32
#define SEG_G 64

// display given digit in seven-segment display
void display(uint8_t n) {
    switch(n) {
        case 0: PORTD = SEG_ALL-SEG_G; break;
        case 1: PORTD = SEG_B+SEG_C; break;
        case 2: PORTD = SEG_ALL-SEG_F-SEG_C; break;
        case 3: PORTD = SEG_ALL-SEG_F-SEG_E; break;
        case 4: PORTD = SEG_F+SEG_G+SEG_B+SEG_C; break;
        case 5: PORTD = SEG_ALL-SEG_B-SEG_E; break;
        case 6: PORTD = SEG_ALL-SEG_B; break;
        case 7: PORTD = SEG_A+SEG_B+SEG_C; break;
        case 8: PORTD = SEG_ALL; break;
        case 9: PORTD = SEG_ALL-SEG_E; break;
        default:PORTD = 0; break;
    }
}

To have something useful to display, we’ll make the 16-bit timer1 count seconds which we will then display. For multiplexing, we’ll use 8-bit timer0 with prescaler of 8 and overflow interrupt. At 1 MHz (ATtiny2313 with default fuses), this results in 10^6 / 256 / 8 = ~500 calls per second. Each call, we turn off one display, reconfigure the segments to light up, and turn on the other display. This results in ~250 Hz refresh rate which should be plenty:

volatile uint8_t timer_seconds = 0;

ISR(TIMER1_COMPA_vect) { // 1 Hz counter
    timer_seconds++;
} 

volatile uint8_t active_display = 0;

ISR(TIMER0_OVF_vect) { // multiplex code
    PORTB |= (1 << active_display); // display OFF (VCC at base)

    active_display ^= 1; // toggle between 0 and 1
    
    if(active_display == 0) // prepare next digit
        display(timer_seconds % 10);
    else
        display((timer_seconds/10) % 10);
	
	PORTB &= ~(1 << active_display); // display ON (GND at base)
}

Note that because we are using a PNP transistor, we actually need to “write 1″ to a pin to switch the current off! Now all we need to do is initialize the ports and timers and let the interrupts do their magic:

int main(void) {	
	// initialize ports
	DDRD = SEG_ALL; // PD0-PD6 as outputs
	DDRB = (1<<PB0) + (1<<PB1); // PB0+PB1 as segment selectors
	
	// init timer 0 (multiplex)
	TIMSK |= (1 << TOIE0); // timer 0 (8-bit) overflow interrupt
	
	// init timer 1 (100 Hz clock and logic)
	TCCR1B |= (1 << WGM12); // configure for CTC mode
	TIMSK |= (1 << OCIE1A); // CTC interrupt
	OCR1A = 15625; // with prescaler 64, results in 1 Hz @ 1 MHz

	sei(); //  enable global interrupts

	// start timers
	TCCR0B |= (1 << CS01); // timer 0 at clk/8
	TCCR1B |= (1 << CS11)+(1 << CS10); // timer 1 at clk/64

	while(1) {} // loop forever

	return 1;
}

This is actually all of the code needed for the project. Just add includes to avr/io.h and avr/interrupt.h and you are all set. For the lazy ones, here’s the complete source file.

Once you have this one working, it is of course easy to add a shift register like the 74HC595 in front of ULN2003 to reduce the amount of pins needed on AVR side (3+2 required with a ’595, and using I2C I/O expander it could even be reduced to 2+2 – and of course you can do the same on multiplex side to switch up to 8 displays with only 2-3 pins).

Thanks for reading! If you liked the tutorial, do subscribe to the RSS feed, there’s more in the works. :)

15 Comments or trackbacks to 7 Segment Multiplexing With ULN2003 & PNP Transistors

Alnoor:
March 3, 2012 at 0:03

Just wanted to let you know, that your transistors are drawn incorrectly.

Alnoor

reply

jokkebk:
March 3, 2012 at 11:43

Thanks Alnoor! I hadn’t noticed the arrow was pointing the wrong way – DipTrace does not have a general PNP/NPN symbols but instead a few hundred different part codes and I had accidentally picked an NPN one. :)

reply

Anik:
April 29, 2012 at 17:03

hey thanks for this!
but can u tell me why u left the CD+ input for 2003 open? shudnt it be +12v?

reply

jokkebk says:
April 30, 2012 at 16:34

The common pin is only connected to output via diodes and I think the main function of those diodes is to provide a path for backcurrent when driving inductive loads. A LED is not an inductive load, so the diodes are not needed, and thus then pin 9 can be left unconnected (connecting it to anything between VCC and 12 V, for example, would never result in any current flowing through that pin).

I even remembered there’s a mention of this in the datasheet, but couldn’t find it when I did a quick peek. Must’ve been somewhere else where I read that…

reply

hamza:
May 27, 2012 at 8:34

Hii I am new in electronics,… Can u please tell me that how to use this code ??

reply

jokkebk says:
May 27, 2012 at 11:31

If you have the necessary parts and AVR programmer, you can get WinAVR package, compile it and use avrdude to flash it to the MCU. :) There should be plenty of tutorials on getting started with AVR microcontrollers, after you can blink a LED, you can then return here.

reply

Nestor Oak:
July 31, 2012 at 18:47

Thanks for great article! But this scheme does not count EXACT seconds, does it? I’ve been making similar project and tried your programm too, this clock is slow for 1 second for every minute!

reply

jokkebk says:
July 31, 2012 at 19:03

No it doesn’t, the internal RC oscillator in AVR chips is usually about 1 % off, as you experienced yourself. Adding a crystal oscillator as a clock source does improve the accuracy very much (to the same level as digital watches).

Also, if you’re using just one chip in a rather constant temperature, you can measure the error and calibrate the timer routines to achieve better accuracy, at least you should be able to get less than second in a minute.

reply

Nestor Oak says:
July 31, 2012 at 20:55

What if I simply make a software correction of time? For example, I extract 1 second from current time every minute (rougly). Is it a good way?

reply

Nestor Oak says:
July 31, 2012 at 21:34

Ah, of course I meant I add 1 second to my watch time

reply

jokkebk says:
August 1, 2012 at 1:07

If you’re not displaying the seconds and only care for minutes, then yes. If not, I’d rather adjust the magic value of OCR1A = 15625 – when you decrease it by 1 %, the clock goes 1 % faster and vice versa.

reply

jokkebk says:
August 1, 2012 at 1:08

Or to correct myself: The clock runs the same speed as always, but the timer interrupt is called slightly faster/slower when you decrease/increase the value of 15625. It also allows for much finer control than adding 1 second each minute.

reply

Nestor Oak says:
August 1, 2012 at 7:42

Oh, it’s very hard to calibrate a clock. I need to know what is exact slowing time, but I can’t start the microcontroller with its program and stopwatch on my cellphone completely simultaniously…

Antonio Rivero:
November 8, 2012 at 23:14

Great article thanks! just one doubt What if the leds are 12V @40mA 3 led bars (I believe the code is STR33R) I think the ULN can handle it but what about the PNP transistor and its base resistor? How can I calculate for the necessary changes ? thank you so much in advance

reply

jokkebk says:
November 8, 2012 at 23:34

If you’re using a simple transistor driver and a resistor to regulate current, you’ll need something that can handle at least the 13-14V voltage (assuming the resistor drop 1-2V). I haven’t checked ULN2003 specs for that. Also, I’m not positive that the PNP transistor can be turned off with MCU 5V voltage anymore, as 14V >> 5V – I’d google a bit on “transistor level conversion” and find a few examples, I recall additional transistor can be used for this.

Don’t have much practical experience on LED strips, so I wish you luck!

reply

Leave a Reply

Your e-mail address will not be published.


8 × = thirty two

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>