AVR ATmega2560 from Basics to Advanced: Complete Hands-On Course

Table of Contents

Welcome to the World of Embedded Systems – Where Code Meets the Physical

Microcontrollers are the silent brains behind nearly every modern electronic device—from smartwatches and thermostats to drones and industrial robots. An embedded system is a dedicated computer built to perform specific tasks, and at its heart lies a microcontroller. Understanding how to program these tiny yet powerful chips is the key to unlocking innovation in robotics, automation, consumer electronics, and the Internet of Things (IoT).

Educational banner for an AVR ATmega2560 course by Dr. Asif Ahmed Memon, featuring an electronics breadboard with a microcontroller and wiring.

Why AVR and the ATmega2560?

Among the many microcontroller families, AVR stands out for its clean architecture, efficiency, and widespread use. The ATmega2560 – the chip powering the Arduino Mega 2560 board – offers a generous 256KB of flash memory, 8KB of SRAM, and a staggering 86 I/O pins. This makes it an incredibly versatile choice for complex projects that outgrow smaller boards.

The Arduino Ecosystem: Your Launchpad

The Arduino ecosystem bridges the gap between raw AVR hardware and rapid prototyping. While many courses focus solely on Arduino’s high-level abstractions, this course goes deeper: we’ll use the Arduino board as a reliable hardware platform while programming the ATmega2560 at the register level. You’ll understand exactly what happens when you call digitalWrite() – then learn to do it faster and more efficiently yourself.

Learn by Building, Not Just Reading

This is a hands-on, project-driven course. Theory exists to serve practice. You’ll work with real hardware – LEDs, sensors, motors, displays – and master:

  • GPIO bit manipulation
  • Timers for precise delays and PWM
  • Interrupts for real-time responsiveness
  • Communication protocols: UART, SPI, I2C

What You’ll Achieve

By the end, you’ll confidently design and debug complete embedded systems – from a custom multimeter to an autonomous sensor node.

Who This Course Is For

Whether you’re a curious beginner, a student brushing up on embedded fundamentals, a hobbyist ready to move beyond drag-and-drop coding, or an aspiring embedded engineer building a portfolio – this course provides a clear, hardware-validated roadmap from basics to advanced AVR system design.

Let’s turn theor

Getting Under the Hood: The ATmega2560 Microcontroller

Now that we’ve set the stage, let’s meet the star of our course: the ATmega2560. This isn’t just a larger version of the classic ATmega328P (found on the Uno); it’s a feature-packed beast designed for serious embedded applications. Understanding its capabilities is the first step toward mastering it.

Core Architecture & Memory

At its heart, the ATmega2560 is an 8-bit AVR RISC microcontroller. That “8-bit” means it processes data in 8-bit chunks, but don’t let that fool you – its speed and efficiency come from executing most instructions in a single clock cycle. Key memory specs include:

  • 256KB In-System Programmable Flash – room for substantial firmware
  • 8KB SRAM – for runtime data and variables
  • 4KB EEPROM – non-volatile storage that survives power loss

It runs at up to 16 MHz, providing deterministic timing perfect for real-time control.

Comprehensive Feature Set & Peripherals

What makes the ATmega2560 truly powerful is its rich suite of onboard peripherals. You don’t need external chips for most tasks – it’s all built in:

Peripheral TypeDetails
Digital I/O Pins86 programmable I/O lines (most are multifunctional)
Analog Inputs16-channel, 10-bit Analog-to-Digital Converter (ADC)
Timers/Counters6 timers: two 8-bit, four 16-bit – ideal for PWM, capture, compare
PWM ChannelsUp to 15 channels for motor control, dimming LEDs, audio generation
Communication4 UARTs (serial), 1 SPI, 1 TWI (I²C compatible)
External Interrupts6 pins for wake-on-signal responsiveness
Other SpecialtiesJTAG interface (debugging), analog comparator, watchdog timer

This peripheral set means you can simultaneously run a motor (PWM), log sensor data (ADC), communicate over serial (UART), and talk to an I²C accelerometer – all on one chip.

The Pin-Out Reality: Raw Chip vs. Arduino Board

Here’s where many learners get confused – and where this course provides clarity. The ATmega2560 chip has 100 pins. The Arduino Mega 2560 board routes a subset of those to convenient headers, adds voltage regulation, USB-to-serial, and resets circuitry. Knowing the mapping is essential for moving beyond “sketches” into real embedded design.

Below is the definitive reference table for the most commonly used pins. Learn this, and you’ll never feel lost again.

ATmega2560 Pin NameArduino Mega 2560 LabelFunction(s)Notes / Best Used For
PE0 (PDI)Pin 0 (RX0)UART0 Receive (RX)Serial debugging via USB
PE1 (PDO)Pin 1 (TX0)UART0 Transmit (TX)
PE2..PE5Pins 2–5External Interrupts (INT4–INT7)Hardware-triggered events
PG5Pin 4OC0B (PWM)Timer 0 PWM output
PH3Pin 6OC4A (PWM)Timer 4 PWM output
PH4Pin 7OC4B (PWM)
PH5Pin 8OC4C (PWM)
PH6Pin 9OC2B (PWM)
PB4Pin 10OC2A / SS (SPI Slave Select)PWM or SPI chip select
PB5Pin 11OC1A / MOSIPWM (Timer 1) or SPI master out
PB6Pin 12OC1B / MISOPWM or SPI master in
PB7Pin 13OC0A / SCKPWM or SPI clock; onboard LED
PJ0Pin 14 (TX3)UART3 TransmitThird hardware serial port
PJ1Pin 15 (RX3)UART3 Receive
PK0..PK7Pins A8–A15ADC8–ADC15Analog inputs (pins A8 through A15)
PF0..PF7Pins A0–A7ADC0–ADC7Analog inputs (pins A0 through A7)
PL0Pin 49OC5C (PWM)Timer 5 PWM output
PL1Pin 48OC5B (PWM)
PL2Pin 47OC5A (PWM)
PL3Pin 46OC5A (alternate)
PG2Pin 44OC3B (PWM)Timer 3 PWM output
PG3Pin 45OC3C (PWM)
PG0Pin 41OC0B (alternate)
PG1Pin 40OC0A (alternate)
PD0Pin 21 (SDA)I²C Data (TWI)For I²C sensors (pull-up required)
PD1Pin 20 (SCL)I²C Clock
PB2Pin 50 (MISO)SPI MISO (master in, slave out)SPI bus communication
PB1Pin 51 (MOSI)SPI MOSI
PB0Pin 52 (SCK)SPI Clock
PB3Pin 53 (SS)SPI Slave SelectHardware SS pin
RESETRESETActive-low resetPull high via 10k resistor

Key Insight: Notice that many Arduino pins (e.g., 44, 45, 46) are actually PWM-capable because they map to Timer 3 and Timer 5 outputs. Similarly, the extra UARTs (Serial1, Serial2, Serial3 on pins 14–19) are native to the ATmega2560, not added by the Arduino board.

Why This Matters for Your Learning Journey

By the end of this course, you won’t just plug wires into numbered headers – you’ll know which timer is generating your PWM, which interrupt vector triggers your ISR, and how to route signals directly from the chip’s pins. This knowledge transforms you from an Arduino user into an embedded designer.

In the next section, we’ll set up your development environment: writing raw C code for the ATmega2560 while still using the Arduino Mega board as your trusty hardware testbed. Stay tuned – the hands-on work begins now.y into action.

Digital I/O: Speaking the Microcontroller’s Language

A microcontroller without I/O pins is like a person without hands – intelligent but incapable of interacting with the world. The ATmega2560 provides 86 programmable digital I/O pins, organized into 11 ports (Port A through Port L, excluding a few reserved pins). Each port is an 8-bit wide register that you can read from or write to – meaning you control eight pins with a single byte of data.

The Three Registers That Rule Every Pin

For any I/O pin on the ATmega2560, three registers determine its behavior. Learn these, and you control everything:

RegisterFull NameWhat It DoesBit Value Meaning
DDRxData Direction RegisterSets pin as input or output1 = Output, 0 = Input
PORTxData RegisterSets output value or enables/disables pull-upOutput mode: 1 = HIGH, 0 = LOW
Input mode: 1 = Pull-up ON, 0 = Pull-up OFF (high-impedance)
PINxInput Pins AddressReads the actual logic level on the pin (input mode)Reading returns 0 or 1 from external signal

Where ‘x’ is the port letter: A, B, C, D, E, F, G, H, J, K, L

The Concept: How One Bit Controls One Pin

Each pin corresponds to exactly one bit position within these three registers. For example, Pin 13 on the Arduino Mega (the built-in LED) is actually PB7 – Port B, bit 7. That means:

  • DDRB bit 7 → direction of pin 13
  • PORTB bit 7 → output value (if DDRB bit 7 = 1)
  • PINB bit 7 → actual voltage read (0V or 5V)

Bit Manipulation in C for AVR – The Essential Toolkit

You cannot use normal variable assignment for individual pins – that would affect all 8 pins simultaneously. Instead, you use bitwise operators. These are your precision tools:

OperationC OperatorExample (Set bit 3 of PORTB)Effect
Set a bit to 1|= (OR)PORTB |= (1 << 3);Turns bit 3 ON, others unchanged
Clear a bit to 0&= (AND with NOT)PORTB &= ~(1 << 3);Turns bit 3 OFF, others unchanged
Toggle a bit^= (XOR)PORTB ^= (1 << 3);Flips bit 3 (1→0, 0→1)
Read a single bit& (AND)(PINB & (1 << 3)) != 0Returns 1 if bit 3 is HIGH, else 0

The (1 << n) pattern: This creates a “mask” with a 1 only at bit position n. Bit positions are 0-indexed (bit 0 = least significant bit).

Practical Example 1: Blinking an LED Without digitalWrite()

Let’s blink the built-in LED on pin 13 (PB7) using raw register access. This is what happens “under the hood” when you call digitalWrite() – but three times faster.

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // Step 1: Set PB7 as output (DDRB bit 7 = 1)
    DDRB |= (1 << 7);      // 1 << 7 = 0b10000000
    
    while(1) {
        // Step 2: Turn LED ON (PORTB bit 7 = 1)
        PORTB |= (1 << 7);
        _delay_ms(500);
        
        // Step 3: Turn LED OFF (PORTB bit 7 = 0)
        PORTB &= ~(1 << 7);
        _delay_ms(500);
    }
}

Practical Example 2: Reading a Button (Pull-up Enabled)

Connect a button between pin 22 (PF0) and GND. No external resistor needed – we’ll use the internal pull-up.

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // Set LED pin PB7 as output
    DDRB |= (1 << 7);
    
    // Set button pin PF0 as input
    DDRF &= ~(1 << 0);      // 0 = input
    
    // Enable pull-up on PF0 (so pin reads HIGH when button open)
    PORTF |= (1 << 0);      // Yes, PORTF even though it's input!
    
    while(1) {
        // Read button: if pressed, PF0 reads LOW because button shorts to GND
        if ((PINF & (1 << 0)) == 0) {  // Button pressed?
            PORTB |= (1 << 7);         // LED ON
        } else {
            PORTB &= ~(1 << 7);        // LED OFF
        }
        _delay_ms(50);  // Simple debounce
    }
}

Understanding the pull-up logic: When button is open, PF0 sees 5V via the internal resistor → reads 1. When button closes to GND, PF0 sees 0V → reads 0. That’s why we check == 0 for “pressed”.

Practical Example 3: Toggling an LED Each Button Press (Interrupt-free)

This demonstrates reading, debouncing, and toggling with bitwise XOR.

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB |= (1 << 7);       // PB7 as output (LED)
    DDRF &= ~(1 << 0);      // PF0 as input (button)
    PORTF |= (1 << 0);      // Enable pull-up
    
    uint8_t last_state = 1;   // Track previous button state
    uint8_t led_state = 0;    // Track LED state
    
    while(1) {
        uint8_t current_state = (PINF & (1 << 0)) ? 1 : 0;
        
        // Detect falling edge: button was released (1) and now pressed (0)
        if (last_state == 1 && current_state == 0) {
            led_state ^= 1;               // Toggle LED state using XOR
            if (led_state) {
                PORTB |= (1 << 7);        // ON
            } else {
                PORTB &= ~(1 << 7);       // OFF
            }
            _delay_ms(50);                // Debounce delay
        }
        last_state = current_state;
        _delay_ms(10);  // Small delay between reads
    }
}

Quick Reference: Common Port-to-Pin Mappings on Arduino Mega

Arduino PinPort & BitDDRxPORTxPINx
13 (LED)PB7DDRBPORTBPINB
10 (SS)PB4DDRBPORTBPINB
50 (MISO)PB2DDRBPORTBPINB
22–29PA0–PA7DDRAPORTAPINA
30–37PC0–PC7DDRCPORTCPINC
38–45PD0–PD7DDRDPORTDPIND
46–49PL0–PL3DDRLPORTLPINL
A0–A7PF0–PF7DDRFPORTFPINF
A8–A15PK0–PK7DDRKPORTKPINK

Common Mistakes to Avoid

❌ Writing to PINx – PINx is read-only. You never write to it.
❌ Forgetting to set DDRx – Default is input. Your pin won’t output anything.
❌ Using PORTx on an input without pull-up – That’s fine if you want high-impedance. But most beginners wonder why their floating pin reads random values.
✅ Rule of thumb: Input? Set DDRx bit = 0. Want pull-up? Set PORTx bit = 1. Want output? Set DDRx bit = 1, then use PORTx to set HIGH/LOW.

Introduction to Timer Operation

So far, we’ve used _delay_ms() to create pauses in our code. But here’s the hard truth about embedded systems: software delays waste CPU cycles. While your microcontroller sits in a delay loop, it cannot read a sensor, respond to a button, or communicate over UART. For any real-world application – from motor control to pulse measurement – you need hardware timers.

What Are Timers, Really?

A timer is simply a hardware counter that increments at a fixed rate, independent of your main program. Think of it as a digital stopwatch running in the background. The ATmega2560 contains six independent timers:

TimerResolutionKey Features
Timer08-bit (0–255)Simple timing, PWM on pins 4 and 13
Timer116-bit (0–65,535)Most versatile – input capture, dual PWM, large range
Timer28-bit (0–255)Asynchronous mode (can run while chip sleeps)
Timer316-bit (0–65,535)Identical to Timer1, on pins 2, 3, 5
Timer416-bit (0–65,535)PWM on pins 6, 7, 8
Timer516-bit (0–65,535)PWM on pins 46, 47, 48

The 8-bit vs. 16-bit difference: An 8-bit timer counts from 0 to 255 then rolls over to 0. A 16-bit timer counts from 0 to 65,535 – giving you much longer timing ranges and finer resolution.

How a Timer Counts: The Prescaler

Nothing happens instantly. The timer increments on each tick of its clock source – typically the system clock (16 MHz on the Arduino Mega). But 16 million ticks per second is far too fast for most timing needs. That’s where the prescaler comes in.

The prescaler divides the system clock before feeding it to the timer:

PrescalerEffective ClockTime per TickMax Time (8-bit)Max Time (16-bit)
1 (no division)16 MHz62.5 ns16 µs4.1 ms
82 MHz0.5 µs128 µs32.8 ms
64250 kHz4 µs1.02 ms262 ms
25662.5 kHz16 µs4.1 ms1.05 seconds
102415.625 kHz64 µs16.4 ms4.19 seconds

Practical insight: To blink an LED once per second, you wouldn’t set a 16-bit timer to count to 16 million – it can’t. Instead, you use a prescaler of 1024 and count to 15,625 (which equals 1 second at 16 MHz ÷ 1024).

The Three Timer Modes You Must Know

Every timer on the ATmega2560 can operate in three fundamental modes:

ModeWhat It DoesWhen to Use It
Normal ModeTimer counts from 0 to maximum (255 or 65,535), then rolls over to 0 and sets a flagSimple delays, measuring long periods, generating interrupts on overflow
CTC Mode (Clear Timer on Compare Match)Timer counts to a value YOU set, then resets to 0 automaticallyPrecise intervals, variable frequencies, generating exact square waves
Fast PWM ModeTimer counts from 0 to maximum, toggles output pin at compare matchMotor speed control, LED dimming, audio generation

The Registers That Control Timers

Each timer uses a consistent set of registers. Here’s the system for Timer1 (16-bit) – learn this pattern, and you’ll understand all six:

RegisterPurposeBits That Matter
TCCR1ATimer/Counter Control Register AWGM10, WGM11 (mode selection bits) COM1A1, COM1A0 (output behavior)
TCCR1BTimer/Counter Control Register BCS12, CS11, CS10 (prescaler selection) WGM12, WGM13 (more mode bits)
TCNT1Timer/Counter Register (the actual count)Read current count, write to reset counter
OCR1AOutput Compare Register AValue to compare against TCNT1
OCR1BOutput Compare Register BSecond compare value (for dual PWM)
TIMSK1Timer Interrupt Mask RegisterOCIE1A, OCIE1B, TOIE1 (enable interrupts)
TIFR1Timer Interrupt Flag RegisterOCF1A, OCF1B, TOV1 (interrupt flags – set by hardware)

The Compare Match Concept: When TCNT1 equals OCR1A, a flag is set. In CTC mode, the timer resets. In PWM mode, an output pin changes state. This is the heart of timer-based control.

Practical Example 1: 1-Second LED Blink Using Timer1 Overflow

No _delay_ms() – just pure hardware timing and interrupts.

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

volatile uint16_t overflow_count = 0;  // Counts how many overflows

// Timer1 overflow interrupt service routine
ISR(TIMER1_OVF_vect) {
    overflow_count++;
    
    // 16,384 overflows = 1 second (each overflow = 61.035 µs × 16,384 ≈ 1 sec)
    if (overflow_count >= 16384) {
        PORTB ^= (1 << 7);   // Toggle LED on pin 13 (PB7)
        overflow_count = 0;
    }
}

int main(void) {
    // Setup LED pin
    DDRB |= (1 << 7);        // PB7 as output
    PORTB |= (1 << 7);       // Start with LED ON
    
    // Setup Timer1
    TCCR1B |= (1 << CS10) | (1 << CS11);  // Prescaler = 64 (16MHz/64 = 250kHz)
    // With prescaler 64: each tick = 4 µs, overflow happens at 65535 ticks = 262 ms
    // We'll count overflows instead of making the timer itself reach 1 second
    
    TIMSK1 |= (1 << TOIE1);   // Enable Timer1 overflow interrupt
    sei();                     // Enable global interrupts (very important!)
    
    while(1) {
        // Main loop is completely free to do other tasks!
        // The LED toggles automatically via interrupt
    }
}

Why this matters: Your main loop can now read sensors, process data, or communicate while the timer handles timing in the background. This is the foundation of real-time systems.

Practical Example 2: Precise 1 kHz Square Wave Using CTC Mode

This generates a perfect 1 kHz square wave on pin 11 (OC1A/PB5) without any software intervention.

#include <avr/io.h>

int main(void) {
    // Pin 11 (PB5) as output for OC1A (Timer1 PWM/CTC output)
    DDRB |= (1 << 5);
    
    // CTC Mode (Clear Timer on Compare Match)
    // WGM13=0, WGM12=1, WGM11=0, WGM10=0  → Mode 4 (CTC)
    TCCR1B |= (1 << WGM12);    // CTC mode
    TCCR1A = 0;                // No output yet
    
    // Set prescaler to 8: 16MHz/8 = 2MHz timer clock
    TCCR1B |= (1 << CS11);     // CS11=1, CS10=0 → prescaler 8
    
    // Set compare value: 2MHz / (2 × 1000 Hz) = 1000
    // We divide by 2 because one full square wave cycle requires HIGH + LOW
    OCR1A = 1000;              // Toggle every 1000 counts = 1kHz waveform
    
    // Toggle OC1A on compare match (COM1A0=1)
    TCCR1A |= (1 << COM1A0);   // Non-PWM mode: toggle output on match
    
    while(1) {
        // The hardware generates the square wave automatically!
        // Main loop free for other tasks
    }
}

Result: Pin 11 now outputs a perfect 1 kHz square wave – measurable with an oscilloscope or even a basic speaker. The CPU never touches the pin again after setup.

Practical Example 3: Variable Frequency Buzzer (User-Controlled)

