Multiplexing a 4-Digit 7-Segment Display with AVR Timer Interrupts
Seven-segment displays are bright, cheap and readable from across a room, which is why they are still used in clocks, counters, weighing machines and petrol pumps. A single digit needs eight lines (segments a to g plus the decimal point). Four digits wired separately would need 32 pins, more than an ATmega328P has.
Multiplexing solves this. All digits share the same eight segment lines, and only one digit is switched on at a time. By cycling through the digits fast enough, persistence of vision makes all four appear lit continuously. This tutorial builds a 4-digit counter using 12 pins and a Timer0 interrupt that refreshes the display in the background.
How Multiplexing Works
In a common cathode display, the cathodes of all eight LEDs of one digit are joined. To light a segment, its anode line must be high and the digit's common cathode must be pulled low. In a multi-digit module, segment line a connects to segment a of every digit, and each digit has its own common pin.
The refresh routine repeats four steps: turn off all digits, put the segment pattern for the next digit on the segment lines, turn on that digit, then wait for a fixed time. If each digit is shown for 2 ms, a full cycle of four digits takes 8 ms, so each digit is refreshed 125 times per second, well above the rate at which flicker is visible.
Time -> | 2 ms | 2 ms | 2 ms | 2 ms | 2 ms | ... Digit 1 | ON | | | | ON | Digit 2 | | ON | | | | Digit 3 | | | ON | | | Digit 4 | | | | ON | | Refresh per digit = 1 / 8 ms = 125 Hz
Because each digit is only on for a quarter of the time, it is dimmer than a statically driven digit. Segment resistors are therefore chosen slightly lower than for a single LED, but still within the pin current limits.
Circuit
A digit's common cathode may carry the current of all eight segments at once, which is more than a single AVR pin should sink. Each digit is therefore switched by an NPN transistor such as the BC547: emitter to ground, collector to the digit's common cathode, and base to the AVR pin through a 4.7 kΩ resistor.
| Signal | AVR pin | Notes |
|---|---|---|
| Segments a to g | PD0 to PD6 | Each through a 220 Ω resistor |
| Decimal point | PD7 | 220 Ω resistor |
| Digit 1 (leftmost) | PB0 | Base of transistor Q1 via 4.7 kΩ |
| Digit 2 | PB1 | Base of Q2 via 4.7 kΩ |
| Digit 3 | PB2 | Base of Q3 via 4.7 kΩ |
| Digit 4 (rightmost) | PB3 | Base of Q4 via 4.7 kΩ |
For a common anode display, reverse the logic: segments are active low, and each digit's common anode is switched to +5 V with a PNP transistor such as the BC557, which is turned on by a low level on its base.
Program: 4-Digit Counter with Background Refresh
Timer0 runs in CTC mode with a prescaler of 256. At 16 MHz the timer clock is 62.5 kHz, so OCR0A = 124 gives a compare match every 125 counts, which is exactly 2 ms.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
#include <stdint.h>
/* Segment patterns, bit0 = a ... bit6 = g, common cathode (1 = on) */
static const uint8_t seg_code[10] = {
0x3F, /* 0 */ 0x06, /* 1 */ 0x5B, /* 2 */ 0x4F, /* 3 */
0x66, /* 4 */ 0x6D, /* 5 */ 0x7D, /* 6 */ 0x07, /* 7 */
0x7F, /* 8 */ 0x6F /* 9 */
};
#define DIGIT_MASK 0x0F /* PB0..PB3 drive transistors */
static volatile uint8_t display_buf[4]; /* segment patterns to show */
static volatile uint16_t tick_2ms = 0;
/* Every 2 ms: show the next digit */
ISR(TIMER0_COMPA_vect)
{
static uint8_t digit = 0;
PORTB &= ~DIGIT_MASK; /* 1. all digits off (no ghosting) */
PORTD = display_buf[digit]; /* 2. segments for this digit */
PORTB |= (1 << digit); /* 3. switch this digit on */
digit = (digit + 1) & 0x03; /* next digit, 0..3 */
tick_2ms++;
}
/* Convert 0..9999 to four segment patterns, blank leading zeros */
static void display_number(uint16_t n)
{
uint8_t tmp[4];
for (int8_t i = 3; i >= 0; i--) {
tmp[i] = seg_code[n % 10];
n /= 10;
}
for (uint8_t i = 0; i < 3 && tmp[i] == seg_code[0]; i++)
tmp[i] = 0x00; /* blank leading zeros */
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
for (uint8_t i = 0; i < 4; i++)
display_buf[i] = tmp[i];
}
}
int main(void)
{
uint16_t count = 0;
uint16_t last = 0;
DDRD = 0xFF; /* all segment lines output */
DDRB |= DIGIT_MASK; /* digit drivers output */
/* Timer0 CTC, prescaler 256, OCR0A = 124 -> 2 ms */
TCCR0A = (1 << WGM01);
OCR0A = 124;
TIMSK0 = (1 << OCIE0A);
TCCR0B = (1 << CS02);
display_number(0);
sei();
while (1) {
uint16_t now;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { now = tick_2ms; }
if ((uint16_t)(now - last) >= 50) { /* 50 x 2 ms = 100 ms */
last += 50;
count = (count + 1) % 10000;
display_number(count);
}
}
}
Code Explanation
The lookup table seg_code maps each decimal digit to its segment pattern. For example, 2 needs segments a, b, d, e and g, which are bits 0, 1, 3, 4 and 6, giving 0x5B. If your wiring places segments on different bits, rebuild the table rather than rewiring.
The ISR first switches every digit off, then changes the segment lines, and only then enables the new digit. Doing it in the other order causes ghosting, where a faint copy of one digit's pattern appears on the neighbouring digit for a moment.
The main program never touches the display hardware. It only fills display_buf, inside an atomic block so the ISR never shows a half-updated number. display_number() splits a value into digits with repeated division by 10 and blanks leading zeros, so 42 appears as two digits on the right.
Troubleshooting
- All digits show the same number: the digit transistors are not switching, or every common pin is tied to ground.
- Wrong shapes: segment order differs from the assumed a = bit 0 mapping. Light one segment line at a time to map them.
- Visible flicker: the refresh interval is too long. Keep the full cycle under about 10 to 15 ms.
- Display very dim: a common anode module wired as common cathode, or base resistors too large to saturate the transistors.
Summary
Multiplexing lets a 4-digit display share eight segment lines, with one transistor-switched digit active at a time. A Timer0 CTC interrupt every 2 ms gives a 125 Hz refresh per digit with no flicker. Blank all digits before changing segments to prevent ghosting, and keep display updates in a buffer that the ISR reads, so the main program stays simple.