DIY 4-Digit Counter: Building an Arduino-Based Display with Push Button Control

Project Overview

In this project, we will build a 4-digit 7-segment counter using an Arduino Mega (ATmega2560). The system will use three pushbuttons to control the count value (increment, decrement, and reset). We will drive the display using multiplexing and handle button inputs with timer-based scanning—all without using delay().

This blog explains:

  • The difference between common anode and common cathode displays.
  • How to use hardware timer interrupts for precise timing.
  • Step-by-step development from a basic timer template to a full counter application.
An Arduino Mega 2560 board connected to a breadboard circuit with a four-digit seven-segment display and push buttons, wired together with colored jumper cables. The setup demonstrates a digital counter project, with 'Robologics Lab' branding and text describing the project on the left side.

Part 1: Understanding 4-Digit 7-Segment Displays

A 7-segment display has 8 LEDs (7 segments + 1 decimal point). In a 4-digit module, each digit shares the same segment lines (a,b,c,d,e,f,g,dp), but each digit has its own common pin.

4-digit-7-segment-LED-display-pinout.png

Common Anode vs Common Cathode

FeatureCommon AnodeCommon Cathode
Common pinConnected to +5VConnected to GND
To turn ON a segmentSegment pin = LOW (0V)Segment pin = HIGH (5V)
To turn OFF a segmentSegment pin = HIGHSegment pin = LOW
Digit selectionSet common pin HIGH to enable that digitSet common pin LOW to enable that digit

In this project: We use a Common Anode display.

  • Digit pins (D1-D4) = HIGH = digit ON.
  • Segment pins (a,b,c,…) = LOW = segment lit.

Multiplexing Technique

We cannot light all digits at once (too many pins). Instead, we:

  1. Turn ON only one digit at a time.
  2. Send the segment pattern for that digit.
  3. Quickly switch to the next digit.
  4. Repeat faster than 60Hz → human eye sees all digits lit simultaneously.

Part 2: Hardware Connections

Pin Mapping (Arduino Mega)

cpp// Digit select pins (common anode control)
#define D4 52 // Units digit (rightmost)
#define D3 50 // Tens digit
#define D2 48 // Hundreds digit
#define D1 46 // Thousands digit (leftmost)

// Segment pins (connected to all digits in parallel)
#define dp 22 // Decimal point
#define g 24 // Segment G
#define f 26 // Segment F
#define e 28 // Segment E
#define d 30 // Segment D
#define c 32 // Segment C
#define b 34 // Segment B
#define a 36 // Segment A

// Pushbuttons (using internal pull-up resistors)
#define S1 14 // Increment button
#define S2 15 // Decrement button
#define S3 16 // Reset button

Circuit diagram of 4 digit digital counter using arduino mega 2560

Wiring Summary

ComponentConnection
4-digit common anode displaySegment pins to digital 22-36 (via 220Ω resistors), Digit pins to 46-52
PushbuttonsOne pin to S1/S2/S3, other pin to GND (use INPUT_PULLUP)
PowerArduino 5V and GND

Part 3: Timer Interrupts – The Heart of Precision

What is a Timer Interrupt?

timer is a hardware counter inside the ATmega2560 that increments at a fixed rate (16 MHz here). When it reaches a preset value (OCR), it triggers an Interrupt – pausing the main loop, running a special function (ISR), then resuming.

Why use timer interrupts?

  • Perfect timing without delay() blocking.
  • Multiple tasks can run at different intervals.
  • Display multiplexing and button scanning happen in background.

Our Timer Configuration (from base code)

The provided base code configures all 6 timers of the ATmega2560 independently:

cpp#define TIMER0_INTERVAL_US 100 // Used for display scanning
#define TIMER1_INTERVAL_US 100 // Used for button scanning
#define TIMER2_INTERVAL_US 100 // (spare)
#define TIMER3_INTERVAL_US 100000 // (spare)
#define TIMER4_INTERVAL_US 500000 // (spare)
#define TIMER5_INTERVAL_US 1000000 // (spare)