Demonstrates changing OCR1A on the fly to produce different tones.

#include <avr/io.h>
#include <util/delay.h>

// Predefined frequencies (Hz) and their OCR1A values
// Formula: OCR1A = (16,000,000 / (2 × prescaler × frequency)) - 1
// For prescaler 64: OCR1A = 125,000 / frequency
const uint16_t notes[] = {0, 250000/261, 250000/294, 250000/330}; // Off, C, D, E

int main(void) {
    DDRB |= (1 << 5);         // Pin 11 as output
    TCCR1B |= (1 << WGM12);   // CTC mode
    TCCR1B |= (1 << CS10) | (1 << CS11);  // Prescaler 64
    TCCR1A |= (1 << COM1A0);  // Toggle on match
    
    uint8_t current_note = 0;
    
    while(1) {
        // Cycle through notes every second
        OCR1A = notes[current_note];
        current_note = (current_note + 1) % 4;
        _delay_ms(1000);
    }
}

The Bigger Picture: Why Timers Transform Your Capabilities

With timers, you graduate from sequential, blocking code to event-driven, responsive systems. Here’s what becomes possible:

Without TimersWith Timers
_delay_ms() freezes everythingBackground timing while CPU works
Imprecise, drift-prone timingCrystal-accurate intervals
One task at a timeMultiple independent timing events
No motor controlServo pulses, PWM speed control
No frequency measurementInput capture for RPM sensors

Timer Operation on the ATmega2560: The Heart of Precise Timing

In the world of embedded systems, timing is not just important – it’s everything. Whether you need to blink an LED at exactly one-second intervals, measure the rpm of a spinning motor, generate an audio tone, or control the position of a servo motor, you need precise, reliable timing. The ATmega2560 provides this capability through its powerful Timer/Counter peripherals.

What Exactly is a Timer/Counter?

At its simplest level, a timer is a hardware counter that increments automatically at a fixed rate. Unlike software delays such as _delay_ms(), which waste CPU cycles by doing nothing, timers operate entirely in the background, independent of your main program.

Think of a timer as a digital stopwatch running continuously alongside your code. You can start it, stop it, reset it, and configure it to trigger actions when it reaches specific values – all without interrupting the flow of your main program except when you want it to.

The term “Timer/Counter” reflects a dual personality:

RoleClock SourceTypical Application
TimerInternal system clock (16 MHz on Arduino Mega)Generating precise delays, PWM signals, time-based events
CounterExternal signal on a dedicated input pinCounting button presses, measuring frequency, tracking encoder pulses

This duality makes the peripheral incredibly versatile. One moment it’s generating a 1 kHz square wave for a buzzer; the next, it’s counting how many times a wheel has rotated.

The ATmega2560 Timer Suite: Six Independent Timers

The ATmega2560 contains six timers, each with distinct capabilities. Understanding which timer to use for which task is a key skill:

TimerResolutionKey Pins (Arduino Labels)Best Suited For
Timer08-bit (0–255)Pin 5 (OC0B), Pin 6 (OC0A)System timing (Arduino’s millis()), simple PWM
Timer116-bit (0–65,535)Pin 11 (OC1A), Pin 12 (OC1B), Pin 46 (OC1C)Servo control, frequency measurement, complex PWM
Timer28-bit (0–255)Pin 9 (OC2B), Pin 10 (OC2A)Low-power applications, audio generation
Timer316-bit (0–65,535)Pin 2 (OC3A), Pin 3 (OC3B), Pin 5 (OC3C)Additional PWM channels, input capture
Timer416-bit (0–65,535)Pin 6 (OC4A), Pin 7 (OC4B), Pin 8 (OC4C)RGB LED control, motor speed control
Timer516-bit (0–65,535)Pin 46 (OC5A), Pin 45 (OC5B), Pin 44 (OC5C)High-precision timing, multi-channel PWM

Why the distinction between 8-bit and 16-bit? An 8-bit timer can only count from 0 to 255 before rolling over to 0. A 16-bit timer counts from 0 to 65,535 – offering 256 times more range and finer resolution. This means a 16-bit timer can measure longer durations and generate smoother PWM signals.

The Prescaler: Controlling the Timer’s Speed

The ATmega2560’s system clock runs at 16 MHz – that’s 16 million ticks per second. If a timer incremented at that rate, it would overflow in just 16 microseconds (for an 8-bit timer) or 4 milliseconds (for a 16-bit timer). For most real-world timing needs, that’s far too fast.

Enter the prescaler – a hardware divider that slows down the clock before it reaches the timer:

System Clock (16 MHz) → Prescaler (÷1, ÷8, ÷64, ÷256, ÷1024) → Timer Clock

Available prescaler values produce the following timer clock speeds and tick durations:

PrescalerTimer Clock SpeedTime per Tick (Period)Max 8-bit TimeMax 16-bit Time
1 (no division)16 MHz62.5 nanoseconds16 microseconds4.1 milliseconds
82 MHz0.5 microseconds128 microseconds32.8 milliseconds
64250 kHz4 microseconds1.02 milliseconds262 milliseconds
25662.5 kHz16 microseconds4.1 milliseconds1.05 seconds
102415.625 kHz64 microseconds16.4 milliseconds4.19 seconds

Practical implication: With a 16-bit timer and a prescaler of 1024, you can measure up to 4.19 seconds before the timer overflows. For longer durations, you simply count overflows in software.

Core Timer Concepts You Must Understand

Before configuring any timer, these four concepts are essential:

1. The Counter Register (TCNTn)

This is the heart of the timer – an 8-bit or 16-bit register that holds the current count. You can read it at any time to see how many ticks have occurred, or write to it to reset or preset the counter.

2. The Output Compare Registers (OCRnA, OCRnB, OCRnC)

These registers hold values you choose. The timer continuously compares the counter value (TCNTn) against these compare registers. When a match occurs, the timer can:

  • Set or clear an output pin automatically
  • Trigger an interrupt
  • Reset the counter (in CTC mode)

This is what enables precise PWM signals and exact timing intervals.

3. Timer Modes of Operation

Every timer on the ATmega2560 can operate in several distinct modes:

Mode CategorySpecific ModesPrimary Use
NormalSimple up-countingBasic delays, overflow interrupts
CTC (Clear Timer on Compare Match)Counter resets at OCRnAExact interval generation, square waves
Fast PWMCounts from 0 to TOP then resetsLED dimming, motor speed control
Phase Correct PWMCounts up then downLow-noise motor control, audio

Each mode changes how the timer behaves when it reaches the compare value or its maximum count.

4. Interrupts: Timers Without Wasted Cycles

Perhaps the most powerful feature of hardware timers is their ability to trigger interrupts – special functions that pause your main program, execute time-critical code, then resume exactly where they left off.

Common timer interrupts include:

  • Overflow Interrupt – triggers when the counter rolls over from maximum to 0
  • Compare Match Interrupt – triggers when TCNTn equals OCRnA or OCRnB

With interrupts, you can have a timer fire an event every millisecond while your main loop reads sensors, processes data, and communicates over serial – all without missing a beat.

Why Timers Are Better Than Software Delays

Many beginners start with _delay_ms(), but this approach has serious limitations:

Software Delay (_delay_ms())Hardware Timer
CPU sits idle, wasting powerCPU continues working
Blocks all other code executionRuns in background
Inaccurate for long delays (drift)Crystal-accurate timing
Cannot measure external eventsCan count external pulses
No PWM capabilityGenerates hardware PWM automatically
Difficult to create multiple timing eventsMultiple timers run independently

Real-World Applications of Timers

Understanding timers opens the door to countless real-world applications:

ApplicationTimer Feature Used
Digital clockOverflow interrupts to track seconds
Servo motor controlPWM output with precise pulse widths
Speedometer (RPM measurement)Input capture on external signal
Audio tone generatorFrequency generation with CTC mode
LED brightness controlPWM with variable duty cycle
Ultrasonic distance sensorMeasuring echo pulse width
Debouncing buttons without blockingPeriodic timer interrupt sampling
Stepper motor controlPrecise step timing with output compare

A Quick Peek at the Control Registers

Each timer is controlled by a small set of registers. While we’ll cover these in depth later, here’s a quick map:

Register CategoryPurposeExample (Timer0)
TCCRnA / TCCRnBControl registers – select mode, prescaler, output behaviorTCCR0A, TCCR0B
TCNTnCounter register – holds current countTCNT0
OCRnA / OCRnBOutput compare registers – hold comparison valuesOCR0A, OCR0B
TIMSKnInterrupt mask – enable/disable timer interruptsTIMSK0
TIFRnInterrupt flags – indicate which event occurredTIFR0

What Makes the ATmega2560 Timers Special

Compared to smaller AVR chips like the ATmega328P (Arduino Uno), the ATmega2560 offers:

  • More timers – Six instead of three
  • More PWM channels – Up to 15 simultaneous PWM outputs
  • Better isolation – Timer0 can keep running millis() while other timers do custom tasks
  • Input capture on multiple timers (Timer1, Timer3, Timer4, Timer5)

This richness makes the ATmega2560 ideal for complex projects requiring multiple independent timing operations – think quadcopter flight controllers, robotic arms with several servos, or multi-sensor data logging systems.

8-Bit Timers: Mastering Timer0 and Timer2 on the ATmega2560

Now that you understand the fundamental concepts of timer operation, it’s time to get hands-on with the 8-bit timers – Timer0 and Timer2. These timers are simpler than their 16-bit counterparts but incredibly powerful for a wide range of applications. Master these, and you’ll have a solid foundation for understanding all timers on the ATmega2560.

Overview: Timer0 vs Timer2

Before diving into registers and modes, let’s understand the similarities and differences between these two timers:

FeatureTimer0Timer2
Resolution8-bit (0–255)8-bit (0–255)
Arduino PWM PinsPin 5 (OC0B), Pin 6 (OC0A)Pin 9 (OC2B), Pin 10 (OC2A)
External Counter InputT0 (Pin 38 / PL0)T2 (Pin 39 / PL1)
Asynchronous Mode❌ No✅ Yes (32.768 kHz crystal on TOSC1/TOSC2)
Prescaler Options/1, /8, /64, /256, /1024/1, /8, /32, /64, /128, /256, /1024
Typical UseSystem timing, millis(), simple PWMLow-power RTC, audio, precision PWM
Interrupt VectorsTIMER0_OVF_vectTIMER0_COMPA_vectTIMER0_COMPB_vectTIMER2_OVF_vectTIMER2_COMPA_vectTIMER2_COMPB_vect

Important Note: On the Arduino Mega platform, Timer0 is used internally for millis()micros(), and delay(). If you reconfigure Timer0, these functions will break. For learning, this is fine. For real projects, consider using Timer2 or a 16-bit timer if you need to preserve Arduino compatibility.

Complete Register Reference for 8-Bit Timers

Every 8-bit timer on the ATmega2560 is controlled through a consistent set of registers. Learn these for Timer0, and Timer2 will feel identical (just change the ‘0’ to ‘2’).

The Register Suite

RegisterAddress (Timer0)Address (Timer2)Purpose
TCNT0 / TCNT20x460xB2Timer/Counter Register (the actual count)
OCR0A / OCR2A0x470xB3Output Compare Register A
OCR0B / OCR2B0x480xB4Output Compare Register B
TCCR0A / TCCR2A0x440xB0Timer/Counter Control Register A
TCCR0B / TCCR2B0x450xB1Timer/Counter Control Register B
TIMSK0 / TIMSK20x6E0x70Timer Interrupt Mask Register
TIFR0 / TIFR20x350x37Timer Interrupt Flag Register

TCCRnA – Control Register A (Detailed Bit Map)

BitNameFunctionValues
7COM0A1Compare Output Mode for Channel A (MSB)See table below
6COM0A0Compare Output Mode for Channel A (LSB)See table below
5COM0B1Compare Output Mode for Channel B (MSB)See table below
4COM0B0Compare Output Mode for Channel B (LSB)See table below
3ReservedAlways 0
2ReservedAlways 0
1WGM01Waveform Generation Mode bit 1Combined with WGM00, WGM02
0WGM00Waveform Generation Mode bit 0Combined with WGM01, WGM02

Compare Output Mode (COM0A1/COM0A0) for Non-PWM Modes (Normal/CTC):

COM0A1COM0A0OC0A Pin Behavior
00Normal port operation (disconnected from timer)
01Toggle OC0A on compare match
10Clear OC0A on compare match (set to 0)
11Set OC0A on compare match (set to 1)

Compare Output Mode (COM0A1/COM0A0) for Fast PWM Mode:

COM0A1COM0A0OC0A Pin Behavior
00Normal port operation (disconnected)
01Reserved
10Non-inverting PWM: Clear on compare, set at BOTTOM
11Inverting PWM: Set on compare, clear at BOTTOM

Compare Output Mode (COM0A1/COM0A0) for Phase Correct PWM:

COM0A1COM0A0OC0A Pin Behavior
00Normal port operation (disconnected)
01Reserved
10Non-inverting: Clear on compare when up-counting, set on compare when down-counting
11Inverting: Set on compare when up-counting, clear on compare when down-counting

TCCRnB – Control Register B (Detailed Bit Map)

BitNameFunctionValues
7FOC0AForce Output Compare AWrite 1 to force a compare match (used in non-PWM modes)
6FOC0BForce Output Compare BWrite 1 to force a compare match (used in non-PWM modes)
5ReservedAlways 0
4ReservedAlways 0
3WGM02Waveform Generation Mode bit 2Combined with WGM01, WGM00
2CS02Clock Select bit 2See prescaler table below
1CS01Clock Select bit 1See prescaler table below
0CS00Clock Select bit 0See prescaler table below

Clock Select (Prescaler) Settings:

CS02CS01CS00Timer Clock SourceTimer0/2 Clock Speed (16MHz System)
000No clock (timer stopped)0 Hz
001I/O clock / 1 (no prescale)16 MHz
010I/O clock / 82 MHz
011I/O clock / 64250 kHz
100I/O clock / 25662.5 kHz
101I/O clock / 102415.625 kHz
110External clock on T0/T2 pin (falling edge)External
111External clock on T0/T2 pin (rising edge)External

TIMSKn – Interrupt Mask Register

BitName (Timer0)Name (Timer2)Function
7ReservedReserved
6ReservedReserved
5ReservedReserved
4ReservedReserved
3ReservedReserved
2OCIE0BOCIE2BOutput Compare B Match Interrupt Enable (1 = enabled)
1OCIE0AOCIE2AOutput Compare A Match Interrupt Enable (1 = enabled)
0TOIE0TOIE2Timer Overflow Interrupt Enable (1 = enabled)

TIFRn – Interrupt Flag Register

BitName (Timer0)Name (Timer2)Function
7ReservedReserved
6ReservedReserved
5ReservedReserved
4ReservedReserved
3ReservedReserved
2OCF0BOCF2BOutput Compare B Match Flag (set by hardware, write 1 to clear)
1OCF0AOCF2AOutput Compare A Match Flag (set by hardware, write 1 to clear)
0TOV0TOV2Timer Overflow Flag (set by hardware, write 1 to clear)

All Waveform Generation Modes (8-Bit)

The combination of WGM02, WGM01, and WGM00 determines how the timer behaves. Here is the complete truth table:

ModeWGM02WGM01WGM00Mode NameTOPUpdate of OCRxTOV Flag Set on
0000Normal0xFFImmediateMAX (255)
1001PWM, Phase Correct, 8-bit0xFFTOPBOTTOM
2010CTCOCR0AImmediateMAX (255)
3011Fast PWM, 8-bit0xFFTOPMAX (255)
4100Reserved
5101PWM, Phase CorrectOCR0ATOPBOTTOM
6110CTC (alternative)OCR0AImmediateMAX (255)
7111Fast PWMOCR0ATOPMAX (255)

Understanding the Table:

  • TOP = The maximum value the counter reaches before resetting or changing direction
  • BOTTOM = 0
  • Update of OCRx = When a new compare value written to OCR0A/OCR0B takes effect
  • TOV Flag Set = When the overflow interrupt flag is triggered

Mode 0: Normal Mode – Simple Overflow Timing

In Normal mode, the counter (TCNTn) simply counts from 0 to 255 (0xFF) and then rolls over to 0, setting the overflow flag (TOVn). This is the simplest mode, perfect for creating longer delays by counting overflows.

Key Characteristics:

  • Counter counts: 0, 1, 2, … 254, 255, 0, 1, …
  • No automatic reset on compare match
  • You can read TCNTn at any time
  • OCRnA and OCRnB can still generate compare interrupts even in Normal mode

Project 1: Precision Stopwatch with 0.1 Second Resolution

This project creates a stopwatch that displays elapsed time in tenths of a second using the serial monitor.

/*
 * Precision Stopwatch using Timer0 Normal Mode
 * Displays time in format: SS.t (seconds and tenths)
 * 
 * Hardware: Arduino Mega 2560
 * Connect button to pin 2 (INT0) with pull-up resistor
 */

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

// Global variables
volatile uint16_t overflow_counter = 0;    // Counts timer overflows
volatile uint8_t tenths = 0;               // Tenths of a second (0-9)
volatile uint8_t seconds = 0;              // Seconds (0-59)
volatile uint8_t running = 1;              // Stopwatch running state
volatile uint8_t button_pressed = 0;       // Debounced button flag

// Timer0 overflow interrupt - fires every 1.024 ms
// Calculate: 16MHz / 64 = 250kHz timer clock
// 250,000 ticks/sec ÷ 256 overflows/sec = 976.5625 overflows/sec
// Each overflow = 1.024 ms
// 100 overflows = 102.4 ms (close to 0.1 sec)
// We'll use 98 overflows for exactly 100.352 ms and compensate
ISR(TIMER0_OVF_vect) {
    static uint8_t overflow_accumulator = 0;
    overflow_accumulator++;
    
    // 98 overflows ≈ 0.1 seconds (98 × 1.024ms = 100.352ms)
    if (overflow_accumulator >= 98 && running) {
        overflow_accumulator = 0;
        tenths++;
        
        if (tenths >= 10) {
            tenths = 0;
            seconds++;
            if (seconds >= 60) {
                seconds = 0;
            }
        }
    }
}

// External interrupt INT0 for button press (pin 2)
ISR(INT0_vect) {
    // Debounce with small delay (using simple counter)
    static uint8_t debounce_counter = 0;
    
    if (debounce_counter == 0) {
        running = !running;  // Toggle stopwatch state
        
        if (!running) {
            // Reset when stopping (optional feature)
            // Uncomment next two lines to reset on stop
            // tenths = 0;
            // seconds = 0;
        }
    }
    
    debounce_counter++;
    if (debounce_counter > 10) debounce_counter = 0;
}

// Function to send a byte over UART0 (simplified)
void uart_send_char(char c) {
    while (!(UCSR0A & (1 << UDRE0)));  // Wait for empty transmit buffer
    UDR0 = c;
}

// Function to send string over UART0
void uart_send_string(const char* str) {
    while (*str) {
        uart_send_char(*str++);
    }
}

// Function to send a 2-digit number with leading zero
void uart_send_2digit(uint8_t num) {
    uart_send_char('0' + (num / 10));
    uart_send_char('0' + (num % 10));
}

void setup_uart(void) {
    // Set baud rate to 9600 (16MHz, U2X0=0)
    UBRR0H = 0;
    UBRR0L = 103;    // 16,000,000 / (16 × 9600) - 1 = 103
    UCSR0B = (1 << TXEN0);  // Enable transmitter
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);  // 8-bit data, 1 stop bit
}