Each timer triggers its own ISR at the specified interval. This allows parallel operation – different tasks run at different rates without interfering.

Part 4: Step-by-Step Code Development

We start from the base timer template and add functions one by one.

Step 1: Basic Timer Setup (Starting Point)

The base program configures all timers in CTC mode. The calc_ocr() function automatically selects the best prescaler and compare value. In setup(), we call configure_all_timers() to start them.

/*
 * ATmega2560 - Parallel 6-Timer CTC Configuration
 * Clock: 16 MHz
 * 
 * INTERVAL RANGES:
 * Timers 0,2 (8-bit):   4 µs to 16,384 µs  (0.004 - 16.384 ms)
 * Timers 1,3,4,5 (16-bit): 4 µs to 4,194,304 µs (0.004 - 4194.304 ms)
 * 
 * WARNING: Timer0 modifies Arduino millis()/delay()/micros().
 * Set TIMER0_INTERVAL_US to 0 to skip Timer0 and preserve Arduino timing.
 */

#include <avr/io.h>
#include <avr/interrupt.h>

#ifndef F_CPU
  #define F_CPU 16000000UL
#endif

// ============================================================================
// USER CONFIGURATION - Set all 6 intervals independently (microseconds)
// ============================================================================
#define TIMER0_INTERVAL_US    1000     // 8-bit: 4-16384 µs (WARNING: affects millis())
#define TIMER1_INTERVAL_US    10000    // 16-bit: 4-4194304 µs
#define TIMER2_INTERVAL_US    100      // 8-bit: 4-16384 µs
#define TIMER3_INTERVAL_US    100000   // 16-bit: 4-4194304 µs
#define TIMER4_INTERVAL_US    500000   // 16-bit: 4-4194304 µs
#define TIMER5_INTERVAL_US    1000000  // 16-bit: 4-4194304 µs

// ============================================================================
// Timer Configuration Engine
// ============================================================================
typedef struct {
    uint8_t  cs_bits;
    uint16_t prescale;
} Prescaler;

// Standard timers (0,1,3,4,5) prescalers: 1,8,64,256,1024
const Prescaler STD_PRESCALERS[5] = {
    {0b001, 1}, {0b010, 8}, {0b011, 64}, {0b100, 256}, {0b101, 1024}
};

// Timer2 prescalers: 1,8,32,64,128,256,1024 (different bit mapping)
const Prescaler T2_PRESCALERS[7] = {
    {0b001, 1}, {0b010, 8}, {0b011, 32}, {0b100, 64}, 
    {0b101, 128}, {0b110, 256}, {0b111, 1024}
};

//Functions

/***********/

static inline uint32_t calc_ocr(uint32_t us, uint8_t is16bit, const Prescaler *table, uint8_t entries, uint8_t *cs) {
    uint32_t max_ocr = is16bit ? 65535 : 255;
    uint32_t best_err = 0xFFFFFFFF;
    uint32_t ocr_out = 0;
    
    for (uint8_t i = 0; i < entries; i++) {
        uint32_t ocr = ((uint64_t)us * F_CPU) / ((uint64_t)table[i].prescale * 1000000UL) - 1;
        if (ocr <= max_ocr) {
            uint32_t actual = ((uint64_t)(ocr + 1) * table[i].prescale * 1000000UL) / F_CPU;
            uint32_t err = (actual > us) ? (actual - us) : (us - actual);
            if (err < best_err) {
                best_err = err;
                ocr_out = ocr;
                *cs = table[i].cs_bits;
            }
        }
    }
    return ocr_out;
}

// Configure single timer
void config_timer(uint8_t n, uint32_t us) {
    if (us == 0) return;  // Skip if interval is 0
    
    uint8_t cs;
    uint32_t ocr;
    
    switch(n) {
        case 0: // 8-bit
            ocr = calc_ocr(us, 0, STD_PRESCALERS, 5, &cs);
            TCCR0A = (1 << WGM01);      // CTC mode
            TCCR0B = cs;                 // Prescaler
            OCR0A = (uint8_t)ocr;       // Compare value
            TIMSK0 = (1 << OCIE0A);     // Enable interrupt
            break;
            
        case 1: // 16-bit
            ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
            TCCR1A = 0;
            TCCR1B = (1 << WGM12) | cs;
            OCR1A = (uint16_t)ocr;
            TIMSK1 = (1 << OCIE1A);
            break;
            
        case 2: // 8-bit with extended prescalers
            ocr = calc_ocr(us, 0, T2_PRESCALERS, 7, &cs);
            TCCR2A = (1 << WGM21);      // CTC mode
            TCCR2B = cs;                 // Prescaler (Timer2 specific)
            OCR2A = (uint8_t)ocr;
            TIMSK2 = (1 << OCIE2A);
            break;
            
        case 3: // 16-bit
            ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
            TCCR3A = 0;
            TCCR3B = (1 << WGM32) | cs;
            OCR3A = (uint16_t)ocr;
            TIMSK3 = (1 << OCIE3A);
            break;
            
        case 4: // 16-bit
            ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
            TCCR4A = 0;
            TCCR4B = (1 << WGM42) | cs;
            OCR4A = (uint16_t)ocr;
            TIMSK4 = (1 << OCIE4A);
            break;
            
        case 5: // 16-bit
            ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
            TCCR5A = 0;
            TCCR5B = (1 << WGM52) | cs;
            OCR5A = (uint16_t)ocr;
            TIMSK5 = (1 << OCIE5A);
            break;
    }
}

// ============================================================================
// Configure ALL timers at once (parallel operation)
// ============================================================================
void configure_all_timers() {
    cli();  // Disable interrupts during configuration
    
    config_timer(0, TIMER0_INTERVAL_US);
    config_timer(1, TIMER1_INTERVAL_US);
    config_timer(2, TIMER2_INTERVAL_US);
    config_timer(3, TIMER3_INTERVAL_US);
    config_timer(4, TIMER4_INTERVAL_US);
    config_timer(5, TIMER5_INTERVAL_US);
    
    sei();  // Enable global interrupts - all timers start simultaneously
}

void setup() {
    configure_all_timers();  // One command starts all 6 timers in parallel
  
    
}

void loop() {
    // Main application logic
    // All 6 timers run independently in background via ISRs
}

// ============================================================================
// Interrupt Service Routines - Independent Execution
// ============================================================================
ISR(TIMER0_COMPA_vect) {
    // Executes every TIMER0_INTERVAL_US microseconds
    // WARNING: If TIMER0_INTERVAL_US > 0, Arduino millis()/delay() run at different speed
    




}

ISR(TIMER1_COMPA_vect) {
    // Executes every TIMER1_INTERVAL_US microseconds
    
}

ISR(TIMER2_COMPA_vect) {
    // Executes every TIMER2_INTERVAL_US microseconds
    
}

ISR(TIMER3_COMPA_vect) {
    // Executes every TIMER3_INTERVAL_US microseconds
    
}

ISR(TIMER4_COMPA_vect) {
    // Executes every TIMER4_INTERVAL_US microseconds

    
    
}

ISR(TIMER5_COMPA_vect) {
    // Executes every TIMER5_INTERVAL_US microseconds
    
}

Step 2: Add Pin Definitions and Setup

We add the #define statements and configure pins in setup().