int main(void) {
    // Setup UART for display
    setup_uart();
    
    // Setup LED on pin 13 (PB7) as running indicator
    DDRB |= (1 << 7);
    
    // Setup button on pin 2 (PD0 / INT0) with internal pull-up
    DDRD &= ~(1 << 0);      // PD0 as input
    PORTD |= (1 << 0);      // Enable pull-up
    EICRA |= (1 << ISC01);   // INT0 on falling edge (button to GND)
    EIMSK |= (1 << INT0);    // Enable INT0
    
    // Configure Timer0 for Normal mode with prescaler 64
    TCCR0A = 0x00;                      // Normal mode (WGM00=0, WGM01=0)
    TCCR0B |= (1 << CS00) | (1 << CS01); // Prescaler 64 (CS02=0, CS01=1, CS00=1)
    TCCR0B &= ~(1 << CS02);             // Ensure CS02=0
    
    // Enable Timer0 overflow interrupt
    TIMSK0 |= (1 << TOIE0);
    
    // Enable global interrupts
    sei();
    
    // Main loop - display update
    while (1) {
        // Display current time
        uart_send_string("\rStopwatch: ");
        uart_send_2digit(seconds);
        uart_send_char('.');
        uart_send_char('0' + tenths);
        uart_send_string(" seconds   ");
        
        // Blink LED to show running state
        if (running) {
            PORTB ^= (1 << 7);  // Toggle LED when running
        } else {
            PORTB &= ~(1 << 7); // LED off when stopped
        }
        
        _delay_ms(100);  // Update display 10 times per second
    }
}

Mode 2: CTC Mode (Clear Timer on Compare Match)

In CTC mode, the timer counts from 0 up to the value stored in OCR0A, then resets to 0. This gives you complete control over the timing interval.

Key Characteristics:

  • Counter counts: 0, 1, 2, … OCR0A-1, OCR0A, 0, 1, …
  • OCR0A determines the top value
  • Great for generating exact frequencies
  • Can toggle OC0A pin automatically on compare match

Project 2: Adjustable Frequency Tone Generator with Pushbutton Control

This project generates audio tones on a speaker connected to pin 6 (OC0A) and changes frequency when a button is pressed.

/*
 * Adjustable Tone Generator using Timer0 CTC Mode
 * Press button to cycle through musical notes
 * 
 * Hardware:
 * - Speaker/buzzer connected between pin 6 (OC0A) and GND
 * - Pushbutton between pin 7 and GND (with internal pull-up)
 */

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

// Musical notes frequencies (Hz)
const uint16_t notes[] = {
    261,   // C4
    294,   // D4
    329,   // E4
    349,   // F4
    392,   // G4
    440,   // A4
    494,   // B4
    523    // C5
};
#define NUM_NOTES 8

volatile uint8_t current_note = 0;
volatile uint8_t button_pressed = 0;

// External interrupt for button on pin 7 (PE6 / INT6)
ISR(INT6_vect) {
    static uint8_t debounce = 0;
    if (debounce == 0) {
        current_note = (current_note + 1) % NUM_NOTES;
        // Update timer frequency
        // Formula for CTC mode: OCR0A = (F_CPU / (2 × Prescaler × Frequency)) - 1
        // With prescaler 64: OCR0A = (16,000,000 / (2 × 64 × Freq)) - 1
        // Simplifies to: OCR0A = (125,000 / Freq) - 1
        uint16_t ocr_value = (125000UL / notes[current_note]) - 1;
        OCR0A = (uint8_t)ocr_value;
    }
    debounce++;
    if (debounce > 10) debounce = 0;
}

void setup_button(void) {
    // Pin 7 is PE6 (Port E bit 6) on Arduino Mega
    DDRE &= ~(1 << 6);      // Input
    PORTE |= (1 << 6);      // Pull-up
    
    // Configure INT6 (falling edge)
    EICRB |= (1 << ISC61);   // ISC61=1, ISC60=0 → falling edge
    EICRB &= ~(1 << ISC60);
    EIMSK |= (1 << INT6);    // Enable INT6
}

int main(void) {
    // Setup speaker output on pin 6 (OC0A / PD6)
    DDRD |= (1 << 6);        // PD6 as output
    
    // Setup button
    setup_button();
    
    // Configure Timer0 for CTC Mode (Mode 2)
    // WGM02=0, WGM01=1, WGM00=0
    TCCR0A |= (1 << WGM01);   // WGM01=1
    TCCR0A &= ~(1 << WGM00);  // WGM00=0
    TCCR0B &= ~(1 << WGM02);  // WGM02=0
    
    // Toggle OC0A on compare match (COM0A1=0, COM0A0=1)
    TCCR0A |= (1 << COM0A0);
    TCCR0A &= ~(1 << COM0A1);
    
    // Prescaler 64 for audio frequency range
    // Timer clock = 16MHz / 64 = 250kHz
    TCCR0B |= (1 << CS01) | (1 << CS00);  // CS02=0, CS01=1, CS00=1
    TCCR0B &= ~(1 << CS02);
    
    // Set initial frequency to C4 (261 Hz)
    // OCR0A = (125,000 / 261) - 1 = 478.9 - 1 ≈ 478
    OCR0A = 478;
    
    // Enable global interrupts
    sei();
    
    while (1) {
        // Blink LED on pin 13 to indicate activity
        PORTB ^= (1 << 7);   // PB7 (pin 13)
        _delay_ms(500);
    }
}

Mode 3: Fast PWM Mode – High-Frequency Pulse Width Modulation

Fast PWM is the workhorse for applications requiring analog-like output from digital pins. The timer counts from 0 to 255 and resets. The output pin is set at the start of the cycle (BOTTOM) and cleared when TCNT0 matches OCR0A (for non-inverting mode).

Key Characteristics:

  • High frequency: PWM frequency = F_CPU / (Prescaler × 256)
  • Example with prescaler 64: 16MHz / (64 × 256) = 976.6 Hz
  • Duty cycle = (OCR0A + 1) / 256 × 100%
  • Double-buffered OCR0A prevents glitches during updates

Project 3: RGB LED Color Mixer with Analog Joystick

Create a full-color LED controller where two potentiometers (or joystick axes) control Red, Green, and Blue channels.

/*
 * RGB LED Color Mixer using Fast PWM on Timer0 and Timer2
 * 
 * Hardware:
 * - RGB LED (common cathode) with 220Ω resistors on each channel
 *   Red: Pin 6 (OC0A)
 *   Green: Pin 5 (OC0B)
 *   Blue: Pin 10 (OC2A) or Pin 9 (OC2B)
 * - Two potentiometers on A0 and A1
 * 
 * Note: This example uses three PWM channels for full RGB control
 */

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

// Global variables for RGB values (0-255)
volatile uint8_t red = 0;
volatile uint8_t green = 0;
volatile uint8_t blue = 0;

// ADC reading function
uint16_t adc_read(uint8_t channel) {
    // Select ADC channel (0-15 for A0-A15)
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    
    // Start conversion
    ADCSRA |= (1 << ADSC);
    
    // Wait for completion
    while (ADCSRA & (1 << ADSC));
    
    // Return 10-bit result
    return ADC;
}

void setup_adc(void) {
    // Enable ADC, prescaler 128 (16MHz/128 = 125kHz ADC clock)
    ADCSRA |= (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
    
    // Reference voltage = AVcc (5V)
    ADMUX |= (1 << REFS0);
    ADMUX &= ~(1 << REFS1);
}

int main(void) {
    // === Setup PWM for RGB channels ===
    
    // RED on Pin 6 (OC0A / PD6)
    DDRD |= (1 << 6);
    
    // GREEN on Pin 5 (OC0B / PD5)
    DDRD |= (1 << 5);
    
    // BLUE on Pin 10 (OC2A / PB4) - Timer2, Channel A
    DDRB |= (1 << 4);   // PB4 is pin 10 on Arduino Mega
    
    // === Configure Timer0 for Fast PWM (Mode 3) on both channels ===
    // Fast PWM Mode 3: WGM02=0, WGM01=1, WGM00=1
    TCCR0A |= (1 << WGM01) | (1 << WGM00);  // WGM01=1, WGM00=1
    TCCR0B &= ~(1 << WGM02);                // WGM02=0
    
    // Non-inverting PWM for both channels
    // RED (OC0A): Clear on compare, set at BOTTOM
    TCCR0A |= (1 << COM0A1);
    TCCR0A &= ~(1 << COM0A0);
    
    // GREEN (OC0B): Clear on compare, set at BOTTOM
    TCCR0A |= (1 << COM0B1);
    TCCR0A &= ~(1 << COM0B0);
    
    // Prescaler 64 for ~977 Hz PWM (no visible flicker)
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TCCR0B &= ~(1 << CS02);
    
    // === Configure Timer2 for Fast PWM (Mode 3) on BLUE channel ===
    // Fast PWM Mode 3 for Timer2
    TCCR2A |= (1 << WGM21) | (1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Non-inverting PWM for OC2A (BLUE)
    TCCR2A |= (1 << COM2A1);
    TCCR2A &= ~(1 << COM2A0);
    
    // Same prescaler 64 for Timer2
    TCCR2B |= (1 << CS21) | (1 << CS20);
    TCCR2B &= ~(1 << CS22);
    
    // Setup ADC for reading potentiometers
    setup_adc();
    
    while (1) {
        // Read potentiometers (10-bit ADC = 0-1023)
        // Scale to 8-bit PWM (0-255)
        uint16_t raw_x = adc_read(0);   // Red control on A0
        uint16_t raw_y = adc_read(1);   // Green control on A1
        uint16_t raw_z = adc_read(2);   // Blue control on A2
        
        red = raw_x >> 2;     // Convert 10-bit to 8-bit: divide by 4
        green = raw_y >> 2;
        blue = raw_z >> 2;
        
        // Update PWM duty cycles
        OCR0A = red;    // RED
        OCR0B = green;  // GREEN
        OCR2A = blue;   // BLUE
        
        // Small delay for stable ADC readings
        _delay_ms(20);
    }
}

Mode 5: Phase Correct PWM with TOP = OCR0A

In this mode, the timer counts up to OCR0A, then counts down to 0. This creates a symmetrical PWM waveform that reduces electrical noise and mechanical vibration in motors.

Key Characteristics:

  • Counter pattern: 0, 1, 2, … OCR0A-1, OCR0A, OCR0A-1, … 2, 1, 0, 1, …
  • PWM frequency = F_CPU / (2 × Prescaler × (OCR0A + 1))
  • Lower frequency than Fast PWM (approximately half)
  • Excellent for motor control and audio

Project 4: Servo Motor Controller (Using Timer0 Phase Correct PWM with External Top)

*Note: Real servo control requires 16-bit timers for adequate resolution. This example demonstrates the concept; a 16-bit timer version will follow in the next section.*

/*
 * Servo Motor Control using Timer0 Phase Correct PWM (Concept Demo)
 * 
 * While Timer0 lacks the resolution for true servo control (needs 1-2ms pulses at 50Hz),
 * this example demonstrates phase correct PWM for LED breathing effect
 * 
 * Hardware: LED on pin 6 with 220Ω resistor to GND
 */

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    // LED on pin 6 (OC0A)
    DDRD |= (1 << 6);
    
    // Phase Correct PWM Mode 5: WGM02=1, WGM01=0, WGM00=1
    // Wait - Mode 5 requires WGM02=1, WGM01=0, WGM00=1
    // Let's use Mode 1 (Phase Correct, 8-bit) for simpler demonstration
    
    // Mode 1: Phase Correct PWM, 8-bit (TOP=255)
    TCCR0A |= (1 << WGM00);             // WGM00=1
    TCCR0A &= ~(1 << WGM01);            // WGM01=0
    TCCR0B &= ~(1 << WGM02);            // WGM02=0
    
    // Non-inverting PWM
    TCCR0A |= (1 << COM0A1);
    TCCR0A &= ~(1 << COM0A0);
    
    // Prescaler 64
    TCCR0B |= (1 << CS01) | (1 << CS00);
    
    // Breathing effect: smoothly fade up and down
    int8_t direction = 1;
    uint8_t brightness = 0;
    
    while (1) {
        OCR0A = brightness;
        _delay_ms(5);
        
        brightness += direction;
        
        if (brightness >= 255) {
            brightness = 255;
            direction = -1;
        } else if (brightness == 0) {
            direction = 1;
        }
    }
}

Mode 7: Fast PWM with TOP = OCR0A – Variable Frequency PWM

This mode allows you to control both the frequency (via OCR0A) and duty cycle (via OCR0B) independently. The counter counts from 0 to OCR0A, then resets.

Frequency Formula: PWM Frequency = F_CPU / (Prescaler × (OCR0A + 1))

Duty Cycle (non-inverting): Duty Cycle = (OCR0B + 1) / (OCR0A + 1) × 100%

Project 5: Adjustable Frequency and Duty Cycle Signal Generator

This project creates a signal generator where one potentiometer controls frequency and another controls duty cycle.

/*
 * Adjustable Frequency and Duty Cycle Signal Generator
 * Using Timer0 Fast PWM Mode 7
 * 
 * Hardware:
 * - Output on pin 5 (OC0B)
 * - Potentiometer A0 for frequency (100Hz - 10kHz)
 * - Potentiometer A1 for duty cycle (0-100%)
 * - Optional: Measure with oscilloscope or logic analyzer
 */

#include <avr/io.h>
#include <util/delay.h>

// ADC reading function
uint16_t adc_read(uint8_t channel) {
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F);
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    return ADC;
}