cppvoid setup() {
configure_all_timers();

// Digit pins as outputs
pinMode(D1, OUTPUT); pinMode(D2, OUTPUT);
pinMode(D3, OUTPUT); pinMode(D4, OUTPUT);

// Segment pins as outputs
pinMode(a, OUTPUT); pinMode(b, OUTPUT); pinMode(c, OUTPUT);
pinMode(d, OUTPUT); pinMode(e, OUTPUT); pinMode(f, OUTPUT);
pinMode(g, OUTPUT); pinMode(dp, OUTPUT);

// Button pins with pull-up
pinMode(S1, INPUT_PULLUP);
pinMode(S2, INPUT_PULLUP);
pinMode(S3, INPUT_PULLUP);

// Initial state: all digits OFF, all segments OFF (HIGH for common anode)
digitalWrite(D1, LOW); digitalWrite(D2, LOW);
digitalWrite(D3, LOW); digitalWrite(D4, LOW);
digitalWrite(a, HIGH); digitalWrite(b, HIGH);
digitalWrite(c, HIGH); digitalWrite(d, HIGH);
digitalWrite(e, HIGH); digitalWrite(f, HIGH);
digitalWrite(g, HIGH); digitalWrite(dp, HIGH);
}

Step 3: BCD to 7-Segment Conversion Function

This function takes a digit (0-9) and returns a byte where each bit represents a segment. For common anode, a 0 bit means ON.

cppchar BCD_to_7Segment(int num) {
// Bit format: dp g f e d c b a
// 0 = LED ON (common anode)
switch (num) {
case 0: return 0b11000000; // Segments a,b,c,d,e,f ON
case 1: return 0b11111001; // Segments b,c ON
case 2: return 0b10100100;
case 3: return 0b10110000;
case 4: return 0b10011001;
case 5: return 0b10010010;
case 6: return 0b10000010;
case 7: return 0b11111000;
case 8: return 0b10000000; // All segments ON
case 9: return 0b10010000;
default: return 0b11111111; // All OFF
}
}

Step 4: Display Function

This function writes the segment pattern to the physical pins.

cppvoid DisplayNum(char seg) {
// Write each bit to the corresponding pin
digitalWrite(a, (seg & 0b00000001) ? HIGH : LOW);
digitalWrite(b, (seg & 0b00000010) ? HIGH : LOW);
digitalWrite(c, (seg & 0b00000100) ? HIGH : LOW);
digitalWrite(d, (seg & 0b00001000) ? HIGH : LOW);
digitalWrite(e, (seg & 0b00010000) ? HIGH : LOW);
digitalWrite(f, (seg & 0b00100000) ? HIGH : LOW);
digitalWrite(g, (seg & 0b01000000) ? HIGH : LOW);
digitalWrite(dp, (seg & 0b10000000) ? HIGH : LOW);
}

Step 5: Counter to BCD Conversion

We store the counter value (0-9999) and split it into separate digits.

cppint Counter = 1234; // Starting value
int TH, H, T, U; // Thousands, Hundreds, Tens, Units

void CounterToBCD() {
if (Counter < 0) Counter = 0;
if (Counter > 9999) Counter = Counter % 10000;

TH = (Counter / 1000) % 10;
H = (Counter / 100) % 10;
T = (Counter / 10) % 10;
U = Counter % 10;
}

Step 6: Display Multiplexing Function

This function is called by a timer interrupt. It displays one digit at a time and cycles through all four.

cppvoid ScanDigits() {
CounterToBCD(); // Update digits from Counter

// Turn off all digit commons (prepare for next digit)
digitalWrite(D1, LOW);
digitalWrite(D2, LOW);
digitalWrite(D3, LOW);
digitalWrite(D4, LOW);

static int digit = 0; // Which digit we are showing now
char seg = 0xFF;

switch(digit) {
case 0: seg = BCD_to_7Segment(U); digitalWrite(D4, HIGH); break;
case 1: seg = BCD_to_7Segment(T); digitalWrite(D3, HIGH); break;
case 2: seg = BCD_to_7Segment(H); digitalWrite(D2, HIGH); break;
case 3: seg = BCD_to_7Segment(TH); digitalWrite(D1, HIGH); break;
}

DisplayNum(seg);
digit = (digit + 1) % 4; // Move to next digit
}

Step 7: Button Scanning with Debounce

Buttons are mechanical and bounce. We implement a debounce counter that requires a stable press for ~8ms before registering.

cppvoid ScanButtons() {
const uint8_t DEBOUNCE_TICKS = 80; // 80 * 100µs = 8ms
static uint8_t s1_cnt = 0, s2_cnt = 0, s3_cnt = 0;

// Read buttons (invert because INPUT_PULLUP reads HIGH when not pressed)
bool raw_s1 = !digitalRead(S1);
bool raw_s2 = !digitalRead(S2);
bool raw_s3 = !digitalRead(S3);

// Debounce for S1 (Increment)
if (raw_s1 && s1_cnt < DEBOUNCE_TICKS) s1_cnt++;
else if (!raw_s1 && s1_cnt > 0) s1_cnt–;

if (s1_cnt >= DEBOUNCE_TICKS && !S1_state) {
S1_state = true;
Counter++;
if (Counter >= 10000) Counter = 0;
} else if (s1_cnt < DEBOUNCE_TICKS && S1_state) {
S1_state = false;
}

// S2 (Decrement) – similar logic
// S3 (Reset) – similar logic, sets Counter = 0
}

Step 8: Assign Functions to Timer ISRs

We use Timer0 for display multiplexing (every 100µs) and Timer1 for button scanning (every 100µs).

cppISR(TIMER0_COMPA_vect) {
ScanDigits(); // Refresh display at 100µs per digit = 2.5kHz per digit
}

ISR(TIMER1_COMPA_vect) {
ScanButtons(); // Check buttons every 100µs
}


Part 5: Final Complete Code

Here is the fully integrated program:

cpp#include <Arduino.h>
#include <avr/io.h>
#include <avr/interrupt.h>

// ==================== PIN DEFINITIONS ====================
#define D4 52
#define D3 50
#define D2 48
#define D1 46

#define dp 22
#define g 24
#define f 26
#define e 28
#define d 30
#define c 32
#define b 34
#define a 36

#define S1 14
#define S2 15
#define S3 16

// ==================== GLOBAL VARIABLES ====================
int U = 0, T = 0, H = 0, TH = 0;
int Counter;
bool S1_state = 0, S2_state = 0, S3_state = 0;

// ==================== TIMER CONFIGURATION ====================
#ifndef F_CPU
#define F_CPU 16000000UL
#endif

#define TIMER0_INTERVAL_US 100 // Display scanning
#define TIMER1_INTERVAL_US 100 // Button scanning
#define TIMER2_INTERVAL_US 100 // (spare)
#define TIMER3_INTERVAL_US 100000 // (spare)
#define TIMER4_INTERVAL_US 500000 // (spare)
#define TIMER5_INTERVAL_US 1000000 // (spare)

typedef struct {
uint8_t cs_bits;
uint16_t prescale;
} Prescaler;

const Prescaler STD_PRESCALERS[5] = {
{0b001, 1}, {0b010, 8}, {0b011, 64}, {0b100, 256}, {0b101, 1024}
};

const Prescaler T2_PRESCALERS[7] = {
{0b001, 1}, {0b010, 8}, {0b011, 32}, {0b100, 64},
{0b101, 128}, {0b110, 256}, {0b111, 1024}
};

// Timer calculation and configuration functions (from base template)
static inline uint32_t calc_ocr(uint32_t us, uint8_t is16bit, const Prescaler *table, uint8_t entries, uint8_t *cs) {
uint32_t max_ocr = is16bit ? 65535 : 255;
uint32_t best_err = 0xFFFFFFFF;
uint32_t ocr_out = 0;

for (uint8_t i = 0; i < entries; i++) {
uint32_t ocr = ((uint64_t)us * F_CPU) / ((uint64_t)table[i].prescale * 1000000UL) – 1;
if (ocr <= max_ocr) {
uint32_t actual = ((uint64_t)(ocr + 1) * table[i].prescale * 1000000UL) / F_CPU;
uint32_t err = (actual > us) ? (actual – us) : (us – actual);
if (err < best_err) {
best_err = err;
ocr_out = ocr;
*cs = table[i].cs_bits;
}
}
}
return ocr_out;
}