void setup_adc(void) {
    ADCSRA |= (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
    ADMUX |= (1 << REFS0);
}

int main(void) {
    // Output on pin 5 (OC0B / PD5)
    DDRD |= (1 << 5);
    
    // Fast PWM Mode 7: WGM02=1, WGM01=1, WGM00=1
    TCCR0A |= (1 << WGM01) | (1 << WGM00);  // WGM01=1, WGM00=1
    TCCR0B |= (1 << WGM02);                 // WGM02=1
    
    // Non-inverting PWM on OC0B
    TCCR0A |= (1 << COM0B1);
    TCCR0A &= ~(1 << COM0B0);
    
    // Prescaler 1 for maximum frequency range
    TCCR0B |= (1 << CS00);      // CS00=1 only → prescaler 1
    TCCR0B &= ~((1 << CS02) | (1 << CS01));
    
    setup_adc();
    
    while (1) {
        // Read frequency potentiometer (0-1023)
        // Map to frequency: 100Hz to 10kHz
        // TOP = (16,000,000 / Prescaler / Frequency) - 1
        // For prescaler 1: TOP = (16,000,000 / Frequency) - 1
        uint16_t freq_raw = adc_read(0);
        uint16_t frequency = 100 + (freq_raw * 9900UL) / 1023;  // 100Hz to 10kHz
        
        // Calculate TOP value
        uint16_t top = (16000000UL / frequency) - 1;
        if (top > 255) top = 255;
        if (top < 1) top = 1;
        OCR0A = (uint8_t)top;
        
        // Read duty cycle potentiometer (0-1023) → 0-100%
        uint16_t duty_raw = adc_read(1);
        uint8_t duty_percent = (duty_raw * 100UL) / 1023;
        
        // Calculate OCR0B for desired duty cycle
        uint16_t ocr_value = (OCR0A + 1) * duty_percent / 100;
        if (ocr_value > 0) ocr_value--;
        OCR0B = (uint8_t)ocr_value;
        
        _delay_ms(50);
    }
}

Timer2: The Asynchronous Power-Saving Timer

Timer2 is nearly identical to Timer0 but with one superpower: asynchronous operation. You can connect a 32.768 kHz watch crystal to TOSC1 and TOSC2 (pins 36 and 37 on Arduino Mega), and Timer2 will run independently, even when the main CPU is in sleep mode.

TOSC1 and TOSC2 Locations:

  • TOSC1 = Pin 36 (PH0)
  • TOSC2 = Pin 37 (PH1)

Project 6: Real-Time Clock (RTC) Using Timer2 Asynchronous Mode

This project creates a battery-backed real-time clock that keeps time even when the main microcontroller is powered off (using a backup battery on the 32.768 kHz crystal circuit).

/*
 * Real-Time Clock using Timer2 Asynchronous Mode
 * 
 * Hardware Requirements:
 * - 32.768 kHz watch crystal connected between pins 36 and 37
 * - 22pF capacitors from each pin to ground
 * - Optional: 3V backup battery for crystal oscillator circuit
 * 
 * This example tracks time and outputs to serial monitor
 */

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

// Time tracking variables (updated by Timer2)
volatile uint8_t seconds = 0;
volatile uint8_t minutes = 0;
volatile uint8_t hours = 0;
volatile uint8_t days = 0;

// Timer2 compare match interrupt (fires once per second)
ISR(TIMER2_COMPA_vect) {
    seconds++;
    if (seconds >= 60) {
        seconds = 0;
        minutes++;
        if (minutes >= 60) {
            minutes = 0;
            hours++;
            if (hours >= 24) {
                hours = 0;
                days++;
            }
        }
    }
}

void setup_uart(void) {
    UBRR0H = 0;
    UBRR0L = 103;    // 9600 baud at 16MHz
    UCSR0B = (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void uart_send_char(char c) {
    while (!(UCSR0A & (1 << UDRE0)));
    UDR0 = c;
}

void uart_send_2digit(uint8_t num) {
    uart_send_char('0' + (num / 10));
    uart_send_char('0' + (num % 10));
}

int main(void) {
    setup_uart();
    
    // Enable asynchronous mode for Timer2
    // First, enable the asynchronous clock input
    ASSR |= (1 << AS2);     // AS2 = 1 → Timer2 uses TOSC1/TOSC2
    
    // Wait for TCNT2UB, OCR2AUB, OCR2BUB, TCR2AUB, TCR2BUB to clear
    // This ensures registers are updated before we write to them
    while (ASSR & ((1 << TCNT2UB) | (1 << OCR2AUB) | 
                   (1 << OCR2BUB) | (1 << TCR2AUB) | (1 << TCR2BUB)));
    
    // Configure Timer2 for CTC mode
    TCCR2A |= (1 << WGM21);     // CTC Mode (WGM22=0, WGM21=1, WGM20=0)
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Set compare value for 1 second interrupts
    // With 32.768 kHz crystal and prescaler 128:
    // Timer clock = 32768 / 128 = 256 Hz
    // To get 1 second: OCR2A = 256 - 1 = 255
    OCR2A = 255;
    
    // Prescaler 128: CS22=1, CS21=0, CS20=0 (for asynchronous mode)
    TCCR2B |= (1 << CS22);
    TCCR2B &= ~((1 << CS21) | (1 << CS20));
    
    // Wait for registers to update in asynchronous mode
    while (ASSR & ((1 << TCNT2UB) | (1 << OCR2AUB) | 
                   (1 << TCR2AUB) | (1 << TCR2BUB)));
    
    // Enable compare match interrupt
    TIMSK2 |= (1 << OCIE2A);
    
    // Enable global interrupts
    sei();
    
    // Send startup message
    uart_send_char('\n');
    uart_send_char('\r');
    
    while (1) {
        // Update time display every 100ms
        uart_send_char('\r');
        uart_send_2digit(hours);
        uart_send_char(':');
        uart_send_2digit(minutes);
        uart_send_char(':');
        uart_send_2digit(seconds);
        
        if (days > 0) {
            uart_send_char(' ');
            uart_send_char('0' + days);
            uart_send_char('d');
        }
        
        _delay_ms(100);
    }
}

Practical Applications Summary Table

ApplicationBest Timer/ModePrescalerKey Settings
LED Breathing EffectTimer0/2, Mode 1/564Phase correct PWM
Servo ControlTimer1 (16-bit), Mode 148Fast PWM with ICR1 top
Audio Tone GenerationTimer0/2, Mode 2 (CTC)64 or 256Toggle OC on compare
Motor Speed ControlTimer0/2, Mode 364Fast PWM non-inverting
Simple Millisecond DelayTimer0/2, Mode 064Overflow interrupt
Frequency CounterTimer0/2, Counter modeExternalExternal clock on T0/T2
Real-Time ClockTimer2, Mode 2 (CTC)Async 12832.768 kHz crystal
Variable Frequency PWMTimer0/2, Mode 71 or 8TOP = OCR0A
LED Dimmer (Manual)Timer0/2, Mode 364Fast PWM, update OCR
Square Wave GeneratorTimer0/2, Mode 2 (CTC)64Toggle OC on match

Common Pitfalls and Debugging Tips

ProblemLikely CauseSolution
PWM not working on pinDDR not set to outputSet corresponding DDR bit
Wrong pin outputtingPin not connected to correct timerCheck pin mapping table
Timer interrupt not firingGlobal interrupts disabledVerify sei() called
Glitches on PWM outputUpdating OCR during PWM cycleUse double-buffered mode or update at TOP
Timer2 not countingForgot to enable AS2 in ASSRSet ASSR |= (1 << AS2) for async mode
Arduino functions brokenReconfigured Timer0Use Timer2 or 16-bit timer instead
Unexpected timingWrong prescaler calculationDouble-check clock source and divider
External counter not countingPin direction wrongSet DDR bit to 0 (input)

Practical Problems for Timer 0

Before diving into the problems, let’s establish the standard hardware configuration that will be used throughout all 15 exercises.

Hardware Connection Map

ComponentPortPinsArduino Mega Labels
LEDs (8)PORTAPA0 – PA7Pins 22 – 29
Pushbuttons (8)PORTLPL0 – PL7Pins 49 – 42 (reverse order)

Detailed Wiring Instructions

LED Bank (PORTA):

  • Connect anode of each LED through a 220Ω current-limiting resistor to PA0-PA7
  • Connect cathodes of all LEDs to GND (common cathode configuration)
  • PA0 = LED0 (Pin 22), PA1 = LED1 (Pin 23), …, PA7 = LED7 (Pin 29)

Pushbutton Bank (PORTL):

  • Connect one terminal of each button to PL0-PL7
  • Connect the other terminal of each button to GND
  • Enable internal pull-up resistors in software (no external resistors needed)
  • Button pressed = logic 0 (LOW), button released = logic 1 (HIGH)
  • PL0 = Button0 (Pin 49), PL1 = Button1 (Pin 48), …, PL7 = Button7 (Pin 42)

Common Initialization Functions

// LED functions (PORTA)
void leds_init(void) {
    DDRA = 0xFF;      // All PORTA pins as outputs
    PORTA = 0x00;     // All LEDs OFF initially
}

void led_on(uint8_t led_num) {
    PORTA |= (1 << led_num);
}

void led_off(uint8_t led_num) {
    PORTA &= ~(1 << led_num);
}

void led_toggle(uint8_t led_num) {
    PORTA ^= (1 << led_num);
}

void leds_set(uint8_t pattern) {
    PORTA = pattern;  // Set all LEDs at once (bit0 = LED0)
}

uint8_t leds_read(void) {
    return PORTA;     // Read current LED state
}

// Button functions (PORTL)
void buttons_init(void) {
    DDRL = 0x00;      // All PORTL pins as inputs
    PORTL = 0xFF;     // Enable internal pull-ups on all buttons
}

uint8_t buttons_read(void) {
    return PINL;      // Read all buttons (1 = not pressed, 0 = pressed)
}

uint8_t is_button_pressed(uint8_t button_num) {
    return ((PINL >> button_num) & 0x01) == 0;  // Return 1 if pressed
}

Common Helper Functions

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

// Debounce a single button (returns 1 if stable press detected)
uint8_t debounce_button(uint8_t button_num) {
    if (is_button_pressed(button_num)) {
        _delay_ms(30);  // Wait for debounce
        if (is_button_pressed(button_num)) {
            // Wait for release
            while (is_button_pressed(button_num));
            _delay_ms(30);
            return 1;
        }
    }
    return 0;
}

// Get the first pressed button (returns 0-7 or 0xFF if none)
uint8_t get_first_pressed_button(void) {
    uint8_t buttons = buttons_read();
    for (uint8_t i = 0; i < 8; i++) {
        if (!(buttons & (1 << i))) {
            return i;
        }
    }
    return 0xFF;  // No button pressed
}

Problem Set 1: Basic Timer Configuration and Overflow Interrupts

Problem 1: LED Binary Counter with 1-Second Interval Using Timer0 Overflow

Scenario: You need to create a binary counter that displays values from 0 to 255 on the 8 LEDs (representing the 8 least significant bits). The counter should increment every 1 second using Timer0 in Normal mode with overflow interrupts, leaving the main loop free to monitor button presses.

Requirements:

  • 8 LEDs on PORTA display binary count (0 to 255)
  • Increment count every 1 second using Timer0
  • When button0 is pressed, reset counter to 0
  • When button1 is pressed, pause/resume counting
  • No _delay_ms() in the main loop

Solution:

/*
 * Problem 1: LED Binary Counter with 1-Second Interval
 * 
 * Theory:
 * - Timer0 with prescaler 1024 gives tick period = 64 µs
 * - Overflow period = 256 ticks × 64 µs = 16.384 ms
 * - Need 61 overflows for ~1 second: 61 × 16.384ms = 999.4ms
 * - Use overflow counter to track time
 * - Reset TCNT0 to fine-tune timing
 */

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

// Global variables
volatile uint8_t overflow_count = 0;
volatile uint8_t timer_elapsed = 0;  // Set to 1 when 1 second passes
volatile uint8_t counter_value = 0;
volatile uint8_t paused = 0;

// Timer0 overflow interrupt - runs every 16.384ms
ISR(TIMER0_OVF_vect) {
    overflow_count++;
    
    // 61 overflows = 999.4 ms
    if (overflow_count >= 61) {
        overflow_count = 0;
        timer_elapsed = 1;
        
        // Compensate for the 0.6ms shortfall by resetting TCNT0
        // 0.6ms / 64µs = 9.375 ≈ 9 ticks
        TCNT0 = 256 - 9;  // Start later to extend the next overflow period
    }
}

// External interrupt for button0 (reset on pin 49 - PL0)
ISR(INT0_vect) {
    if (is_button_pressed(0)) {
        counter_value = 0;
        PORTA = counter_value;
        _delay_ms(50);  // Debounce
    }
}

void setup_pause_button(void) {
    // Button1 is on PL1 (pin 48) - use pin change interrupt
    PCICR |= (1 << PCIE2);      // Enable pin change interrupt for PORTL
    PCMSK2 |= (1 << PCINT1);    // Enable interrupt on PL1
}

ISR(PCINT2_vect) {
    if (is_button_pressed(1)) {
        paused ^= 1;  // Toggle pause state
        _delay_ms(50);
    }
}

int main(void) {
    // Initialize hardware
    leds_init();
    buttons_init();
    
    // Setup reset button (button0 on PL0) as external interrupt
    EICRB |= (1 << ISC40);      // INT4 on falling edge (PL0 is INT4?)
    // Note: On ATmega2560, PL0 is actually INT8, not INT4
    // Adjust based on actual pin mapping: PL0 = INT8
    EICRB |= (1 << ISC80);      // Falling edge on INT8
    EIMSK |= (1 << INT8);       // Enable INT8
    
    setup_pause_button();
    
    // Configure Timer0 for Normal mode
    TCCR0A = 0x00;                          // Normal mode
    TCCR0B |= (1 << CS02) | (1 << CS00);    // Prescaler 1024
    TCCR0B &= ~(1 << CS01);
    
    TIMSK0 |= (1 << TOIE0);                 // Enable overflow interrupt
    
    sei();  // Enable global interrupts
    
    while (1) {
        if (timer_elapsed && !paused) {
            timer_elapsed = 0;
            counter_value++;
            PORTA = counter_value;  // Update LEDs
        }
        
        // Main loop can perform other tasks here
    }
}

Explanation:

This solution demonstrates the core concept of using timer overflows to create long delays. The key calculation is determining how many overflows are needed for one second. With a 16MHz clock and prescaler 1024, each timer tick takes 64µs. The timer overflows every 256 ticks (16.384ms). We need 61 overflows to reach approximately 1 second (61 × 16.384ms = 999.4ms). The remaining 0.6ms is compensated by adjusting TCNT0 register to start counting from a value other than zero, effectively delaying the next overflow.

The button handling uses external interrupts to immediately respond to button presses without polling in the main loop. The paused variable allows the counter to stop incrementing while the interrupt continues to run, demonstrating how timing and user input can coexist peacefully.


Problem 2: LED Chase Effect with Variable Speed Using Button Input

Scenario: Create a Knight Rider style LED chaser (back and forth) where the speed is controlled by buttons. Button0 increases speed (decreases delay), Button1 decreases speed (increases delay). Use Timer0 to generate the timing intervals.

Requirements:

  • LEDs chase back and forth (LED0 → LED7 → LED0)
  • Button0: Increase speed (shorter interval)
  • Button1: Decrease speed (longer interval)
  • Speed range: 50ms to 1000ms
  • Use timer interrupts, not software delays

Solution:

/*
 * Problem 2: Variable Speed LED Chaser
 * 
 * Theory:
 * - Use Timer0 in CTC mode for precise interval control
 * - OCR0A determines the interval
 * - Changing OCR0A on the fly changes the speed
 * - Formula: Delay = (OCR0A + 1) × Prescaler / F_CPU
 * - With prescaler 256: OCR0A = (Delay × 16MHz / 256) - 1
 */

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

// Global variables
volatile uint8_t timer_flag = 0;
volatile uint8_t current_led = 0;
volatile int8_t direction = 1;  // 1 = forward, -1 = backward
volatile uint16_t current_delay_ms = 500;  // Start at 500ms

// Speed limits (in milliseconds)
#define MIN_DELAY_MS 50
#define MAX_DELAY_MS 1000
#define STEP_MS 50

// Calculate OCR0A value for a given delay in milliseconds
// Using prescaler 256: OCR0A = (Delay × 16000000 / 256 / 1000) - 1 = (Delay × 62.5) - 1
uint8_t delay_to_ocr(uint16_t delay_ms) {
    uint16_t ocr = (delay_ms * 625UL) / 10;  // Same as delay_ms × 62.5
    if (ocr > 255) return 255;
    if (ocr < 1) return 1;
    return (uint8_t)(ocr - 1);
}

// Timer0 Compare Match A interrupt - runs at configured interval
ISR(TIMER0_COMPA_vect) {
    timer_flag = 1;
}

// Button0 interrupt (increase speed - PL0)
ISR(INT8_vect) {  // PL0 is INT8 on ATmega2560
    if (is_button_pressed(0) && current_delay_ms > MIN_DELAY_MS) {
        current_delay_ms -= STEP_MS;
        OCR0A = delay_to_ocr(current_delay_ms);
        _delay_ms(50);  // Debounce
    }
}

// Button1 interrupt (decrease speed - PL1)
ISR(INT9_vect) {  // PL1 is INT9
    if (is_button_pressed(1) && current_delay_ms < MAX_DELAY_MS) {
        current_delay_ms += STEP_MS;
        OCR0A = delay_to_ocr(current_delay_ms);
        _delay_ms(50);
    }
}

void setup_buttons(void) {
    // Enable interrupts for PL0 (INT8) and PL1 (INT9)
    EICRB |= (1 << ISC80);   // INT8 falling edge
    EICRB |= (1 << ISC90);   // INT9 falling edge
    EIMSK |= (1 << INT8) | (1 << INT9);
}

int main(void) {
    leds_init();
    buttons_init();
    setup_buttons();
    
    // Turn off all LEDs initially
    PORTA = 0x00;
    
    // Configure Timer0 for CTC mode (Mode 2)
    TCCR0A |= (1 << WGM01);   // CTC mode
    TCCR0A &= ~(1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    // Set initial compare value for 500ms
    OCR0A = delay_to_ocr(current_delay_ms);
    
    // Prescaler 256: CS02=1, CS01=0, CS00=0
    TCCR0B |= (1 << CS02);
    TCCR0B &= ~((1 << CS01) | (1 << CS00));
    
    // Enable compare match interrupt
    TIMSK0 |= (1 << OCIE0A);
    
    sei();
    
    while (1) {
        if (timer_flag) {
            timer_flag = 0;
            
            // Turn off current LED
            led_off(current_led);
            
            // Move to next LED
            current_led += direction;
            
            // Change direction at boundaries
            if (current_led >= 7) {
                current_led = 7;
                direction = -1;
            } else if (current_led == 0 && direction == -1) {
                direction = 1;
            }
            
            // Turn on new LED
            led_on(current_led);
        }
    }
}

Explanation:

This problem introduces CTC (Clear Timer on Compare Match) mode, which is superior to overflow counting for generating precise intervals. The key advantage is that you control the exact timing by setting OCR0A, and the timer automatically resets when reaching that value.

The speed control demonstrates how you can dynamically change timer parameters at runtime. When a button is pressed, the interrupt service routine recalculates and updates OCR0A, and the next timer interval immediately uses the new value. This creates a responsive user experience without any performance penalty.

The formula OCR0A = (Delay × 62.5) - 1 comes from: 16MHz / 256 = 62,500 Hz timer clock. Each tick is 16µs. To get a delay of D milliseconds, you need (D × 1000) / 16 = D × 62.5 ticks. Since timer counts from 0 to OCR0A inclusive, we subtract 1.


Problem 3: 8-Button Combination Lock with Timer-Controlled Lockout Period

Scenario: Implement a 4-button combination lock (buttons 0,2,5,7 in sequence). After 3 failed attempts, the system locks for 10 seconds, indicated by blinking LEDs. Use Timer0 to manage the lockout period without blocking the CPU.

Requirements:

  • Correct combination: Press buttons 0, 2, 5, 7 in order
  • Each button press must be within 5 seconds of the previous
  • After 3 failed attempts, lockout for 10 seconds
  • During lockout, all LEDs blink at 2Hz
  • Lockout uses timer interrupts, not blocking delays

Solution:

/*
 * Problem 3: Combination Lock with Timer Lockout
 * 
 * Features:
 * - 4-button sequence: 0 → 2 → 5 → 7
 * - 5-second timeout between presses
 * - 3 failed attempts = 10-second lockout
 * - Blinking LED indicator during lockout
 */

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

// State machine states
typedef enum {
    STATE_WAIT_BUTTON0,
    STATE_WAIT_BUTTON2,
    STATE_WAIT_BUTTON5,
    STATE_WAIT_BUTTON7,
    STATE_UNLOCKED,
    STATE_LOCKOUT
} system_state_t;

// Global variables
volatile system_state_t system_state = STATE_WAIT_BUTTON0;
volatile uint8_t failed_attempts = 0;
volatile uint8_t timer_flag = 0;
volatile uint8_t lockout_remaining = 0;
volatile uint8_t blink_flag = 0;
volatile uint8_t lockout_blink_counter = 0;

// Timer0 Compare Match A interrupt - 1 second interval
ISR(TIMER0_COMPA_vect) {
    timer_flag = 1;
    
    if (system_state == STATE_LOCKOUT && lockout_remaining > 0) {
        lockout_remaining--;
        
        // Blink every 500ms (alternate seconds)
        blink_flag ^= 1;
        if (blink_flag) {
            PORTA = 0xFF;  // All LEDs ON
        } else {
            PORTA = 0x00;  // All LEDs OFF
        }
        
        if (lockout_remaining == 0) {
            // Lockout ended
            system_state = STATE_WAIT_BUTTON0;
            failed_attempts = 0;
            PORTA = 0x00;  // All LEDs OFF
        }
    }
}

// Button press detection and state machine
void check_buttons(void) {
    uint8_t pressed = get_first_pressed_button();
    if (pressed == 0xFF) return;  // No button pressed
    
    switch (system_state) {
        case STATE_WAIT_BUTTON0:
            if (pressed == 0) {
                system_state = STATE_WAIT_BUTTON2;
                PORTA = 0x01;  // Show progress on LED0
                // Reset timeout timer
                TCNT0 = 0;
            } else {
                // Wrong button, reset sequence
                system_state = STATE_WAIT_BUTTON0;
                PORTA = 0x00;
                failed_attempts++;
                _delay_ms(50);
            }
            break;
            
        case STATE_WAIT_BUTTON2:
            if (pressed == 2) {
                system_state = STATE_WAIT_BUTTON5;
                PORTA = 0x05;  // LED0 and LED2 ON
                TCNT0 = 0;
            } else {
                system_state = STATE_WAIT_BUTTON0;
                PORTA = 0x00;
                failed_attempts++;
                _delay_ms(50);
            }
            break;
            
        case STATE_WAIT_BUTTON5:
            if (pressed == 5) {
                system_state = STATE_WAIT_BUTTON7;
                PORTA = 0x25;  // LEDs 0,2,5 ON
                TCNT0 = 0;
            } else {
                system_state = STATE_WAIT_BUTTON0;
                PORTA = 0x00;
                failed_attempts++;
                _delay_ms(50);
            }
            break;
            
        case STATE_WAIT_BUTTON7:
            if (pressed == 7) {
                system_state = STATE_UNLOCKED;
                // Unlocked pattern: all LEDs flashing rapidly
                for (int i = 0; i < 5; i++) {
                    PORTA = 0xFF;
                    _delay_ms(100);
                    PORTA = 0x00;
                    _delay_ms(100);
                }
                system_state = STATE_WAIT_BUTTON0;
                PORTA = 0x00;
            } else {
                system_state = STATE_WAIT_BUTTON0;
                PORTA = 0x00;
                failed_attempts++;
                _delay_ms(50);
            }
            break;
            
        default:
            break;
    }
    
    // Check for lockout condition
    if (failed_attempts >= 3 && system_state != STATE_LOCKOUT) {
        system_state = STATE_LOCKOUT;
        lockout_remaining = 10;  // 10 seconds lockout
        PORTA = 0x00;
    }
    
    // Wait for button release
    while (get_first_pressed_button() != 0xFF);
    _delay_ms(50);
}

int main(void) {
    leds_init();
    buttons_init();
    PORTA = 0x00;
    
    // Configure Timer0 for CTC mode, 1 second interval
    TCCR0A |= (1 << WGM01);   // CTC mode
    TCCR0B |= (1 << CS02) | (1 << CS00);  // Prescaler 1024
    TCCR0B &= ~(1 << CS01);
    
    // OCR0A for 1 second: 16MHz / 1024 = 15625 Hz
    // 15625 ticks/sec, need 15625 ticks → OCR0A = 15625 - 1 = 15624
    // But 8-bit timer max is 255! We need a different approach.
    // Use prescaler 256: 16MHz/256 = 62500 Hz, need 62500 ticks → still too large
    // Solution: Use a smaller interval and count occurrences
    
    // Instead, use 16ms interval and count 62 occurrences for ~1 second
    TCCR0A |= (1 << WGM01);   // CTC mode
    TCCR0B |= (1 << CS02);    // Prescaler 256
    OCR0A = 249;               // 250 ticks × 16µs = 4ms interval
    
    // We'll count 250 intervals for 1 second
    TIMSK0 |= (1 << OCIE0A);
    
    sei();
    
    while (1) {
        if (timer_flag) {
            static uint16_t second_counter = 0;
            timer_flag = 0;
            second_counter++;
            
            if (second_counter >= 250) {  // 250 × 4ms = 1000ms
                second_counter = 0;
                // This is where the 1-second elapsed logic goes
                if (system_state != STATE_LOCKOUT) {
                    // Check for timeout between button presses
                    static uint8_t timeout_counter = 0;
                    timeout_counter++;
                    if (timeout_counter >= 5 && system_state != STATE_WAIT_BUTTON0) {
                        // Timeout occurred - reset sequence
                        system_state = STATE_WAIT_BUTTON0;
                        PORTA = 0x00;
                        failed_attempts++;
                        timeout_counter = 0;
                    }
                }
            }
        }
        
        check_buttons();
    }
}

Explanation:

This problem demonstrates a complex state machine combined with timer-based timeouts. The key challenge is that an 8-bit timer cannot directly generate a 1-second interval because the maximum OCR value is 255. The solution is to use a smaller interval (4ms in this case) and count how many times it occurs.

The prescaler 256 gives a timer clock of 62.5kHz (16µs per tick). Setting OCR0A = 249 creates a 4ms interval (250 ticks × 16µs = 4000µs). By counting 250 such intervals in software, we achieve exactly 1 second.

During lockout, the timer interrupt handles both the countdown and the LED blinking simultaneously, demonstrating how a single timer can manage multiple timing tasks. The state machine ensures that button presses are only valid in the correct sequence, and any wrong button resets the sequence and increments the failure counter.


Problem Set 2: PWM and Duty Cycle Control

Problem 4: LED Brightness Control with PWM Using Buttons

Scenario: Control the brightness of all 8 LEDs simultaneously using PWM from Timer0. Button0 increases brightness, Button1 decreases brightness. The brightness level (0-255) should be displayed in binary on the LEDs.

Requirements:

  • Use Timer0 Fast PWM mode on OC0A (Pin 6) but drive all LEDs through a transistor array
  • Button0: Increase brightness by 16 (20 steps total)
  • Button1: Decrease brightness by 16
  • Display current brightness value (0-255) on the 8 LEDs
  • Smooth brightness transitions without flicker

Solution:

/*
 * Problem 4: Global LED Brightness Control with PWM
 * 
 * Circuit note: Use a single NPN transistor (e.g., 2N2222) to drive all
 * 8 LEDs from the PWM pin. Connect PWM pin to transistor base through
 * 1kΩ resistor, emitter to GND, collector to LED cathodes.
 * LED anodes to VCC through individual 220Ω resistors.
 */

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

// Global variables
volatile uint8_t current_brightness = 128;  // Start at half brightness
volatile uint8_t brightness_changed = 1;

// Timer0 Compare Match B interrupt - runs at PWM frequency
ISR(TIMER0_COMPB_vect) {
    // Used for fine timing if needed
}

// Button0 interrupt (increase brightness)
ISR(INT8_vect) {
    if (is_button_pressed(0) && current_brightness < 240) {
        current_brightness += 16;
        brightness_changed = 1;
        _delay_ms(50);
    }
}

// Button1 interrupt (decrease brightness)
ISR(INT9_vect) {
    if (is_button_pressed(1) && current_brightness >= 16) {
        current_brightness -= 16;
        brightness_changed = 1;
        _delay_ms(50);
    }
}

void setup_timer0_pwm(void) {
    // Pin 6 (OC0A/PD6) as output for PWM signal
    DDRD |= (1 << 6);
    
    // Fast PWM Mode 3: WGM02=0, WGM01=1, WGM00=1
    TCCR0A |= (1 << WGM01) | (1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    // Non-inverting PWM on OC0A: Clear on compare, set at BOTTOM
    TCCR0A |= (1 << COM0A1);
    TCCR0A &= ~(1 << COM0A0);
    
    // Prescaler 64 for ~977 Hz PWM (no visible flicker)
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TCCR0B &= ~(1 << CS02);
    
    // Initialize duty cycle
    OCR0A = current_brightness;
    
    // Enable compare interrupt if needed
    // TIMSK0 |= (1 << OCIE0B);
}

int main(void) {
    leds_init();
    buttons_init();
    
    // Display initial brightness on LEDs
    PORTA = current_brightness;
    
    setup_timer0_pwm();
    
    // Setup button interrupts
    EICRB |= (1 << ISC80) | (1 << ISC90);  // Falling edge on INT8 and INT9
    EIMSK |= (1 << INT8) | (1 << INT9);
    
    sei();
    
    while (1) {
        if (brightness_changed) {
            brightness_changed = 0;
            OCR0A = current_brightness;      // Update PWM duty cycle
            PORTA = current_brightness;      // Display brightness on LEDs
        }
    }
}

Explanation:

This problem introduces Fast PWM mode, which is ideal for generating analog-like outputs from digital pins. The LED brightness is controlled by varying the duty cycle of the PWM signal. A higher duty cycle means the LED is ON for a larger percentage of each cycle, appearing brighter.

The PWM frequency is calculated as: 16MHz / (64 × 256) = 976.5625 Hz. This frequency is high enough that the LED’s persistence of vision makes it appear continuously lit at varying brightness levels, with no visible flicker.

The hardware configuration uses a single transistor to drive all LEDs from the PWM pin. This is more efficient than using separate PWM channels for each LED when they all need the same brightness level. The 8 LEDs serve as a binary display showing the current brightness value (0-255), giving visual feedback of the setting.


Problem 5: Individual LED Dimming Using Timer0 and Timer2 PWM Channels

Scenario: You need to independently control the brightness of 6 LEDs using both Timer0 (2 channels) and Timer2 (2 channels). Use buttons to select which LED to control, then adjust its brightness with two other buttons.

Requirements:

  • 6 PWM outputs available: OC0A, OC0B (Timer0) and OC2A, OC2B (Timer2)
  • Buttons 0-3 select which LED to control
  • Button4: Increase brightness (+16)
  • Button5: Decrease brightness (-16)
  • Button6: Reset all to 50% brightness
  • Button7: Toggle between smooth and step mode

Solution:

/*
 * Problem 5: Individual LED Dimming on 6 Channels
 * 
 * Hardware mapping:
 * - Timer0 OC0A (PD6/Arduino pin 6) → LED0
 * - Timer0 OC0B (PD5/Arduino pin 5) → LED1
 * - Timer2 OC2A (PB4/Arduino pin 10) → LED2
 * - Timer2 OC2B (PB5/Arduino pin 9?) Actually PB5 is OC1A, need correct mapping
 * 
 * Correct ATmega2560 PWM pins:
 * - OC0A: PD6 (Pin 6) → LED0
 * - OC0B: PD5 (Pin 5) → LED1
 * - OC2A: PB4 (Pin 10) → LED2
 * - OC2B: PB6? Wait, check datasheet: OC2B is PB7? No.
 *   Actually Timer2 OC2B is PD3 (Pin 2) on Arduino Mega.
 *   Let's use: OC2B = PD3 (Pin 2) → LED3
 * 
 * For 6 outputs, we'll use:
 * LED0: PD6 (OC0A) - Pin 6
 * LED1: PD5 (OC0B) - Pin 5
 * LED2: PB4 (OC2A) - Pin 10
 * LED3: PD3 (OC2B) - Pin 2
 * LED4: PB5 (OC1A) - Pin 11 (Timer1)
 * LED5: PB6 (OC1B) - Pin 12 (Timer1)
 */

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

// Brightness values for 6 LEDs
volatile uint8_t brightness[6] = {128, 128, 128, 128, 128, 128};
volatile uint8_t selected_led = 0;
volatile uint8_t smooth_mode = 0;
volatile uint8_t update_display = 1;

// Button mappings
// Button0-5: Select LED 0-5
// Button6: Increase brightness
// Button7: Decrease brightness
// Button8: Reset all to 128
// Button9: Toggle smooth mode

void update_pwm_outputs(void) {
    OCR0A = brightness[0];   // LED0
    OCR0B = brightness[1];   // LED1
    OCR2A = brightness[2];   // LED2
    OCR2B = brightness[3];   // LED3
    OCR1A = brightness[4];   // LED4
    OCR1B = brightness[5];   // LED5
}

void setup_pwm_timers(void) {
    // === Timer0 (LED0 and LED1) ===
    // Fast PWM Mode 3
    TCCR0A |= (1 << WGM01) | (1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    // Non-inverting PWM on both channels
    TCCR0A |= (1 << COM0A1) | (1 << COM0B1);
    TCCR0A &= ~((1 << COM0A0) | (1 << COM0B0));
    // Prescaler 64
    TCCR0B |= (1 << CS01) | (1 << CS00);
    
    // Output pins for Timer0
    DDRD |= (1 << 6) | (1 << 5);  // PD6 and PD5 as outputs
    
    // === Timer2 (LED2 and LED3) ===
    TCCR2A |= (1 << WGM21) | (1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    TCCR2A |= (1 << COM2A1) | (1 << COM2B1);
    TCCR2A &= ~((1 << COM2A0) | (1 << COM2B0));
    TCCR2B |= (1 << CS21) | (1 << CS20);
    
    // Output pins for Timer2
    DDRB |= (1 << 4);   // PB4 (pin 10) for OC2A
    DDRD |= (1 << 3);   // PD3 (pin 2) for OC2B
    
    // === Timer1 (LED4 and LED5) - 16-bit timer ===
    // Fast PWM mode 14 (ICR1 top)
    TCCR1A |= (1 << WGM11);
    TCCR1B |= (1 << WGM12) | (1 << WGM13);
    TCCR1A |= (1 << COM1A1) | (1 << COM1B1);
    ICR1 = 255;  // 8-bit resolution
    TCCR1B |= (1 << CS11) | (1 << CS10);  // Prescaler 64
    
    DDRB |= (1 << 5) | (1 << 6);  // PB5 (pin 11), PB6 (pin 12)
}

// Button press handler
void process_button(uint8_t button_num) {
    switch (button_num) {
        case 0: case 1: case 2: case 3: case 4: case 5:
            selected_led = button_num;
            update_display = 1;
            break;
        case 6:  // Increase brightness
            if (brightness[selected_led] < 240) {
                brightness[selected_led] += 16;
                update_pwm_outputs();
                update_display = 1;
            }
            break;
        case 7:  // Decrease brightness
            if (brightness[selected_led] >= 16) {
                brightness[selected_led] -= 16;
                update_pwm_outputs();
                update_display = 1;
            }
            break;
        case 8:  // Reset all
            for (int i = 0; i < 6; i++) {
                brightness[i] = 128;
            }
            update_pwm_outputs();
            update_display = 1;
            break;
        case 9:  // Toggle smooth mode
            smooth_mode ^= 1;
            update_display = 1;
            break;
        default:
            break;
    }
}

int main(void) {
    leds_init();
    buttons_init();
    setup_pwm_timers();
    
    // Initialize PWM outputs
    update_pwm_outputs();
    
    // Display initial selected LED on LED display
    PORTA = (1 << selected_led);
    
    // Setup for button scanning using timer (instead of interrupts)
    // Use Timer2 CTC for button scanning every 10ms
    
    sei();
    
    while (1) {
        // Scan buttons
        for (int i = 0; i < 10; i++) {
            if (debounce_button(i)) {
                process_button(i);
                _delay_ms(100);
            }
        }
        
        // Update LED display to show selected LED and its brightness
        if (update_display) {
            update_display = 0;
            
            if (smooth_mode) {
                // In smooth mode, use binary display for brightness
                PORTA = brightness[selected_led];
            } else {
                // In step mode, show which LED is selected
                PORTA = (1 << selected_led);
            }
        }
   

Explanation:

This problem demonstrates how to use multiple timers simultaneously to generate multiple PWM signals. The ATmega2560’s rich timer suite allows up to 15 independent PWM channels, but here we focus on Timer0 and Timer2 (8-bit timers) plus Timer1 (16-bit) to achieve 6 channels.

The key insight is that each timer can control multiple outpµt compare channels (OCRnA and OCRnB), but they share the same frequency (determined by the timer’s top value and prescaler). For LED dimming, this is acceptable because all LEDs can operate at the same PWM frequency.

The button handling uses a polling approach with debouncing, which is simpler than interrupt-based handling for many buttons. The smooth_mode feature demonstrates how the LED display can show different types of information (selected LED vs. brightness value) based on the current mode.


Problem 6: Breathing LED Effect with Adjustable Period Using Timer2

Scenario: Create a “breathing” LED effect where the LED smoothly fades in and out like a heartbeat. The period of the breathing cycle should be adjustable using buttons (2-10 seconds). Use Timer2 Phase Correct PWM for smooth transitions.

Requirements:

  • Single LED (LED0) fades in and out continuously
  • Breathing period adjustable from 2 to 10 seconds
  • Button0: Increase period
  • Button1: Decrease period
  • Display current period (in seconds) on LEDs 0-7 as binary value
  • Use Phase Correct PWM for smoother transitions

Solution:

/*
 * Problem 6: Breathing LED with Adjustable Period
 * 
 * Theory:
 * - Phase Correct PWM gives symmetrical waveform, ideal for smooth fading
 * - Use Timer2 in Phase Correct PWM mode
 * - Change brightness according to sine wave pattern
 * - Timer interrupt updates brightness at fixed rate
 */

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

// Sine lookup table for 64 steps (0-255 brightness)
const uint8_t sine_table[64] = {
    128, 140, 153, 165, 177, 188, 199, 209,
    218, 226, 234, 241, 247, 252, 255, 258,
    260, 261, 261, 260, 258, 255, 252, 247,
    241, 234, 226, 218, 209, 199, 188, 177,
    165, 153, 140, 128, 115, 102, 90, 78,
    67, 56, 46, 37, 29, 21, 14, 8,
    3, 0, 0, 0, 3, 8, 14, 21,
    29, 37, 46, 56, 67, 78, 90, 102
};

// Global variables
volatile uint8_t table_index = 0;
volatile uint8_t step_counter = 0;
volatile uint16_t period_ms = 4000;  // Start at 4 seconds
volatile uint8_t period_changed = 1;
volatile uint8_t button_pressed_flag = 0;

// Timer2 Compare Match A interrupt - updates brightness
ISR(TIMER2_COMPA_vect) {
    step_counter++;
    
    // Calculate how many steps per cycle based on period
    // Update rate is 50Hz (every 20ms)
    // Steps per cycle = (period_seconds × 50) / 2 (because sine table has 64 steps,
    // but breathing cycle needs 2 passes through table - fade in and out)
    
    static uint16_t steps_per_step = 1;
    
    if (period_changed) {
        period_changed = 0;
        // 64 steps × 2 = 128 steps per full breath cycle
        // Each step at 50Hz = 20ms per update
        // Total time = 128 × 20ms × steps_per_step = 2560ms × steps_per_step
        // Solve for steps_per_step = period_ms / 2560
        steps_per_step = period_ms / 40;  // 2560ms / 64? Let me recalc:
        // Actually: 64 steps in table, each shown for X updates
        // Total updates per cycle = 64 × X
        // Each update = 20ms
        // Period = 64 × X × 20ms = 1280ms × X
        // So X = period_ms / 1280
        if (steps_per_step < 1) steps_per_step = 1;
    }
    
    if (step_counter >= steps_per_step) {
        step_counter = 0;
        table_index = (table_index + 1) % 64;
        
        // Update PWM duty cycle
        OCR2A = sine_table[table_index];
        
        // Also display current brightness on LED bank
        PORTA = sine_table[table_index];
    }
}

// Button debouncing and handling
void check_buttons(void) {
    if (!button_pressed_flag) {
        if (is_button_pressed(0) && period_ms < 10000) {
            period_ms += 500;
            period_changed = 1;
            button_pressed_flag = 1;
            // Update LED display to show period
            PORTA = (uint8_t)(period_ms / 40);  // Display period/40 as binary
        }
        else if (is_button_pressed(1) && period_ms > 2000) {
            period_ms -= 500;
            period_changed = 1;
            button_pressed_flag = 1;
            PORTA = (uint8_t)(period_ms / 40);
        }
    }
    
    if (get_first_pressed_button() == 0xFF) {
        button_pressed_flag = 0;
    }
}

int main(void) {
    leds_init();
    buttons_init();
    
    // Configure Timer2 for Phase Correct PWM on OC2A (LED2/Pin 10)
    DDRB |= (1 << 4);  // PB4 (pin 10) as output
    
    // Phase Correct PWM Mode 1 (8-bit, TOP=255)
    TCCR2A |= (1 << WGM20);      // WGM20=1
    TCCR2A &= ~(1 << WGM21);     // WGM21=0
    TCCR2B &= ~(1 << WGM22);     // WGM22=0
    
    // Non-inverting PWM
    TCCR2A |= (1 << COM2A1);
    TCCR2A &= ~(1 << COM2A0);
    
    // Prescaler 64 for ~977 Hz PWM
    TCCR2B |= (1 << CS21) | (1 << CS20);
    TCCR2B &= ~(1 << CS22);
    
    // Setup Timer2 for 20ms CTC interrupt (to update brightness)
    // Use separate timer for timing updates
    // Actually, we'll use Timer0 for the update rate
    
    // Configure Timer0 for 20ms interrupt
    TCCR0A |= (1 << WGM01);      // CTC mode
    TCCR0B |= (1 << CS01) | (1 << CS00);  // Prescaler 64
    OCR0A = 249;                  // 250 ticks × 64 µs = 16ms (close to 20ms)
    
    TIMSK0 |= (1 << OCIE0A);     // Enable compare interrupt
    
    // Display initial period
    PORTA = (uint8_t)(period_ms / 40);
    
    sei();
    
    while (1) {
        check_buttons();
    }
}

Explanation:

This problem introduces Phase Correct PWM, which is superior to Fast PWM for applications requiring smooth transitions because the waveform is symmetrical, reducing harmonic distortion. The breathing effect is achieved by varying the brightness according to a sine wave pattern stored in a lookup table.

The sine table contains 64 steps that represent one complete cycle of a sine wave (0° to 360°), but scaled to fit in a breathing pattern (fade in to max brightness, then fade out). The timer interrupt updates the brightness at a fixed rate (approximately 50Hz), and the steps_per_step variable controls how many update cycles each sine table entry is displayed, effectively controlling the overall period of the breathing cycle.

The button handling allows the user to adjust the period from 2 to 10 seconds in 0.5-second increments. The current period is displayed in binary on the 8 LEDs, providing immediate visual feedback.


Problem Set 3: External Event Counting and Pulse Measurement

Problem 7: Real-Time RPM Counter Using External Timer Input

Scenario: A rotating machine has a hall effect sensor that generates a pulse per revolution. Connect this sensor to the T0 input (Timer0 external counter pin, PL0/Arduino pin 49) and measure RPM. Use the LEDs to display the RPM value in binary.

Requirements:

  • External pulses on T0 pin (PL0/button0 pin – shared with button0)
  • Count pulses for exactly 1 second using Timer2
  • Calculate RPM = (pulse_count × 60) / time_seconds
  • Display RPM (0-255) on LEDs
  • Button1 resets the display
  • Button2 toggles between RPM and raw pulse count display

Solution:

/*
 * Problem 7: Real-Time RPM Counter Using Timer0 as External Counter
 * 
 * Hardware notes:
 * - Connect sensor output to PL0 (Arduino pin 49)
 * - This pin is shared with button0, so remove button0 for this project
 * - Sensor should output 0-5V logic levels, active low or high
 * 
 * Theory:
 * - Timer0 in counter mode counts external pulses on T0 pin
 * - Timer2 in CTC mode generates 1-second gate time
 * - Read TCNT0 after 1 second to get pulses per second
 * - RPM = pulses_per_second × 60 (if 1 pulse per revolution)
 */

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

volatile uint16_t rpm = 0;
volatile uint8_t measurement_ready = 0;
volatile uint8_t display_mode = 0;  // 0 = RPM, 1 = Raw count
volatile uint8_t lcd_data = 0;

// Timer2 Compare Match A interrupt - 1 second gate
ISR(TIMER2_COMPA_vect) {
    // Read the pulse count from Timer0
    uint16_t pulse_count = TCNT0;
    
    // Calculate RPM (60 seconds per minute)
    rpm = (pulse_count * 60UL);
    
    // Reset Timer0 counter for next measurement
    TCNT0 = 0;
    
    measurement_ready = 1;
}

// Button1 interrupt (reset display - PL1)
ISR(INT9_vect) {
    // Reset is automatic
    _delay_ms(50);
}

// Button2 interrupt (toggle display mode - PL2)
ISR(INT10_vect) {
    display_mode ^= 1;
    _delay_ms(50);
}

void setup_timer0_counter(void) {
    // Configure Timer0 as external counter
    TCCR0A = 0x00;                      // Normal mode
    
    // External clock on T0 pin (PL0), falling edge
    // CS02=1, CS01=1, CS00=0 → External clock, falling edge
    TCCR0B |= (1 << CS02) | (1 << CS01);
    TCCR0B &= ~(1 << CS00);
    
    // Initialize counter to 0
    TCNT0 = 0;
}

void setup_timer2_one_second(void) {
    // Timer2 in CTC mode
    TCCR2A |= (1 << WGM21);             // CTC mode
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Prescaler 1024: 16MHz/1024 = 15625 Hz
    // Need 15625 ticks for 1 second → OCR2A = 15625 - 1 = 15624
    // But 8-bit timer max is 255! Need different approach.
    
    // Instead, use prescaler 128 and count multiple overflows
    // 16MHz/128 = 125000 Hz, each tick = 8µs
    // 256 ticks = 2.048ms overflow
    // 489 overflows = 1001.5ms (close enough)
    
    TCCR2B |= (1 << CS22);              // Prescaler 128
    TCCR2B &= ~((1 << CS21) | (1 << CS20));
    
    // Use overflow interrupts instead
    TIMSK2 |= (1 << TOIE2);
}

volatile uint16_t overflow_count = 0;

ISR(TIMER2_OVF_vect) {
    overflow_count++;
    if (overflow_count >= 489) {
        overflow_count = 0;
        
        // This is our 1-second trigger
        uint16_t pulse_count = TCNT0;
        rpm = (pulse_count * 60UL);
        TCNT0 = 0;
        measurement_ready = 1;
    }
}

int main(void) {
    // Don't initialize LEDs as usual since PORTA is used for display
    DDRA = 0xFF;        // All PORTA as outputs
    PORTA = 0x00;
    
    // Initialize buttons (but note: button0 pin is used as counter input)
    // Only buttons 1 and 2 are used
    DDRL &= ~((1 << 1) | (1 << 2));  // PL1, PL2 as inputs
    PORTL |= (1 << 1) | (1 << 2);    // Pull-ups enabled
    
    // Setup external interrupts for buttons
    EICRB |= (1 << ISC90);   // INT9 falling edge
    EIMSK |= (1 << INT9);
    
    EICRB |= (1 << ISC100);  // INT10 falling edge
    EIMSK |= (1 << INT10);
    
    setup_timer0_counter();
    setup_timer2_one_second();
    
    sei();
    
    while (1) {
        if (measurement_ready) {
            measurement_ready = 0;
            
            if (display_mode == 0) {
                // Display RPM (clamp to 0-255 for LED display)
                if (rpm > 255) {
                    lcd_data = 255;
                } else {
                    lcd_data = (uint8_t)rpm;
                }
            } else {
                // Display raw pulse count from last second
                // Note: pulse count is lost after reading, store it
                static uint16_t last_pulse_count = 0;
                last_pulse_count = rpm / 60;  // Reverse calculation
                if (last_pulse_count > 255) {
                    lcd_data = 255;
                } else {
                    lcd_data = (uint8_t)last_pulse_count;
                }
            }
            
            PORTA = lcd_data;
        }
    }
}

Explanation:

This problem demonstrates the “counter” mode of Timer0, where the timer increments based on external pulses rather than the internal clock. This is perfect for measuring frequency or counting events like revolutions, button presses, or encoder pulses.

The key challenge is that the 8-bit timer can only count up to 256 pulses before overflowing. For RPM measurement, if the machine spins faster than 256 RPM, we need to count overflows as well. The solution uses Timer2 to generate a precise 1-second gate time, during which Timer0 counts external pulses.

The RPM calculation is straightforward: pulses per second × 60 = RPM (assuming one pulse per revolution). For higher RPM applications, you would need to implement overflow counting using the TOV0 flag to count multiple overflows during the gate period.


Problem 8: Pulse Width Measurement of External Signal

Scenario: You need to measure the pulse width (high time) of an external signal connected to the T0 input pin. This could be used for reading RC receiver signals, ultrasonic distance sensors, or measuring duty cycles. Display the measured pulse width in microseconds on the LEDs.

Requirements:

  • Measure high pulse width on T0 pin (PL0)
  • Use Timer2 in CTC mode to measure time
  • Display pulse width in microseconds (0-255 µs range)
  • Button0 freezes the current reading
  • Button1 toggles between high pulse and low pulse measurement

Solution:

/*
 * Problem 8: External Pulse Width Measurement
 * 
 * Hardware: Connect signal to PL0 (pin 49)
 * Signal should be 0-5V logic level
 * 
 * Theory:
 * - Use Timer2 to measure elapsed time
 * - Start timer on rising edge, capture on falling edge
 * - Use Pin Change Interrupts to detect edges
 */

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

volatile uint8_t measured_width = 0;      // Pulse width in microseconds
volatile uint8_t measurement_ready = 0;
volatile uint8_t frozen_value = 0;
volatile uint8_t freeze = 0;
volatile uint8_t measure_high_pulse = 1;  // 1 = high pulse, 0 = low pulse
volatile uint32_t start_time = 0;
volatile uint8_t measurement_in_progress = 0;

// Pin Change Interrupt for PORTL (PL0 is on PORTL)
ISR(PCINT2_vect) {
    uint8_t pin_state = PINL & (1 << 0);  // Read PL0 state
    
    if (measure_high_pulse) {
        // Measuring high pulse
        if (pin_state && !measurement_in_progress) {
            // Rising edge detected - start measurement
            start_time = TCNT2;
            measurement_in_progress = 1;
        }
        else if (!pin_state && measurement_in_progress) {
            // Falling edge detected - end measurement
            uint32_t end_time = TCNT2;
            uint32_t elapsed_ticks = end_time - start_time;
            
            // Convert ticks to microseconds
            // Timer2 with prescaler 8: 16MHz/8 = 2MHz, 1 tick = 0.5 µs
            measured_width = (uint8_t)(elapsed_ticks / 2);
            measurement_ready = 1;
            measurement_in_progress = 0;
        }
    } else {
        // Measuring low pulse (opposite polarity)
        if (!pin_state && !measurement_in_progress) {
            // Falling edge detected - start measurement
            start_time = TCNT2;
            measurement_in_progress = 1;
        }
        else if (pin_state && measurement_in_progress) {
            // Rising edge detected - end measurement
            uint32_t end_time = TCNT2;
            uint32_t elapsed_ticks = end_time - start_time;
            measured_width = (uint8_t)(elapsed_ticks / 2);
            measurement_ready = 1;
            measurement_in_progress = 0;
        }
    }
}

// Button0 interrupt (freeze display)
ISR(INT9_vect) {  // PL1 (pin 48)
    freeze ^= 1;
    if (freeze) {
        frozen_value = measured_width;
    }
    _delay_ms(50);
}

// Button1 interrupt (toggle high/low measurement)
ISR(INT10_vect) {  // PL2 (pin 47)
    measure_high_pulse ^= 1;
    measurement_in_progress = 0;  // Reset any ongoing measurement
    _delay_ms(50);
}

void setup_timer2_for_timing(void) {
    // Normal mode, prescaler 8 for 0.5µs resolution
    TCCR2A = 0x00;
    TCCR2B = 0x00;
    
    // Prescaler 8: CS22=0, CS21=0, CS20=1
    TCCR2B |= (1 << CS20);
    
    // Clear timer
    TCNT2 = 0;
}

void setup_pin_change_interrupt(void) {
    // Enable pin change interrupt for PORTL
    PCICR |= (1 << PCIE2);
    
    // Enable interrupt for PL0
    PCMSK2 |= (1 << PCINT0);
}

int main(void) {
    // Setup LED display
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Setup button pins (PL1, PL2) - PL0 is input signal
    DDRL &= ~((1 << 1) | (1 << 2));
    PORTL |= (1 << 1) | (1 << 2);
    
    // Setup external interrupts for buttons
    EICRB |= (1 << ISC90) | (1 << ISC100);
    EIMSK |= (1 << INT9) | (1 << INT10);
    
    // Setup PL0 as input with no pull-up (external signal drives it)
    DDRL &= ~(1 << 0);
    PORTL &= ~(1 << 0);  // No pull-up
    
    setup_timer2_for_timing();
    setup_pin_change_interrupt();
    
    sei();
    
    while (1) {
        if (measurement_ready && !freeze) {
            measurement_ready = 0;
            PORTA = measured_width;
        } else if (freeze) {
            PORTA = frozen_value;
        }
        
        // Indicate measurement mode on LED7 (most significant bit)
        if (measure_high_pulse) {
            PORTA |= (1 << 7);   // Set bit7 to indicate high-pulse mode
        } else {
            PORTA &= ~(1 << 7);  // Clear bit7 for low-pulse mode
        }
    }
}

Explanation:

This problem introduces pin change interrupts and demonstrates how to measure time between events. The pulse width measurement is a classic embedded systems task used in reading RC receiver signals, ultrasonic sensors (HC-SR04), and frequency analysis.

Timer2 is configured with a prescaler of 8, giving a resolution of 0.5 microseconds per tick (16MHz/8 = 2MHz, period = 500ns). This allows pulse widths from 0 to 127 microseconds (0-255 ticks) to be measured accurately. For longer pulses, a larger prescaler or a 16-bit timer would be needed.

The pin change interrupt triggers on any edge (rising or falling) of PL0. Inside the ISR, the software tracks the state machine: waiting for the first edge, measuring, and capturing on the opposite edge. The measure_high_pulse flag allows the user to measure either the high period or low period of the incoming signal.

The display shows the measured pulse width in microseconds, and the freeze button allows the user to capture and hold a reading while the measurement continues in the background.


Problem 9: Frequency Counter Using Timer0 and Timer1

Scenario: Build a frequency counter capable of measuring signals from 1Hz to 62.5kHz using Timer0 as the counter and Timer1 for precise time base. Display the frequency on the LEDs in binary.

Requirements:

  • Measure frequency on T0 pin (PL0)
  • Range: 1Hz to 62.5kHz
  • Update display every second
  • Button0: Display frequency in Hz (0-255 range, with scaling)
  • Button1: Display period in milliseconds
  • Use Timer1 (16-bit) for accurate gate timing

Solution:

/*
 * Problem 9: Frequency Counter Using Timer0 and Timer1
 * 
 * Features:
 * - Frequency range: 1Hz to 62.5kHz (8-bit timer max 256 ticks at 62.5kHz gives ~4ms)
 * - Actually, with 1-second gate, max count is 65,535 (16-bit counter)
 * - Use both Timer0 and Timer1 to achieve wider range
 * 
 * Circuit: External signal to PL0 (pin 49)
 */

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

// Global variables
volatile uint32_t frequency = 0;
volatile uint8_t measurement_ready = 0;
volatile uint16_t period_ms = 0;
volatile uint8_t display_mode = 0;  // 0 = freq, 1 = period
volatile uint32_t overflow_count = 0;
volatile uint16_t final_count = 0;

// Timer1 overflow interrupt - 16-bit timer counts external clock source
ISR(TIMER1_OVF_vect) {
    overflow_count++;
}

// Timer2 Compare A interrupt - 1 second gate time
ISR(TIMER2_COMPA_vect) {
    // Disable counting
    TCCR1B = 0;
    
    // Calculate total counts: (overflow_count × 65536) + TCNT1
    final_count = (overflow_count * 65536UL) + TCNT1;
    
    // Frequency = counts per second
    frequency = final_count;
    
    // Calculate period in milliseconds (if frequency > 0)
    if (frequency > 0) {
        period_ms = 1000 / frequency;
    } else {
        period_ms = 0;
    }
    
    // Reset for next measurement
    overflow_count = 0;
    TCNT1 = 0;
    
    // Re-enable counting
    TCCR1B |= (1 << CS10);  // External clock on T1 pin
    
    measurement_ready = 1;
}

void setup_timer1_as_counter(void) {
    // Timer1 in normal mode, external clock on T1 pin (PL1 - pin 48)
    TCCR1A = 0x00;
    
    // External clock on T1 (PL1), rising edge
    // CS12=0, CS11=1, CS10=0 (falling edge)
    // CS12=0, CS11=1, CS10=1 (rising edge)
    TCCR1B |= (1 << CS11) | (1 << CS10);  // Rising edge
    TCCR1B &= ~(1 << CS12);
    
    // Enable overflow interrupt
    TIMSK1 |= (1 << TOIE1);
    
    TCNT1 = 0;
}

void setup_timer2_one_second_gate(void) {
    // Timer2 in CTC mode
    TCCR2A |= (1 << WGM21);
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Prescaler 128: 16MHz/128 = 125000 Hz, period = 8µs
    // To get 1 second: 125000 ticks → OCR2A = 124
    // 125000 ticks × 8µs = 1,000,000 µs = 1 second
    // Wait, 125 ticks × 8µs = 1ms, OCR2A=124 gives 125 ticks
    // Need 1000 interrupts for 1 second
    
    TCCR2B |= (1 << CS22);
    TCCR2B &= ~((1 << CS21) | (1 << CS20));
    OCR2A = 124;  // 125 ticks × 8µs = 1ms
    
    TIMSK2 |= (1 << OCIE2A);
}

volatile uint16_t ms_counter = 0;

// Timer2 compare interrupt - runs every 1ms
ISR(TIMER2_COMPA_vect) {
    ms_counter++;
    if (ms_counter >= 1000) {
        ms_counter = 0;
        
        // This is our 1-second gate - do measurement here
        static uint8_t meas_in_progress = 0;
        
        if (!meas_in_progress) {
            meas_in_progress = 1;
            
            // Start counting
            overflow_count = 0;
            TCNT1 = 0;
            TCCR1B |= (1 << CS11) | (1 << CS10);
        } else {
            meas_in_progress = 0;
            
            // Stop counting
            TCCR1B = 0;
            
            // Calculate total counts
            final_count = (overflow_count * 65536UL) + TCNT1;
            frequency = final_count;
            
            if (frequency > 0) {
                period_ms = 1000 / frequency;
                
                // Handle frequency > 255 by using scaling
                if (frequency > 255 && display_mode == 0) {
                    // Show upper 8 bits for frequencies > 255
                    PORTA = (uint8_t)(frequency >> 8);
                } else if (display_mode == 0) {
                    PORTA = (uint8_t)frequency;
                } else {
                    // Display period with scaling (0-255ms)
                    if (period_ms > 255) {
                        PORTA = 255;
                    } else {
                        PORTA = (uint8_t)period_ms;
                    }
                }
            } else {
                PORTA = 0x00;  // No signal
            }
        }
    }
}

// Button interrupt handlers
ISR(INT9_vect) {  // PL1 - toggle display mode
    display_mode ^= 1;
    _delay_ms(50);
}

int main(void) {
    // LED display on PORTA
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Setup buttons (PL1 and PL2 only, PL0 is signal input)
    DDRL &= ~((1 << 1) | (1 << 2));
    PORTL |= (1 << 1) | (1 << 2);
    
    // Setup external interrupts
    EICRB |= (1 << ISC90) | (1 << ISC100);
    EIMSK |= (1 << INT9) | (1 << INT10);
    
    // Configure signal input on PL0 (T1 timer input)
    DDRL &= ~(1 << 0);
    PORTL &= ~(1 << 0);
    
    setup_timer1_as_counter();
    setup_timer2_one_second_gate();
    
    sei();
    
    while (1) {
        // Main loop idle - all work done in interrupts
    }
}

Explanation:

This problem combines both 8-bit and 16-bit timer concepts for a practical frequency measurement application. Timer1 (16-bit) is used as an external counter because it can count up to 65,535 before overflowing, providing a much wider range than an 8-bit counter. Timer2 generates the precise 1-second gate time using CTC mode.

The frequency calculation handles overflow events by counting how many times the 16-bit counter overflows during the gate period. The total count is (overflow_count × 65536) + TCNT1, which can represent up to 4,294,967,295 counts per second (theoretically). Practically, the maximum frequency is limited by the signal’s rise time and the microcontroller’s maximum input frequency (~8MHz for the ATmega2560).

The display shows the frequency in Hz for values up to 255, and for higher frequencies, it shows the upper 8 bits (giving resolution of 256Hz per step). The user can toggle to period display mode to see the pulse period in milliseconds.


Problem Set 4: Interrupt-Driven Button Handling

Problem 10: 8-Button Music Sequencer with Timer-Based Timing

Scenario: Create an 8-step music sequencer where each button corresponds to a musical note. When a button is pressed, that note plays for 250ms. Use Timer0 to generate the timing for note duration and for tempo synchronization.

Requirements:

  • Buttons 0-7 produce tones (frequencies) on a speaker connected to OC0A (pin 6)
  • Pressing a button plays the note for 250ms using timer interrupt
  • Multiple simultaneous button presses play all pressed notes (polyphony using time-division)
  • Use Timer2 to scan buttons at 100Hz and detect presses
  • Display currently playing notes on LEDs

Solution:

/*
 * Problem 10: 8-Button Music Sequencer
 * 
 * Hardware:
 * - 8 buttons on PL0-PL7
 * - 8 LEDs on PA0-PA7
 * - Speaker/buzzer on OC0A (pin 6) with transistor driver
 * 
 * Note frequencies (octave 4):
 * Button0: C4 (261Hz)
 * Button1: D4 (294Hz)
 * Button2: E4 (329Hz)
 * Button3: F4 (349Hz)
 * Button4: G4 (392Hz)
 * Button5: A4 (440Hz)
 * Button6: B4 (494Hz)
 * Button7: C5 (523Hz)
 */

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

// Musical note frequencies (Hz)
const uint16_t note_freq[8] = {261, 294, 329, 349, 392, 440, 494, 523};

// OCR0A values for CTC mode to generate notes
// With prescaler 64: OCR0A = (16,000,000 / (2 × 64 × Freq)) - 1 = (125,000 / Freq) - 1
const uint8_t note_ocr[8] = {
    125000/261 - 1,   // 478
    125000/294 - 1,   // 424
    125000/329 - 1,   // 379
    125000/349 - 1,   // 357
    125000/392 - 1,   // 318
    125000/440 - 1,   // 283
    125000/494 - 1,   // 252
    125000/523 - 1    // 238
};

volatile uint8_t active_notes = 0x00;     // Bitmask of currently playing notes
volatile uint8_t note_timer[8] = {0};     // Countdown timers for each note (250ms)
volatile uint8_t button_states[8] = {0};  // Debounced button states
volatile uint8_t scan_counter = 0;

// Timer0 Compare A interrupt - generates audio on OC0A
// This runs at the frequency determined by OCR0A
ISR(TIMER0_COMPA_vect) {
    // In CTC mode with toggle on compare, this interrupt fires at twice the
    // desired frequency. We don't need to do anything here except ensure
    // the OC0A pin toggles automatically (handled by hardware)
}

// Timer2 Compare A interrupt - 10ms tick for button scanning and note duration
ISR(TIMER2_COMPA_vect) {
    // Scan buttons
    uint8_t current_buttons = buttons_read();
    
    for (int i = 0; i < 8; i++) {
        uint8_t current_state = (current_buttons & (1 << i)) ? 0 : 1;
        
        // Debounce logic: require stable state for 3 scans (30ms)
        if (button_states[i] != current_state) {
            button_states[i] = current_state;
            // Reset counter for this button
            note_timer[i] = 0;
        } else {
            // State stable, increment counter if button is pressed
            if (current_state == 1 && note_timer[i] < 25) {  // 25 × 10ms = 250ms
                note_timer[i]++;
                if (note_timer[i] == 25) {
                    // Note duration ended
                    if (active_notes & (1 << i)) {
                        active_notes &= ~(1 << i);
                        // Update sound output if no notes left
                        if (active_notes == 0) {
                            // Stop sound by disabling timer output
                            TCCR0A &= ~(1 << COM0A0);
                        }
                    }
                }
            }
        }
    }
    
    // Update sound based on active notes
    // Simple polyphony: play the lowest active note (arbitrary choice)
    if (active_notes != 0) {
        uint8_t note_to_play = 0;
        // Find the lowest active note
        for (int i = 0; i < 8; i++) {
            if (active_notes & (1 << i)) {
                note_to_play = i;
                break;
            }
        }
        
        // Set the frequency
        OCR0A = note_ocr[note_to_play];
        // Enable output
        TCCR0A |= (1 << COM0A0);
    }
    
    // Update LED display to show active notes
    PORTA = active_notes;
}

// External interrupt for new note presses
// Using pin change interrupt on PORTL
ISR(PCINT2_vect) {
    uint8_t new_presses = buttons_read();
    
    for (int i = 0; i < 8; i++) {
        if (!(new_presses & (1 << i)) && !(active_notes & (1 << i))) {
            // Button just pressed and not already active
            active_notes |= (1 << i);
            note_timer[i] = 0;  // Reset timer for this note
        }
    }
}

void setup_timer0_audio(void) {
    // Pin 6 (OC0A) as output
    DDRD |= (1 << 6);
    
    // CTC Mode for Timer0
    TCCR0A |= (1 << WGM01);
    TCCR0A &= ~(1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    // Toggle OC0A on compare match
    TCCR0A |= (1 << COM0A0);
    TCCR0A &= ~(1 << COM0A1);
    
    // Prescaler 64 for audio range
    TCCR0B |= (1 << CS01) | (1 << CS00);
    TCCR0B &= ~(1 << CS02);
    
    // Start with no output (COM0A0=0)
    TCCR0A &= ~(1 << COM0A0);
}

void setup_timer2_button_scan(void) {
    // CTC mode for Timer2
    TCCR2A |= (1 << WGM21);
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Prescaler 128: 16MHz/128 = 125kHz, period = 8µs
    // Need 10ms interval: 10ms / 8µs = 1250 ticks
    OCR2A = 1249;  // 1250 ticks × 8µs = 10ms
    
    TCCR2B |= (1 << CS22);
    TCCR2B &= ~((1 << CS21) | (1 << CS20));
    
    TIMSK2 |= (1 << OCIE2A);
}

int main(void) {
    leds_init();
    buttons_init();
    
    setup_timer0_audio();
    setup_timer2_button_scan();
    
    // Enable pin change interrupts for button scanning
    PCICR |= (1 << PCIE2);
    PCMSK2 = 0xFF;  // All pins of PORTL
    
    sei();
    
    while (1) {
        // Main loop idle - all timing handled by interrupts
    }
}

Explanation:

This problem demonstrates a complex real-time system using multiple timers for different purposes. Timer0 generates the audio tones using CTC mode with toggle output, producing square waves at the desired musical frequencies. Timer2 generates a 10ms tick used for button debouncing and note duration counting.

The polyphony implementation is simplified: instead of mixing multiple frequencies through software (which requires digital signal processing), it plays only the lowest active note. This is adequate for simple melodies where chords are not required.

The button scanning uses a state machine approach to debounce, requiring three consecutive stable readings before accepting a button state change. Each note duration is tracked independently using an array of timers, allowing multiple notes to be played simultaneously with independent durations.


Problem 11: Button-Debounced Counter with LED Display

Scenario: Implement a robust button counter that increments on button presses with proper debouncing, using Timer0 to manage the debounce timing instead of blocking delays. Display the count (0-255) on the 8 LEDs.

Requirements:

  • Button0 increments counter
  • Button1 decrements counter
  • Button2 resets counter to 0
  • Use timer interrupts for debouncing (no _delay_ms())
  • Display counter value on LEDs
  • Counter rolls over from 255 to 0 and vice versa

Solution:

/*
 * Problem 11: Button-Debounced Counter with Timer-Based Debouncing
 * 
 * Features:
 * - Non-blocking debouncing using timer interrupts
 * - Count range: 0-255 (fits in 8 LEDs)
 * - Three buttons: increment, decrement, reset
 */

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

// Button state tracking
volatile uint8_t button_state[3] = {1, 1, 1};      // Current state (1 = released)
volatile uint8_t button_last_state[3] = {1, 1, 1}; // Previous state
volatile uint8_t debounce_counter[3] = {0};        // Debounce countdown
volatile uint8_t button_event[3] = {0};            // Event flag (1 = valid press)

// Counter value
volatile uint8_t counter = 0;
volatile uint8_t display_update = 1;

// Button mappings (using first 3 buttons on PORTL)
#define BUTTON_INC   0   // PL0
#define BUTTON_DEC   1   // PL1
#define BUTTON_RESET 2   // PL2

// Timer0 Compare A interrupt - 5ms tick for button scanning
ISR(TIMER0_COMPA_vect) {
    // Read current button states (active low)
    uint8_t current_raw = buttons_read();
    uint8_t current[3];
    
    current[0] = (current_raw >> BUTTON_INC) & 0x01;
    current[1] = (current_raw >> BUTTON_DEC) & 0x01;
    current[2] = (current_raw >> BUTTON_RESET) & 0x01;
    
    // Process each button
    for (int i = 0; i < 3; i++) {
        if (current[i] != button_state[i]) {
            // State changed - start debounce counter
            if (debounce_counter[i] == 0) {
                debounce_counter[i] = 10;  // 10 × 5ms = 50ms debounce
            }
        } else {
            // State stable
            if (debounce_counter[i] > 0) {
                debounce_counter[i]--;
                if (debounce_counter[i] == 0) {
                    // Debounce complete - check if this is a valid press
                    if (current[i] == 0) {  // Pressed (active low)
                        button_state[i] = current[i];
                        button_event[i] = 1;
                    } else {
                        button_state[i] = current[i];
                    }
                }
            }
        }
    }
}

int main(void) {
    leds_init();
    buttons_init();
    
    PORTA = 0x00;  // Start with all LEDs off
    
    // Configure Timer0 for 5ms interrupts
    // CTC mode, prescaler 64, OCR0A for 5ms
    // 16MHz / 64 = 250kHz timer clock, period = 4µs
    // 5ms / 4µs = 1250 ticks → OCR0A = 1249
    TCCR0A |= (1 << WGM01);   // CTC mode
    TCCR0A &= ~(1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    TCCR0B |= (1 << CS01) | (1 << CS00);  // Prescaler 64
    TCCR0B &= ~(1 << CS02);
    
    OCR0A = 1249;  // 1250 ticks × 4µs = 5ms
    
    TIMSK0 |= (1 << OCIE0A);  // Enable compare interrupt
    
    sei();
    
    while (1) {
        // Process button events
        if (button_event[BUTTON_INC]) {
            button_event[BUTTON_INC] = 0;
            counter++;
            display_update = 1;
        }
        
        if (button_event[BUTTON_DEC]) {
            button_event[BUTTON_DEC] = 0;
            counter--;
            display_update = 1;
        }
        
        if (button_event[BUTTON_RESET]) {
            button_event[BUTTON_RESET] = 0;
            counter = 0;
            display_update = 1;
        }
        
        // Update display
        if (display_update) {
            display_update = 0;
            PORTA = counter;
        }
        
        // Optional: Add auto-repeat if button held (uncomment to enable)
        // Would require additional timer logic
    }
}

Explanation:

This problem demonstrates proper button debouncing using timer interrupts instead of blocking delays. The key advantage is that the CPU is free to do other work while waiting for the debounce period to expire.

The debouncing algorithm works by detecting state changes and starting a 50ms countdown. Only when the button state remains stable for the entire debounce period does it register as a valid press. This eliminates the false triggering caused by mechanical contact bounce.

The 5ms timer tick provides good responsiveness while allowing plenty of time for the main loop to process other tasks. The counter value is displayed in real-time on the LEDs, providing immediate visual feedback.


Problem 12: Long Press vs Short Press Detection

Scenario: You need to distinguish between short button presses (<1 second) and long button presses (>2 seconds) on a single button. Short press toggles LED0, long press toggles all LEDs. Use Timer1 to measure button press duration.

Requirements:

  • Single button on PL0
  • Short press (<1000ms): Toggle LED0 only
  • Long press (>2000ms): Toggle all LEDs
  • Display press duration on LEDs 1-7 as binary value (duration in 100ms units)
  • Use timer to measure press duration precisely

Solution:

/*
 * Problem 12: Long Press vs Short Press Detection
 * 
 * Detects and distinguishes between short and long button presses
 * Uses Timer1 (16-bit) to measure press duration accurately
 */

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

// Button state machine
typedef enum {
    IDLE,           // Waiting for button press
    PRESS_DETECTED, // Edge detected, waiting for debounce
    COUNTING,       // Button held, measuring duration
    RELEASE_DETECTED // Release edge, processing
} button_state_t;

volatile button_state_t state = IDLE;
volatile uint16_t press_duration = 0;  // Duration in 100ms units (0-255)
volatile uint8_t debounce_counter = 0;
volatile uint8_t process_press = 0;
volatile uint8_t is_long_press = 0;
volatile uint8_t led0_state = 0;
volatile uint8_t all_leds_state = 0;

// Pin change interrupt for button (PL0)
ISR(PCINT2_vect) {
    uint8_t button_reading = (PINL >> 0) & 0x01;  // 0 = pressed
    
    switch (state) {
        case IDLE:
            if (button_reading == 0) {  // Button pressed
                state = PRESS_DETECTED;
                debounce_counter = 10;  // 50ms debounce
                // Clear timer and start counting
                TCNT1 = 0;
                TCCR1B |= (1 << CS11) | (1 << CS10);  // Start timer, prescaler 64
                // 64 ticks = 4µs, timer counts milliseconds
                // We'll use compare interrupt for 100ms intervals
            }
            break;
            
        case PRESS_DETECTED:
            // Wait for debounce period to expire (handled in timer interrupt)
            if (debounce_counter == 0) {
                if (button_reading == 0) {
                    state = COUNTING;
                    press_duration = 0;
                } else {
                    state = IDLE;  // False trigger
                    TCCR1B = 0;    // Stop timer
                }
            }
            break;
            
        case COUNTING:
            if (button_reading == 1) {  // Button released
                state = RELEASE_DETECTED;
                TCCR1B = 0;  // Stop timer
            }
            break;
            
        case RELEASE_DETECTED:
            // Done - process in main loop
            process_press = 1;
            is_long_press = (press_duration >= 20);  // 20 × 100ms = 2000ms
            state = IDLE;
            break;
    }
}

// Timer1 Compare A interrupt - 100ms intervals for duration measurement
ISR(TIMER1_COMPA_vect) {
    if (state == COUNTING) {
        press_duration++;
        if (press_duration > 255) press_duration = 255;  // Clamp
        
        // Update LED display to show duration (in 100ms units)
        PORTA = (PORTA & 0x01) | ((press_duration & 0xFE) << 0);
        // Keep LED0 for short/long indication, LEDs 1-7 show duration
    }
}

// Timer2 Compare interrupt - 1ms for debounce counting
ISR(TIMER2_COMPA_vect) {
    static uint8_t ms_counter = 0;
    ms_counter++;
    
    if (ms_counter >= 100) {  // Every 100ms
        ms_counter = 0;
        if (debounce_counter > 0) {
            debounce_counter--;
        }
    }
}

void setup_timer1_duration(void) {
    // CTC mode for Timer1
    TCCR1A = 0x00;
    TCCR1B |= (1 << WGM12);  // CTC mode
    TCCR1B &= ~(1 << WGM13);
    
    // Prescaler 64: 16MHz/64 = 250kHz, 4µs per tick
    // Need 100ms = 100,000µs / 4µs = 25000 ticks
    OCR1A = 25000 - 1;  // 25000 ticks = 100ms
    TCCR1B &= ~((1 << CS12) | (1 << CS11) | (1 << CS10));  // Start stopped
    
    TIMSK1 |= (1 << OCIE1A);  // Enable compare interrupt
}

void setup_timer2_milliseconds(void) {
    // CTC mode, prescaler 64, OCR2A for 1ms
    TCCR2A |= (1 << WGM21);
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    TCCR2B |= (1 << CS21) | (1 << CS20);  // Prescaler 64
    OCR2A = 249;  // 250 ticks × 4µs = 1ms
    
    TIMSK2 |= (1 << OCIE2A);
}

int main(void) {
    // LED display: LED0 (PA0) for short/long indication
    // LEDs 1-7 (PA1-PA7) for press duration
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Button on PL0 as input with pull-up
    DDRL &= ~(1 << 0);
    PORTL |= (1 << 0);
    
    // Pin change interrupt for button
    PCICR |= (1 << PCIE2);
    PCMSK2 |= (1 << PCINT0);
    
    setup_timer1_duration();
    setup_timer2_milliseconds();
    
    sei();
    
    while (1) {
        if (process_press) {
            process_press = 0;
            
            if (is_long_press) {
                // Long press: toggle all LEDs
                all_leds_state ^= 1;
                if (all_leds_state) {
                    PORTA ^= 0xFE;  // Toggle LEDs 1-7
                } else {
                    PORTA ^= 0xFE;
                }
                // Indicate long press on LED0
                PORTA |= (1 << 0);
                _delay_ms(200);
                PORTA &= ~(1 << 0);
            } else {
                // Short press: toggle LED0 only
                led0_state ^= 1;
                if (led0_state) {
                    PORTA |= (1 << 0);
                } else {
                    PORTA &= ~(1 << 0);
                }
            }
        }
    }
}

Explanation:

This problem demonstrates a state machine for button press detection combined with accurate duration measurement using Timer1 (16-bit). The system distinguishes between short presses (<1000ms) and long presses (>2000ms), enabling a single button to perform two different functions.

The key insight is that the duration measurement runs continuously while the button is held, using Timer1 in CTC mode to generate 100ms interrupts. The press duration is displayed in real-time on LEDs 1-7, providing visual feedback to the user about how long the button has been pressed.

The debouncing is handled separately using Timer2, ensuring that the initial press detection is stable before starting the duration measurement. This prevents false triggers from contact bounce.


Problem Set 5: Complex Real-World Applications

Problem 13: Reaction Time Tester Using Two Timers

Scenario: Build a reaction time tester. LEDs light up after a random delay (1-5 seconds), and the user presses a button as quickly as possible. The reaction time is measured in milliseconds using Timer1 and displayed on the LEDs.

Requirements:

  • Random delay between 1-5 seconds before LED lights
  • User presses button (PL0) after seeing LED
  • Measure time from LED on to button press using Timer1
  • Display reaction time in milliseconds (0-255ms range)
  • Button1 resets and starts a new test
  • 8 LEDs show reaction time in binary

Solution:

/*
 * Problem 13: Reaction Time Tester
 * 
 * Features:
 * - Random delay (1-5 seconds)
 * - Accurate time measurement using Timer1 (16-bit)
 * - Displays reaction time in milliseconds
 */

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

// State machine states
typedef enum {
    WAITING,        // Waiting for start
    DELAYING,       // Random delay period
    AWAITING_PRESS, // LED on, waiting for button
    MEASURING,      // Button pressed, measuring
    DISPLAYING      // Showing result
} test_state_t;

// Global variables
volatile test_state_t state = WAITING;
volatile uint16_t random_delay_ms = 2000;  // Random delay in ms
volatile uint16_t reaction_time_ms = 0;    // Measured reaction time
volatile uint16_t elapsed_ms = 0;          // Timer counter
volatile uint8_t led_on = 0;
volatile uint8_t display_counter = 0;

// Random number generator seed (read from EEPROM)
uint16_t random_seed = 0x1234;

// Timer1 Compare A interrupt - 1ms clock base
ISR(TIMER1_COMPA_vect) {
    if (state == DELAYING) {
        elapsed_ms++;
        if (elapsed_ms >= random_delay_ms) {
            // Time to turn on LED
            elapsed_ms = 0;
            PORTA |= (1 << 0);  // Turn on LED0
            led_on = 1;
            state = AWAITING_PRESS;
        }
    }
    else if (state == MEASURING) {
        elapsed_ms++;
        if (elapsed_ms > 255) elapsed_ms = 255;
    }
}

// Pin change interrupt for button
ISR(PCINT2_vect) {
    uint8_t button_pressed = ((PINL >> 0) & 0x01) == 0;
    
    if (button_pressed) {
        switch (state) {
            case WAITING:
                // Start new test
                state = DELAYING;
                elapsed_ms = 0;
                // Generate random delay between 1000ms and 5000ms
                random_delay_ms = 1000 + (rand() % 4000);
                PORTA = 0x00;  // All LEDs off
                break;
                
            case AWAITING_PRESS:
                // Button pressed - reaction time measured
                reaction_time_ms = elapsed_ms;
                led_on = 0;
                PORTA = 0x00;  // Turn off LED
                state = DISPLAYING;
                display_counter = 0;
                break;
                
            case DISPLAYING:
                // Button pressed during display - return to waiting
                state = WAITING;
                PORTA = 0x00;
                break;
                
            default:
                break;
        }
    }
}

// Timer2 Compare interrupt - 50ms for display refresh
ISR(TIMER2_COMPA_vect) {
    static uint8_t flash_count = 0;
    
    if (state == MEASURING) {
        // Show elapsed time during measurement (feedback)
        PORTA = (PORTA & 0x01) | ((elapsed_ms & 0xFE) << 0);
    }
    else if (state == DISPLAYING) {
        // Display reaction time for 5 seconds
        display_counter++;
        if (display_counter >= 100) {  // 5 seconds (100 × 50ms)
            state = WAITING;
            PORTA = 0x00;
        } else {
            // Flash the result periodically
            flash_count++;
            if (flash_count < 20) {
                PORTA = (uint8_t)reaction_time_ms;
            } else if (flash_count < 30) {
                PORTA = 0x00;
            } else if (flash_count >= 40) {
                flash_count = 0;
            }
        }
    }
}

void setup_timer1_one_ms(void) {
    // CTC mode for Timer1
    TCCR1A = 0x00;
    TCCR1B |= (1 << WGM12);
    
    // Prescaler 64: 16MHz/64 = 250kHz, period = 4µs
    // Need 1ms = 1000µs / 4µs = 250 ticks → OCR1A = 249
    OCR1A = 249;
    TCCR1B |= (1 << CS11) | (1 << CS10);  // Prescaler 64
    TCCR1B &= ~(1 << CS12);
    
    TIMSK1 |= (1 << OCIE1A);
}

void setup_timer2_50ms(void) {
    // CTC mode for Timer2
    TCCR2A |= (1 << WGM21);
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Prescaler 256: 16MHz/256 = 62.5kHz, period = 16µs
    // Need 50ms = 50,000µs / 16µs = 3125 ticks → OCR2A = 3124
    // But 8-bit timer max is 255! Need different approach.
    
    // Instead, use prescaler 1024 and count overflows
    TCCR2B |= (1 << CS22) | (1 << CS21) | (1 << CS20);  // Prescaler 1024
    // 16MHz/1024 = 15625Hz, period = 64µs
    // 50ms / 64µs = 781.25 ticks → not integer
    // Use 64ms instead: 256 ticks × 64µs = 16.384ms
    // Or use overflow counting
    
    // Simpler: Keep using 1ms from Timer1 and use a counter in the ISR
}

// Alternative: Use Timer0 for 50ms using multiple overflows
volatile uint8_t fifty_ms_flag = 0;

// Timer0 overflow - approximate 16.384ms, count 3 for ~49ms
ISR(TIMER0_OVF_vect) {
    static uint8_t overflow_50ms_counter = 0;
    overflow_50ms_counter++;
    if (overflow_50ms_counter >= 3) {  // 3 × 16.384ms = 49.152ms
        overflow_50ms_counter = 0;
        fifty_ms_flag = 1;
    }
}

void setup_timer0_50ms(void) {
    // Timer0 normal mode, prescaler 1024
    TCCR0A = 0x00;
    TCCR0B |= (1 << CS02) | (1 << CS00);
    TCCR0B &= ~(1 << CS01);
    
    TIMSK0 |= (1 << TOIE0);
}

// Simple random number generator
uint16_t my_rand(void) {
    random_seed = random_seed * 1103515245 + 12345;
    return (random_seed >> 16) & 0x7FFF;
}

int main(void) {
    // LEDs on PORTA
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Button on PL0
    DDRL &= ~(1 << 0);
    PORTL |= (1 << 0);
    
    // Pin change interrupt
    PCICR |= (1 << PCIE2);
    PCMSK2 |= (1 << PCINT0);
    
    setup_timer1_one_ms();
    setup_timer0_50ms();
    
    // Initialize random number generator
    random_seed = my_rand();
    srand(random_seed);
    
    sei();
    
    while (1) {
        if (fifty_ms_flag) {
            fifty_ms_flag = 0;
            
            // Update Timer1 elapsed time display during measurement
            if (state == MEASURING) {
                // Show elapsed time on LEDs (0-255ms)
                PORTA = (uint8_t)elapsed_ms;
            }
        }
    }
}

Explanation:

This problem combines multiple timing concepts into a complete application. The reaction time tester requires both variable delays (the random waiting period) and precise time measurement (the reaction time itself).

Timer1 provides the 1ms time base used for both the random delay countdown and the reaction time measurement. The random delay uses the rand() function to generate a value between 1000 and 5000 ms, creating unpredictability for the user.

The state machine keeps track of the test sequence: waiting for start, delaying with LED off, waiting for button press after LED turns on, measuring, and displaying results. The display uses a flashing pattern to show the reaction time result vividly.

This problem demonstrates how embedded systems can create interactive applications that require both timing accuracy and user input responsiveness.


Problem 14: Rotating LED Display with Speed Control Using Timer2 PWM

Scenario: Create a rotating LED effect (like a marquee) where the speed is controlled by a continuous analog input (simulated by button presses that increase/decrease speed). Use Timer2 PWM to drive the LED brightness for a fading effect between transitions.

Requirements:

  • 8 LEDs rotate left continuously
  • Speed controlled by buttons (0-15 speed levels)
  • Use Timer2 PWM to create fading between LED transitions
  • Each LED fades in and out smoothly
  • Display current speed level on LEDs 0-3 in binary

Solution:

/*
 * Problem 14: Rotating LED Display with Fading Effect
 * 
 * Features:
 * - LEDs rotate left continuously
 * - Speed control with buttons
 * - PWM fading for smooth transitions
 */

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

// Global variables
volatile uint8_t current_led = 0;
volatile uint8_t speed_level = 8;  // 0-15 (8 = medium speed)
volatile uint8_t speed_changed = 1;
volatile uint8_t fade_step = 0;
volatile uint8_t fade_direction = 1;  // 1 = fading in, 0 = fading out
volatile uint8_t brightness[8] = {0};  // Brightness values for each LED
volatile uint8_t fade_timer = 0;

// Button mappings
#define BUTTON_SPEED_UP   0   // PL0
#define BUTTON_SPEED_DOWN 1   // PL1

// Timer2 Compare A interrupt - PWM update for fading
// This runs at the PWM frequency (~977Hz) but we'll use it for fading steps
ISR(TIMER2_COMPA_vect) {
    static uint8_t pwm_cycle_counter = 0;
    
    // We want to update the fade at a much slower rate than PWM frequency
    // Use a counter to divide down the PWM cycles
    pwm_cycle_counter++;
    
    // Calculate update rate based on speed level
    // Higher speed = faster fade updates
    uint8_t update_divisor = 32 - (speed_level * 2);
    if (update_divisor < 4) update_divisor = 4;
    
    if (pwm_cycle_counter >= update_divisor) {
        pwm_cycle_counter = 0;
        
        // Update fade state
        if (fade_direction) {
            fade_step++;
            if (fade_step >= 128) {
                fade_step = 128;
                fade_direction = 0;
                
                // Move to next LED
                current_led = (current_led + 1) % 8;
            }
        } else {
            if (fade_step > 0) {
                fade_step--;
            } else {
                fade_direction = 1;
                fade_step = 0;
            }
        }
        
        // Calculate brightness for each LED
        // The active LED gets brightness based on fade_step (0-128 scale to 0-255)
        uint8_t active_brightness = (fade_step * 2);
        if (active_brightness > 255) active_brightness = 255;
        
        // Previous LED gets decreasing brightness
        uint8_t prev_brightness = 255 - active_brightness;
        
        // Clear all brightnesses
        for (int i = 0; i < 8; i++) {
            brightness[i] = 0;
        }
        
        // Set brightness for current and previous LEDs
        brightness[current_led] = active_brightness;
        uint8_t prev_led = (current_led == 0) ? 7 : current_led - 1;
        brightness[prev_led] = prev_brightness;
        
        // Update PWM outputs
        // For simplicity, we're using a single PWM output and multiplexing
        // But with only one PWM pin, we need to time-multiplex the LEDs
        
        // For true independent PWM per LED, we'd need separate PWM channels
        // This example demonstrates the concept; for 8 independent PWM,
        // use a technique called "binary code modulation" or external PWM driver
    }
}

// Timer0 Compare interrupt - button scanning (10ms)
ISR(TIMER0_COMPA_vect) {
    static uint8_t speed_debounce[2] = {0};
    
    uint8_t buttons = buttons_read();
    
    // Speed up button
    if (!(buttons & (1 << BUTTON_SPEED_UP))) {
        if (speed_debounce[0] == 0) {
            if (speed_level < 15) {
                speed_level++;
                speed_changed = 1;
            }
            speed_debounce[0] = 10;  // 100ms debounce
        } else {
            speed_debounce[0]--;
        }
    } else {
        if (speed_debounce[0] > 0) speed_debounce[0]--;
    }
    
    // Speed down button
    if (!(buttons & (1 << BUTTON_SPEED_DOWN))) {
        if (speed_debounce[1] == 0) {
            if (speed_level > 0) {
                speed_level--;
                speed_changed = 1;
            }
            speed_debounce[1] = 10;
        } else {
            speed_debounce[1]--;
        }
    } else {
        if (speed_debounce[1] > 0) speed_debounce[1]--;
    }
}

void setup_timer2_pwm_fade(void) {
    // Configure Timer2 for Fast PWM on OC2A (pin 10)
    DDRB |= (1 << 4);  // PB4 (pin 10) as output
    
    // Fast PWM Mode 3
    TCCR2A |= (1 << WGM21) | (1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Non-inverting PWM
    TCCR2A |= (1 << COM2A1);
    TCCR2A &= ~(1 << COM2A0);
    
    // Prescaler 64 for ~977Hz PWM
    TCCR2B |= (1 << CS21) | (1 << CS20);
    TCCR2B &= ~(1 << CS22);
    
    // Enable compare interrupt for timing fade steps
    TIMSK2 |= (1 << OCIE2A);
    
    OCR2A = 0;  // Start with 0% duty cycle
}

void setup_timer0_button_scan(void) {
    // CTC mode, prescaler 64, 10ms interrupt
    TCCR0A |= (1 << WGM01);
    TCCR0A &= ~(1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    TCCR0B |= (1 << CS01) | (1 << CS00);
    OCR0A = 249;  // 250 ticks × 4µs ×? Wait recalc
    // Actually 16MHz/64=250kHz, 250 ticks × 4µs = 1ms, not 10ms
    // Need 2500 ticks for 10ms, but OCR0A is 8-bit max 255
    // Use overflow counting instead
    
    TCCR0B |= (1 << CS02) | (1 << CS00);  // Prescaler 1024
    // 16MHz/1024 = 15625Hz, period = 64µs
    // 10ms / 64µs = 156.25 ticks → use 156
    OCR0A = 155;  // 156 ticks × 64µs = 10ms
    
    TIMSK0 |= (1 << OCIE0A);
}

// Simple multiplexing to drive multiple LEDs from one PWM signal
// Uses 8 transistors, one per LED, controlled by PORTA
void update_led_multiplex(void) {
    static uint8_t mux_counter = 0;
    
    // Cycle through LEDs very fast (1kHz)
    mux_counter = (mux_counter + 1) % 8;
    
    // Turn off all LEDs
    PORTA = 0x00;
    
    // Set PWM duty cycle for current LED
    OCR2A = brightness[mux_counter];
    
    // Enable the current LED
    PORTA |= (1 << mux_counter);
}

int main(void) {
    // LEDs on PORTA for multiplexing control
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Buttons on PORTL
    buttons_init();
    
    setup_timer2_pwm_fade();
    setup_timer0_button_scan();
    
    // Also need a fast interrupt for LED multiplexing
    // Use Timer1 for high-speed multiplexing (1kHz)
    // Configure Timer1 for 1ms interrupt for multiplexing
    
    TCCR1A = 0x00;
    TCCR1B |= (1 << WGM12);  // CTC mode
    OCR1A = 249;  // 250 ticks × 4µs = 1ms
    TCCR1B |= (1 << CS11) | (1 << CS10);  // Prescaler 64
    TIMSK1 |= (1 << OCIE1A);
    
    sei();
    
    while (1) {
        // Display speed level on LEDs 0-3
        if (speed_changed) {
            speed_changed = 0;
            // Show speed on LEDs 0-3 (lower nibble)
            PORTA = (PORTA & 0xF0) | (speed_level & 0x0F);
        }
    }
}

// Timer1 interrupt for LED multiplexing
ISR(TIMER1_COMPA_vect) {
    update_led_multiplex();
}

Explanation:

This problem demonstrates advanced techniques including LED multiplexing, PWM fading, and speed control. Since the ATmega2560 has limited PWM outputs, multiplexing allows a single PWM signal to control multiple LEDs by rapidly switching between them.

The fading effect uses a brightness[] array that stores the desired brightness for each LED. The fade_step variable creates a triangular wave pattern (increase then decrease) that moves between LEDs, creating a rotating “chasing” effect with smooth transitions.

The speed control affects how quickly the fade pattern advances, creating faster or slower rotation. The speed level is displayed on the lower 4 LEDs, providing visual feedback of the current setting.


Problem 15: Complete Digital Clock with Alarm Using Multiple Timers

Scenario: Build a complete digital clock with alarm functionality using all available timers. Display hours (0-23) and minutes (0-59) on the LEDs using binary-coded decimal (BCD) format. Use Timer2’s asynchronous mode for accurate timekeeping.

Requirements:

  • 6 LEDs for hours (0-23) and 8 LEDs for minutes? Adjust: Use all 8 LEDs for time display
    • Bits 0-4: Minutes (0-59, needs 6 bits, use bits 0-5)
    • Bits 6-7: Hours (0-23, needs 5 bits, use bits 6-7 for upper bits, need more display)
  • Better: Use all 8 LEDs to display time in BCD or binary
  • Set time using buttons (increment hours, increment minutes)
  • Set alarm using buttons (alarm hours, alarm minutes)
  • Alarm triggers all LEDs blinking at 2Hz
  • Use Timer2 asynchronous mode with 32.768 kHz crystal for accurate timekeeping

Solution:

/*
 * Problem 15: Complete Digital Clock with Alarm
 * 
 * Hardware:
 * - 32.768 kHz crystal on TOSC1 (pin 36) and TOSC2 (pin 37)
 * - 8 LEDs on PORTA for time display
 * - 3 buttons: Mode, Up, Down
 * 
 * Display format: Binary (8 LEDs)
 * - Low 6 bits: Minutes (0-59)
 * - High 2 bits: Hours (0-23) - actually need 5 bits for hours
 * Solution: Use two display modes (alternate every 2 seconds)
 */

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

// Timekeeping variables
volatile uint8_t seconds = 0;
volatile uint8_t minutes = 0;
volatile uint8_t hours = 0;
volatile uint8_t alarm_hours = 7;
volatile uint8_t alarm_minutes = 0;
volatile uint8_t alarm_enabled = 1;

// Display mode
volatile uint8_t display_mode = 0;  // 0 = time, 1 = alarm
volatile uint8_t mode_switch_counter = 0;

// Button state machine
typedef enum {
    MODE_NORMAL,
    MODE_SET_HOURS,
    MODE_SET_MINUTES,
    MODE_SET_ALARM_HOURS,
    MODE_SET_ALARM_MINUTES
} clock_mode_t;

volatile clock_mode_t clock_mode = MODE_NORMAL;
volatile uint8_t button_debounce[3] = {0};
volatile uint8_t blink_state = 0;

// Alarm flag
volatile uint8_t alarm_triggered = 0;
volatile uint8_t alarm_blink_counter = 0;

// Timer2 asynchronous compare interrupt - 1 second
ISR(TIMER2_COMPA_vect) {
    if (!alarm_triggered && clock_mode == MODE_NORMAL) {
        seconds++;
        if (seconds >= 60) {
            seconds = 0;
            minutes++;
            if (minutes >= 60) {
                minutes = 0;
                hours++;
                if (hours >= 24) {
                    hours = 0;
                }
            }
        }
        
        // Check alarm
        if (alarm_enabled && hours == alarm_hours && minutes == alarm_minutes && seconds == 0) {
            alarm_triggered = 1;
            alarm_blink_counter = 0;
        }
    }
    
    // Update display mode flashing (every 2 seconds)
    static uint8_t mode_flash = 0;
    mode_flash++;
    if (mode_flash >= 2) {
        mode_flash = 0;
        blink_state ^= 1;
    }
    
    // Handle alarm blinking
    if (alarm_triggered) {
        alarm_blink_counter++;
        if (alarm_blink_counter >= 60) {  // 60 seconds
            alarm_triggered = 0;
            PORTA = 0x00;
        }
    }
}

// Timer0 compare interrupt - 50ms for button scanning
ISR(TIMER0_COMPA_vect) {
    static uint8_t button_states[3] = {1, 1, 1};
    uint8_t current_raw = buttons_read();
    
    // Mapping: PL0=MODE, PL1=UP, PL2=DOWN
    uint8_t current[3] = {
        (current_raw >> 0) & 0x01,
        (current_raw >> 1) & 0x01,
        (current_raw >> 2) & 0x01
    };
    
    for (int i = 0; i < 3; i++) {
        if (current[i] != button_states[i]) {
            if (button_debounce[i] == 0) {
                button_debounce[i] = 4;  // 200ms debounce
            }
        } else if (button_debounce[i] > 0) {
            button_debounce[i]--;
            if (button_debounce[i] == 0 && current[i] == 0) {
                // Valid button press
                process_button(i);
            }
        }
        button_states[i] = current[i];
    }
}

void process_button(uint8_t button) {
    switch (button) {
        case 0:  // MODE button
            switch (clock_mode) {
                case MODE_NORMAL:
                    clock_mode = MODE_SET_HOURS;
                    break;
                case MODE_SET_HOURS:
                    clock_mode = MODE_SET_MINUTES;
                    break;
                case MODE_SET_MINUTES:
                    clock_mode = MODE_SET_ALARM_HOURS;
                    break;
                case MODE_SET_ALARM_HOURS:
                    clock_mode = MODE_SET_ALARM_MINUTES;
                    break;
                case MODE_SET_ALARM_MINUTES:
                    clock_mode = MODE_NORMAL;
                    break;
            }
            break;
            
        case 1:  // UP button
            switch (clock_mode) {
                case MODE_SET_HOURS:
                    hours = (hours + 1) % 24;
                    break;
                case MODE_SET_MINUTES:
                    minutes = (minutes + 1) % 60;
                    seconds = 0;
                    break;
                case MODE_SET_ALARM_HOURS:
                    alarm_hours = (alarm_hours + 1) % 24;
                    break;
                case MODE_SET_ALARM_MINUTES:
                    alarm_minutes = (alarm_minutes + 1) % 60;
                    break;
                default:
                    break;
            }
            break;
            
        case 2:  // DOWN button
            switch (clock_mode) {
                case MODE_SET_HOURS:
                    hours = (hours + 23) % 24;
                    break;
                case MODE_SET_MINUTES:
                    minutes = (minutes + 59) % 60;
                    seconds = 0;
                    break;
                case MODE_SET_ALARM_HOURS:
                    alarm_hours = (alarm_hours + 23) % 24;
                    break;
                case MODE_SET_ALARM_MINUTES:
                    alarm_minutes = (alarm_minutes + 59) % 60;
                    break;
                default:
                    break;
            }
            break;
    }
}

void update_display(void) {
    uint16_t display_value;
    
    if (alarm_triggered) {
        // Show "AA" pattern (alternating bits)
        if (alarm_blink_counter & 0x04) {
            PORTA = 0xAA;
        } else {
            PORTA = 0x55;
        }
        return;
    }
    
    // Choose what to display
    if (display_mode == 0) {
        display_value = (hours << 6) | minutes;
        // Hours in bits 6-7 only gives 0-3 range, not enough for 0-23
        // Alternative: Scrolling display or alternate between hours and minutes
        // Let's alternate every 2 seconds
        static uint8_t alt_display = 0;
        if (blink_state == 0 && clock_mode == MODE_NORMAL) {
            // Show hours (0-23) on LEDs 0-4
            display_value = hours;
            // Indicate hours mode with LED7
            PORTA = (display_value & 0x1F) | (1 << 7);
        } else {
            // Show minutes (0-59) on LEDs 0-5
            display_value = minutes;
            PORTA = (display_value & 0x3F);
        }
    } else {
        display_value = (alarm_hours << 6) | alarm_minutes;
        // Similar alternate display for alarm
        static uint8_t alt_alarm = 0;
        if (blink_state == 0) {
            PORTA = (alarm_hours & 0x1F) | (1 << 6);
        } else {
            PORTA = (alarm_minutes & 0x3F);
        }
    }
    
    // Blink setting value when in set mode
    if (clock_mode != MODE_NORMAL) {
        if (blink_state) {
            PORTA = 0x00;
        }
        // Indicate which field is being set
        switch (clock_mode) {
            case MODE_SET_HOURS:
                PORTA |= (1 << 7);  // LED7 on for hours setting
                break;
            case MODE_SET_MINUTES:
                PORTA |= (1 << 6);  // LED6 on for minutes setting
                break;
            case MODE_SET_ALARM_HOURS:
                PORTA |= (1 << 5);  // LED5 on for alarm hours
                break;
            case MODE_SET_ALARM_MINUTES:
                PORTA |= (1 << 4);  // LED4 on for alarm minutes
                break;
            default:
                break;
        }
    }
}

void setup_timer2_async_rtc(void) {
    // Enable asynchronous mode for Timer2
    ASSR |= (1 << AS2);
    
    // Wait for all registers to be updated
    while (ASSR & ((1 << TCNT2UB) | (1 << OCR2AUB) | 
                   (1 << TCR2AUB) | (1 << TCR2BUB)));
    
    // CTC mode for 1-second interrupts
    TCCR2A |= (1 << WGM21);
    TCCR2A &= ~(1 << WGM20);
    TCCR2B &= ~(1 << WGM22);
    
    // Prescaler 128 with 32.768kHz crystal: 32768/128 = 256Hz
    // Need 256 ticks for 1 second → OCR2A = 255
    OCR2A = 255;
    TCCR2B |= (1 << CS22);  // Prescaler 128 in async mode
    TCCR2B &= ~((1 << CS21) | (1 << CS20));
    
    // Wait for registers to update
    while (ASSR & ((1 << TCNT2UB) | (1 << OCR2AUB) | 
                   (1 << TCR2AUB) | (1 << TCR2BUB)));
    
    TIMSK2 |= (1 << OCIE2A);  // Enable compare interrupt
}

void setup_timer0_button_scan(void) {
    // CTC mode, 50ms interrupt
    TCCR0A |= (1 << WGM01);
    TCCR0A &= ~(1 << WGM00);
    TCCR0B &= ~(1 << WGM02);
    
    // Prescaler 1024: 16MHz/1024 = 15625Hz, period = 64µs
    // 50ms / 64µs = 781.25 → OCR0A = 780
    OCR0A = 780;
    TCCR0B |= (1 << CS02) | (1 << CS00);
    
    TIMSK0 |= (1 << OCIE0A);
}

int main(void) {
    // LED display
    DDRA = 0xFF;
    PORTA = 0x00;
    
    // Buttons on PL0, PL1, PL2
    DDRL &= ~((1 << 0) | (1 << 1) | (1 << 2));
    PORTL |= (1 << 0) | (1 << 1) | (1 << 2);
    
    setup_timer2_async_rtc();
    setup_timer0_button_scan();
    
    sei();
    
    while (1) {
        update_display();
        
        // Alternate display mode every 5 seconds in normal mode
        static uint16_t mode_timer = 0;
        if (clock_mode == MODE_NORMAL && !alarm_triggered) {
            mode_timer++;
            if (mode_timer >= 100) {  // 5 seconds (100 × 50ms)
                mode_timer = 0;
                display_mode ^= 1;
            }
        } else {
            mode_timer = 0;
        }
        
        // Power saving: idle sleep when not needed
        // set_sleep_mode(SLEEP_MODE_IDLE);
        // sleep_mode();
    }
}

Explanation:

This final problem brings together almost all the concepts from previous problems: multiple timers, asynchronous operation, state machines, button debouncing, display multiplexing, and alarm functionality.

The key feature is Timer2’s asynchronous mode with a 32.768 kHz watch crystal, which provides accurate timekeeping independent of the main CPU clock. This allows the clock to keep time even when the microcontroller is in sleep mode, making it suitable for battery-powered applications.

The alarm system uses the same timekeeping base and triggers when the current time matches the alarm settings. The display alternates between showing time and alarm settings, with blinking indicators during set mode. This problem demonstrates how a complete embedded application can be built using the timer concepts learned throughout this course.


Summary of Timer Applications

ProblemTimer(s) UsedMode(s)Key Concept
1Timer0NormalOverflow counting for long delays
2Timer0CTCVariable interval generation
3Timer0CTCState machine with timeouts
4Timer0Fast PWMGlobal brightness control
5Timer0, Timer2, Timer1Fast PWMMultiple PWM channels
6Timer2Phase Correct PWMSine wave breathing effect
7Timer0, Timer2Counter, CTCExternal event counting
8Timer2NormalPulse width measurement
9Timer0, Timer1, Timer2Counter, CTCFrequency counter
10Timer0, Timer2CTC, CTCPolyphonic music sequencer
11Timer0CTCNon-blocking debouncing
12Timer1, Timer2CTCLong/short press detection
13Timer1, Timer2, Timer0CTC, CTC, NormalReaction time measurement
14Timer0, Timer1, Timer2CTC, CTC, Fast PWMLED multiplexing and fading
15Timer0, Timer2CTC, Async CTCComplete RTC clock with alarm

Leave a Comment

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

Table of Contents

Index
Scroll to Top