void config_timer(uint8_t n, uint32_t us) {
if (us == 0) return;
uint8_t cs;
uint32_t ocr;

switch(n) {
case 0: ocr = calc_ocr(us, 0, STD_PRESCALERS, 5, &cs);
TCCR0A = (1 << WGM01); TCCR0B = cs; OCR0A = (uint8_t)ocr; TIMSK0 = (1 << OCIE0A); break;
case 1: ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
TCCR1A = 0; TCCR1B = (1 << WGM12) | cs; OCR1A = (uint16_t)ocr; TIMSK1 = (1 << OCIE1A); break;
case 2: ocr = calc_ocr(us, 0, T2_PRESCALERS, 7, &cs);
TCCR2A = (1 << WGM21); TCCR2B = cs; OCR2A = (uint8_t)ocr; TIMSK2 = (1 << OCIE2A); break;
case 3: ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
TCCR3A = 0; TCCR3B = (1 << WGM32) | cs; OCR3A = (uint16_t)ocr; TIMSK3 = (1 << OCIE3A); break;
case 4: ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
TCCR4A = 0; TCCR4B = (1 << WGM42) | cs; OCR4A = (uint16_t)ocr; TIMSK4 = (1 << OCIE4A); break;
case 5: ocr = calc_ocr(us, 1, STD_PRESCALERS, 5, &cs);
TCCR5A = 0; TCCR5B = (1 << WGM52) | cs; OCR5A = (uint16_t)ocr; TIMSK5 = (1 << OCIE5A); break;
}
}

void configure_all_timers() {
cli();
config_timer(0, TIMER0_INTERVAL_US);
config_timer(1, TIMER1_INTERVAL_US);
config_timer(2, TIMER2_INTERVAL_US);
config_timer(3, TIMER3_INTERVAL_US);
config_timer(4, TIMER4_INTERVAL_US);
config_timer(5, TIMER5_INTERVAL_US);
sei();
}

// ==================== DISPLAY FUNCTIONS ====================
char BCD_to_7Segment(int num) {
switch (num) {
case 0: return 0b11000000; case 1: return 0b11111001;
case 2: return 0b10100100; case 3: return 0b10110000;
case 4: return 0b10011001; case 5: return 0b10010010;
case 6: return 0b10000010; case 7: return 0b11111000;
case 8: return 0b10000000; case 9: return 0b10010000;
default: return 0b11111111;
}
}

void DisplayNum(char seg) {
digitalWrite(a, (seg & 0b00000001) ? HIGH : LOW);
digitalWrite(b, (seg & 0b00000010) ? HIGH : LOW);
digitalWrite(c, (seg & 0b00000100) ? HIGH : LOW);
digitalWrite(d, (seg & 0b00001000) ? HIGH : LOW);
digitalWrite(e, (seg & 0b00010000) ? HIGH : LOW);
digitalWrite(f, (seg & 0b00100000) ? HIGH : LOW);
digitalWrite(g, (seg & 0b01000000) ? HIGH : LOW);
digitalWrite(dp, (seg & 0b10000000) ? HIGH : LOW);
}

void CounterToBCD() {
if (Counter < 0) Counter = 0;
if (Counter > 9999) Counter = Counter % 10000;
TH = (Counter / 1000) % 10;
H = (Counter / 100) % 10;
T = (Counter / 10) % 10;
U = Counter % 10;
}

void ScanDigits() {
CounterToBCD();
digitalWrite(D1, LOW); digitalWrite(D2, LOW);
digitalWrite(D3, LOW); digitalWrite(D4, LOW);

static int digit = 0;
char seg = 0xFF;

switch(digit) {
case 0: seg = BCD_to_7Segment(U); digitalWrite(D4, HIGH); break;
case 1: seg = BCD_to_7Segment(T); digitalWrite(D3, HIGH); break;
case 2: seg = BCD_to_7Segment(H); digitalWrite(D2, HIGH); break;
case 3: seg = BCD_to_7Segment(TH); digitalWrite(D1, HIGH); break;
}
DisplayNum(seg);
digit = (digit + 1) % 4;
}

// ==================== BUTTON FUNCTIONS ====================
void ScanButtons() {
const uint8_t DEBOUNCE_TICKS = 80;
static uint8_t s1_cnt = 0, s2_cnt = 0, s3_cnt = 0;

bool raw_s1 = !digitalRead(S1);
bool raw_s2 = !digitalRead(S2);
bool raw_s3 = !digitalRead(S3);

// S1 Increment
if (raw_s1 && s1_cnt < DEBOUNCE_TICKS) s1_cnt++;
else if (!raw_s1 && s1_cnt > 0) s1_cnt–;
if (s1_cnt >= DEBOUNCE_TICKS && !S1_state) {
S1_state = true; Counter++; if (Counter >= 10000) Counter = 0;
} else if (s1_cnt < DEBOUNCE_TICKS && S1_state) S1_state = false;

// S2 Decrement
if (raw_s2 && s2_cnt < DEBOUNCE_TICKS) s2_cnt++;
else if (!raw_s2 && s2_cnt > 0) s2_cnt–;
if (s2_cnt >= DEBOUNCE_TICKS && !S2_state) {
S2_state = true; Counter–; if (Counter < 0) Counter = 9999;
} else if (s2_cnt < DEBOUNCE_TICKS && S2_state) S2_state = false;

// S3 Reset
if (raw_s3 && s3_cnt < DEBOUNCE_TICKS) s3_cnt++;
else if (!raw_s3 && s3_cnt > 0) s3_cnt–;
if (s3_cnt >= DEBOUNCE_TICKS && !S3_state) {
S3_state = true; Counter = 0;
} else if (s3_cnt < DEBOUNCE_TICKS && S3_state) S3_state = false;
}

// ==================== SETUP & LOOP ====================
void setup() {
configure_all_timers();

pinMode(D1, OUTPUT); pinMode(D2, OUTPUT);
pinMode(D3, OUTPUT); pinMode(D4, OUTPUT);
pinMode(a, OUTPUT); pinMode(b, OUTPUT);
pinMode(c, OUTPUT); pinMode(d, OUTPUT);
pinMode(e, OUTPUT); pinMode(f, OUTPUT);
pinMode(g, OUTPUT); pinMode(dp, OUTPUT);
pinMode(S1, INPUT_PULLUP);
pinMode(S2, INPUT_PULLUP);
pinMode(S3, INPUT_PULLUP);

digitalWrite(D1, LOW); digitalWrite(D2, LOW);
digitalWrite(D3, LOW); digitalWrite(D4, LOW);
digitalWrite(a, HIGH); digitalWrite(b, HIGH);
digitalWrite(c, HIGH); digitalWrite(d, HIGH);
digitalWrite(e, HIGH); digitalWrite(f, HIGH);
digitalWrite(g, HIGH); digitalWrite(dp, HIGH);
}

void loop() {
// All work done in ISRs – main loop is free for other tasks
}

// ==================== INTERRUPT SERVICE ROUTINES ====================
ISR(TIMER0_COMPA_vect) {
ScanDigits();
}

ISR(TIMER1_COMPA_vect) {
ScanButtons();
}

ISR(TIMER2_COMPA_vect) { }
ISR(TIMER3_COMPA_vect) { }
ISR(TIMER4_COMPA_vect) { }
ISR(TIMER5_COMPA_vect) { }

Conclusion

You have built a 4-digit 7-segment counter with three control buttons using timer interrupts. The key takeaways:

  • Common anode displays require LOW segment signals to light up.
  • Multiplexing allows driving many digits with few pins.
  • Timer interrupts provide precise, non-blocking timing for display refresh and button debouncing.
  • The base timer template can be reused for any project needing multiple independent timed actions.

Try modifying the code to add features like:

  • A decimal point indicator.
  • Count-up/down with auto-repeat when holding a button.
  • Saving the counter value to EEPROM.

Leave a Comment

Your email address will not be published. Required fields are marked *

Index
Scroll to Top