Project 3: Hardware Timer and PWM — Bare Metal AVR
Master hardware timers to achieve microsecond-precision timing and generate PWM signals for LED dimming, motor control, and audio synthesis—the foundation of all real-time embedded systems.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Intermediate |
| Time Estimate | 1 week (20-30 hours) |
| Language | C (alt: AVR Assembly for ISRs) |
| Platform | Arduino Uno / ATmega328P |
| Prerequisites | Projects 1-2 (GPIO, UART), basic interrupt concepts |
| Key Topics | Timer/Counter hardware, PWM modes, prescalers, CTC mode, timer interrupts |
1. Learning Objectives
By completing this project, you will:
- Understand timer/counter hardware architecture - Know how counters, compare registers, overflow flags, and prescalers form a complete timing subsystem
- Master timer modes - Configure Normal, CTC, Fast PWM, and Phase-Correct PWM modes from first principles
- Calculate prescaler and compare values - Derive exact register values for any desired frequency or period
- Generate PWM signals - Create variable duty cycle outputs for analog-like control
- Implement timer interrupts - Build non-blocking periodic tasks with microsecond precision
- Generate audio frequencies - Create precise square waves for tones and buzzer control
- Build real-time foundations - Understand how RTOS tick timers and schedulers work
What You Will Build
A comprehensive timer demonstration system that:
- Generates a precise 1-second tick using Timer1 compare match interrupt
- Fades an LED smoothly using Timer0 PWM (0% to 100% brightness)
- Produces audio tones at exact frequencies (A4=440Hz, etc.) using Timer2
- Maintains a millisecond-resolution uptime counter
- Accepts UART commands to control PWM duty cycle and play tones
- (Optional) Controls a servo motor with 1-2ms pulse width
2. Theoretical Foundation
2.1 Timer/Counter Hardware
What is a Hardware Timer?
A hardware timer is a specialized counter circuit built into the microcontroller that increments automatically with each clock pulse. Unlike software loops that consume CPU cycles and can be interrupted, hardware timers run independently of CPU execution, providing deterministic timing.
The Timer/Counter Subsystem:
F_CPU (System Clock)
│
│ 16 MHz on Arduino Uno
▼
┌───────────────────────┐
│ Prescaler │
│ ┌─────────────────┐ │
│ │ /1 /8 /64 /256 │ │
│ │ /1024 │ │
│ └────────┬────────┘ │
│ │ │
│ Timer Clock │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Counter (TCNTn) │
│ ┌─────────────────┐ │
│ │ 8-bit: 0-255 │ │
│ │ 16-bit: 0-65535│ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ ▼ ▼ │
│ ┌───────┐ ┌───────┐ │
│ │ OCRnA │ │ OCRnB │ │ Compare Registers
│ └───┬───┘ └───┬───┘ │
│ │ │ │
└─────┼──────────┼──────┘
│ │
┌─────────┴──────────┴─────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Compare │ │ Overflow │
│ Match │ │ Flag │
│ Interrupt│ │ Interrupt│
└──────────┘ └──────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ OCnA │ │ Timer │
│ Pin │ │ Reset │
│ Toggle │ │ or Wrap │
└──────────┘ └──────────┘
The Count Register (TCNTn)
The heart of any timer is its count register. This register increments (or decrements in some modes) with each tick of the timer clock:
8-bit Counter Operation (Timer0, Timer2):
Time →
┌─────────────────────────────────────────────────────┐
255 ──┤ ▲ │
│ ╱│╲ │
200 ──┤ ╱ │ ╲ │
│ ╱ │ ╲ │
150 ──┤ ╱ │ ╲ │
│ ╱ │ ╲ │
TCNT 100 ──┤ ╱ │ ╲ │
│ ╱ │ ╲ │
50 ──┤ ╱ │ ╲ │
│ ╱ │ ╲ │
0 ──┴────────────────────▼─────────┴─────────▼────────────┘
│◄──── One count cycle (256 ticks) ────►│
│ │
Overflow Overflow
Interrupt Interrupt
16-bit Counter Operation (Timer1):
┌─────────────────────────────────────────────────────┐
65535 ──┤ ▲ │
│ ╱ ╲ │
50000 ──┤ ╱ ╲ │
│ ╱ ╲ │
30000 ──┤ ╱ ╲ │
│ ╱ ╲ │
TCNT1 │ ╱ ╲ │
15000 ──┤ ╱ ╲ │
│ ╱ ╲ │
0 ──┴─────────────────────▼─────────────────▼─────────────┘
│◄──── One count cycle (65536 ticks) ──►│
Compare Match Mechanism
When the counter value equals a compare register (OCRnA or OCRnB), several things can happen depending on configuration:
Compare Match Operation:
Counter value: 0 50 100 150 200 OCR=200 255 0
│ │ │ │ │ ▼ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
TCNT: ────┴─────┴─────┴────┴────┴──────────┴────┴────►
│
│ Compare Match!
▼
┌─────────────────────────────────────────────────┐
│ What can happen: │
│ │
│ 1. Trigger Interrupt (ISR executes) │
│ 2. Toggle/Set/Clear output pin (OCnA/OCnB) │
│ 3. Reset counter to 0 (CTC mode) │
│ 4. Change PWM output state │
└─────────────────────────────────────────────────┘
ATmega328P Timer Summary
The ATmega328P has three timers with different capabilities:
┌─────────────────────────────────────────────────────────────────────┐
│ ATmega328P Timer Overview │
├─────────────┬───────────┬───────────┬───────────────────────────────┤
│ Timer │ Bits │ Pins │ Best Used For │
├─────────────┼───────────┼───────────┼───────────────────────────────┤
│ Timer0 │ 8-bit │ OC0A=PD6 │ PWM output, millis() base, │
│ │ │ OC0B=PD5 │ short delays │
│ │ │ (pin 6,5) │ │
├─────────────┼───────────┼───────────┼───────────────────────────────┤
│ Timer1 │ 16-bit │ OC1A=PB1 │ Precise timing, servo PWM, │
│ │ │ OC1B=PB2 │ input capture, long periods │
│ │ │ (pin 9,10)│ (up to 4+ seconds) │
├─────────────┼───────────┼───────────┼───────────────────────────────┤
│ Timer2 │ 8-bit │ OC2A=PB3 │ PWM, async operation, │
│ │ │ OC2B=PD3 │ tone generation, RTC with │
│ │ │(pin 11,3) │ external 32.768kHz crystal │
└─────────────┴───────────┴───────────┴───────────────────────────────┘
2.2 Timer Modes (Normal, CTC, PWM)
Understanding timer modes is crucial. Each mode changes how the counter behaves when it reaches certain values.
Normal Mode
In Normal mode, the counter simply counts from 0 to MAX (255 for 8-bit, 65535 for 16-bit) and then overflows back to 0.
Normal Mode Operation (8-bit):
255 ──┬─────────▲─────────────▲─────────────▲─────────────
│ ╱│ ╱│ ╱│
200 ──┼───────╱─┼───────────╱─┼───────────╱─┼─────────────
│ ╱ │ ╱ │ ╱ │
150 ──┼─────╱───┼─────────╱───┼─────────╱───┼─────────────
TCNT │ ╱ │ ╱ │ ╱ │
100 ──┼───╱─────┼───────╱─────┼───────╱─────┼─────────────
│ ╱ │ ╱ │ ╱ │
50 ──┼─╱───────┼─────╱───────┼─────╱───────┼─────────────
│╱ │ ╱ │ ╱ │
0 ──▼─────────└───▼─────────└───▼─────────└─────────────
│ TOV0 │ TOV0 │ TOV0 │
│interrupt│ interrupt │ interrupt │
│ │ │ │
│◄─ 256 ─►│◄─── 256 ───►│◄─── 256 ───►│
ticks ticks ticks
Period = 256 * (1 / timer_clock)
At 16MHz with prescaler 1024: Period = 256 * (1024/16000000) = 16.384 ms
Normal Mode Use Cases:
- Simple overflow interrupts at fixed intervals
- Measuring elapsed time (read TCNT, calculate difference)
- Base for software timers
Normal Mode Registers (Timer1):
TCCR1A = 0x00; // Normal mode (WGM = 0000)
TCCR1B = (1 << CS12); // Prescaler = 256
TIMSK1 = (1 << TOIE1); // Enable overflow interrupt
// Period = 65536 * 256 / 16000000 = 1.048576 seconds
CTC Mode (Clear Timer on Compare Match)
CTC mode is the workhorse for precise timing. The counter resets to 0 when it matches OCRnA, giving you exact control over the period.
CTC Mode Operation (Timer1, OCR1A = 15624):
OCR1A
15624 ──┬───────▲───────────▲───────────▲───────────
│ ╱│ ╱│ ╱│
10000 ──┼─────╱─┼─────────╱─┼─────────╱─┼───────────
│ ╱ │ ╱ │ ╱ │
5000 ──┼───╱───┼───────╱───┼───────╱───┼───────────
TCNT1 │ ╱ │ ╱ │ ╱ │
2500 ──┼─╱─────┼─────╱─────┼─────╱─────┼───────────
│╱ │ ╱ │ ╱ │
0 ──▼───────└───▼───────└───▼───────└───────────
│ Compare│ Compare │ Compare │
│ Match! │ Match! │ Match! │
│Counter │ Counter │ Counter │
│ clears │ clears │ clears │
│ │ │ │
│◄─────►│◄─────────►│◄─────────►│
15625 15625 15625
counts counts counts
Key Insight: You control the TOP value!
Period = (OCR1A + 1) * prescaler / F_CPU
Frequency = F_CPU / (prescaler * (OCR1A + 1))
Example: 1 Hz with prescaler 1024
OCR1A = (16000000 / (1024 * 1)) - 1 = 15624
Period = (15624 + 1) * 1024 / 16000000 = 1.000 second exactly!
CTC Mode for Square Wave Generation:
CTC Mode with Toggle OC1A:
OCR1A
15624 ──┬───────▲───────────▲───────────▲───────────
│ ╱│ ╱│ ╱│
TCNT1 │ ╱ │ ╱ │ ╱ │
│ ╱ │ ╱ │ ╱ │
0 ──▼───────└───────▼───└───────▼───└───────────
OC1A Pin:
HIGH ──┬───────┐ ┌───────────┐
│ │ │ │
LOW ──┴───────└───────────┘ └───────────
│◄─────►│◄─────────►│◄─────────►│
T/2 T/2 T/2
Square wave frequency = F_CPU / (2 * prescaler * (OCR1A + 1))
CTC Mode Registers (Timer1 for 1 Hz):
TCCR1A = 0; // CTC mode (WGM12 in TCCR1B)
TCCR1B = (1 << WGM12) | // CTC mode
(1 << CS12) | (1 << CS10); // Prescaler = 1024
OCR1A = 15624; // (16000000/1024/1) - 1
TIMSK1 = (1 << OCIE1A); // Compare match interrupt
Fast PWM Mode
Fast PWM counts from 0 to TOP (either 255/65535 or OCRnA) in a single slope. The output pin state changes based on compare match.
Fast PWM Mode (8-bit, non-inverting):
TOP=255
255 ──┬────▲────────────▲────────────▲────────────
│ ╱│ ╱│ ╱│
200 ──┼──╱─┼──────────╱─┼──────────╱─┼────────────
│ ╱ │ ╱ │ ╱ │
OCRnA=150─┼╱───┼────────╱───┼────────╱───┼─── Compare
│ │ ╱ │ ╱ │ Match
100 ──┼────┼──────╱─────┼──────╱─────┼────────────
TCNT │ │ ╱ │ ╱ │
50 ──┼────┼────╱───────┼────╱───────┼────────────
│ │ ╱ │ ╱ │
0 ──▼────└──▼─────────└──▼─────────└────────────
│ │ │ │
│ │ │ │
OCnA Pin (non-inverting COM0A1=1, COM0A0=0):
HIGH ──┬────────────────┬────────────────┬────────
│ │ │
│ ┌───────────┤ ┌───────────┤
LOW ──┴────┘ OCR/256 └────┘ OCR/256 └────────
│◄─────────────►│◄─────────────►│
One PWM One PWM
period period
Duty Cycle = (OCRnA + 1) / 256 (for 8-bit)
At OCRnA = 150: Duty = 151/256 = 59%
PWM Frequency = F_CPU / (prescaler * 256)
At prescaler 64: PWM freq = 16000000 / (64 * 256) = 976.56 Hz
Non-Inverting vs Inverting PWM:
Non-Inverting (COM0A1=1, COM0A0=0):
- Pin HIGH from BOTTOM to Compare Match
- Pin LOW from Compare Match to TOP
- Higher OCRnA = higher duty cycle = brighter LED
HIGH ──┬────────────┐ ┌────────────
│ 150 ticks │ 105 ticks │
LOW ──┴────────────└─────────────────┘────────────
│◄── ON ────►│◄──── OFF ──────►│
Inverting (COM0A1=1, COM0A0=1):
- Pin LOW from BOTTOM to Compare Match
- Pin HIGH from Compare Match to TOP
- Higher OCRnA = lower duty cycle
HIGH ──┬ ┌─────────────────┐
│ │ 105 ticks │
LOW ──┴────────────┘ 150 ticks └────────────
│◄── OFF ───►│◄──── ON ───────►│
Fast PWM Registers (Timer0 for LED dimming):
DDRD |= (1 << PD6); // OC0A as output
TCCR0A = (1 << COM0A1) | // Non-inverting PWM on OC0A
(1 << WGM01) | (1 << WGM00); // Fast PWM mode
TCCR0B = (1 << CS01) | (1 << CS00); // Prescaler = 64
OCR0A = 127; // 50% duty cycle
// PWM frequency = 16000000 / (64 * 256) = 976.56 Hz
Phase-Correct PWM Mode
Phase-Correct PWM counts up to TOP, then counts back down to 0 (dual-slope). This creates symmetric PWM pulses centered in the period, which is better for motor control.
Phase-Correct PWM Mode (8-bit):
255 ──┬──────────/\──────────────────/\──────────
│ / \ / \
200 ──┼────────/────\──────────────/────\────────
│ / \ / \
OCRnA=150─┼──────/────────\──────────/────────\──Compare
│ / \ / \ Match
100 ──┼────/────────────\──────/────────────\──(both
TCNT │ / \ / \ ways)
50 ──┼──/────────────────\──/────────────────\
│ / \/ \
0 ──▼/────────────────────▼────────────────────
│◄────── UP ────────►│◄───── DOWN ─────►│
│◄────────── One Period ───────────────►│
OCnA Pin:
HIGH ──┬──────┐ ┌──────┐
│ │ │ │
LOW ──┴──────└────────────────────┘──────└────
│◄────►│◄─────────────────►│◄────►│
ON OFF ON
Note: PWM is symmetric around the peak!
This reduces harmonic content and noise.
PWM Frequency = F_CPU / (prescaler * 510) (not 512!)
At prescaler 64: PWM freq = 16000000 / (64 * 510) = 490.2 Hz
Why Phase-Correct for Motors?
Fast PWM (motor current):
┌────┐ ┌────┐ ┌────┐
│ │ │ │ │ │
────┘ └──────────────┘ └──────────────┘ └────
│◄──►│ │◄──►│
Current spike Current spike
at same time at same time
All PWM transitions align = large current spikes!
Phase-Correct PWM (motor current):
┌────┐ ┌────┐ ┌────┐
│ │ │ │ │ │
───────┘ └────────────┘ └────────────┘ └─────
│◄──►│
Current pulses spread out
Less electrical noise!
2.3 Prescaler Theory
The prescaler divides the system clock to slow down the timer, enabling longer periods with the same counter size.
Prescaler as a Clock Divider
Prescaler Operation:
System Clock (16 MHz):
────┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬────►
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
16 million pulses per second
Prescaler = 1 (no division):
────┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬────►
Timer clock = 16 MHz
Prescaler = 8:
────┬───────┬───────┬───────┬───────┬───────┬───────┬──►
│ │ │ │ │ │ │
Timer clock = 2 MHz (every 8th CPU clock)
Prescaler = 64:
────┬───────────────────────────────────────────────┬──►
│ │
Timer clock = 250 kHz (every 64th CPU clock)
Prescaler = 1024:
────┬─────────────────────────────────────────────────────────────────────────┬──►
│ (long gap between timer ticks) │
Timer clock = 15,625 Hz (every 1024th CPU clock)
Prescaler Selection Table for 16 MHz
┌──────────────────────────────────────────────────────────────────────────────┐
│ Prescaler Selection Guide (F_CPU = 16 MHz) │
├───────────┬────────────┬──────────────┬──────────────┬──────────────────────┤
│ Prescaler │Timer Clock │ 8-bit Max │ 16-bit Max │ Typical Use │
│ │ (Hz) │ Period │ Period │ │
├───────────┼────────────┼──────────────┼──────────────┼──────────────────────┤
│ 1 │ 16,000,000 │ 16 us │ 4.096 ms │ High-freq PWM, │
│ │ │ │ │ microsecond timing │
├───────────┼────────────┼──────────────┼──────────────┼──────────────────────┤
│ 8 │ 2,000,000 │ 128 us │ 32.768 ms │ Audio frequencies, │
│ │ │ │ │ fast PWM │
├───────────┼────────────┼──────────────┼──────────────┼──────────────────────┤
│ 64 │ 250,000 │ 1.024 ms │ 262.144 ms │ General PWM, │
│ │ │ │ │ millisecond timing │
├───────────┼────────────┼──────────────┼──────────────┼──────────────────────┤
│ 256 │ 62,500 │ 4.096 ms │ 1.048576 s │ Second-resolution │
│ │ │ │ │ timing │
├───────────┼────────────┼──────────────┼──────────────┼──────────────────────┤
│ 1024 │ 15,625 │ 16.384 ms │ 4.194304 s │ Long delays, │
│ │ │ │ │ low-power timing │
└───────────┴────────────┴──────────────┴──────────────┴──────────────────────┘
Calculation formulas:
Timer Clock = F_CPU / Prescaler
Max Period (8-bit) = 256 / Timer_Clock
Max Period (16-bit) = 65536 / Timer_Clock
Choosing the Right Prescaler
Goal: Generate a 1 Hz (1 second) interrupt
Step 1: Determine required timer counts
Counts_needed = F_CPU / (Prescaler * Frequency)
Step 2: Check if counts fit in timer
8-bit timer: must be <= 256
16-bit timer: must be <= 65536
Example calculations for 1 Hz:
Prescaler 1: 16,000,000 / (1 * 1) = 16,000,000 counts (too large!)
Prescaler 8: 16,000,000 / (8 * 1) = 2,000,000 counts (too large!)
Prescaler 64: 16,000,000 / (64 * 1) = 250,000 counts (too large for 16-bit!)
Prescaler 256: 16,000,000 / (256 * 1) = 62,500 counts (fits in 16-bit!)
Prescaler 1024: 16,000,000 / (1024 * 1) = 15,625 counts (fits in 16-bit!)
Both 256 and 1024 work. Choose based on:
- Lower prescaler = finer resolution (can adjust in smaller steps)
- Higher prescaler = simpler math (15,625 is "rounder" than 62,500)
- With prescaler 1024: OCR1A = 15625 - 1 = 15624
2.4 PWM Fundamentals
Duty Cycle and Average Voltage
PWM creates the illusion of analog voltage by rapidly switching between HIGH and LOW:
PWM Duty Cycle Visualization:
100% Duty Cycle (OCRnA = 255):
HIGH ─────────────────────────────────────────────────────
LOW _____________________________________________________
Average voltage = 5V * 100% = 5.0V
LED: Maximum brightness
75% Duty Cycle (OCRnA = 191):
HIGH ─────────────────────────┐
└───────────────────────────
LOW _________________________
│◄─────── 75% ON ───────►│◄──── 25% OFF ───►│
Average voltage = 5V * 75% = 3.75V
LED: Bright but not maximum
50% Duty Cycle (OCRnA = 127):
HIGH ───────────────┐ ┌───────────────
└─────────────────────┘
LOW _______________ ________________
│◄── 50% ON ──►│◄──── 50% OFF ─────►│
Average voltage = 5V * 50% = 2.5V
LED: Medium brightness
25% Duty Cycle (OCRnA = 63):
HIGH ────────┐
└────────────────────────────────────────────
LOW ________
│◄ 25% ►│◄────────── 75% OFF ───────────────►│
Average voltage = 5V * 25% = 1.25V
LED: Dim
0% Duty Cycle (OCRnA = 0):
HIGH _____________________________________________________
LOW ─────────────────────────────────────────────────────
Average voltage = 5V * 0% = 0V
LED: Off
PWM Frequency Considerations
PWM Frequency Trade-offs:
Too LOW frequency (< 100 Hz):
┌────────────────────────────────────────────────────────┐
│ Visible flicker with LEDs │
│ Motor "cogging" (jerky motion) │
│ Audible buzz with motors │
└────────────────────────────────────────────────────────┘
Too HIGH frequency (> 100 kHz):
┌────────────────────────────────────────────────────────┐
│ Increased switching losses (heat) │
│ EMI (electromagnetic interference) │
│ Driver transistor stress │
└────────────────────────────────────────────────────────┘
Sweet spots by application:
Application │ Recommended Frequency │ Reason
────────────────────┼───────────────────────┼────────────────────
LED dimming │ 500 Hz - 2 kHz │ No flicker, efficient
DC motor control │ 1 kHz - 20 kHz │ Smooth torque, quiet
Servo motors │ 50 Hz (standard) │ Servo protocol spec
Switching power │ 50 kHz - 500 kHz │ Small inductors
Audio PWM │ 40 kHz+ │ Above audible range
PWM Resolution
Resolution Trade-off:
8-bit PWM (256 steps):
└──────────────────────────────────────────────────────────┘
0% ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 100%
▲ ▲
0 255
Each step = 1/256 = 0.39% change
At 50% brightness, next step = 50.39%
Good for most LED applications
10-bit PWM (1024 steps):
└──────────────────────────────────────────────────────────┘
0% ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100%
▲ ▲
0 1023
Each step = 1/1024 = 0.098% change
Smoother fades, better for video/display
16-bit PWM (65536 steps):
└──────────────────────────────────────────────────────────┘
0% ████████████████████████████████████████████ 100%
▲ ▲
0 65535
Each step = 1/65536 = 0.0015% change
Near-continuous, professional grade
2.5 Timer Interrupts
Timer interrupts allow you to execute code at precise intervals without polling.
Timer Interrupt Flow:
Normal Program Execution:
─────────────────────────────────────────────────────────────────►
│ │ │
│ Timer Match │ Timer Match │ Timer Match
│ Interrupt │ Interrupt │ Interrupt
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ ISR │ │ ISR │ │ ISR │
│ runs │ │ runs │ │ runs │
└───┬───┘ └───┬───┘ └───┬───┘
│ │ │
────────┘ └────────────────────┘─────────────►
Program resumes
ISR Execution Details:
1. Timer match occurs
┌──────────────────────────────────────────────────────────┐
│ TCNT1 == OCR1A ? Yes! │
│ Set interrupt flag (OCF1A in TIFR1) │
└──────────────────────────────────────────────────────────┘
│
2. CPU notices flag ▼
┌──────────────────────────────────────────────────────────┐
│ Is interrupt enabled (OCIE1A in TIMSK1)? │
│ Is global interrupt enabled (I flag in SREG)? │
│ If both yes, save current PC to stack │
│ Clear interrupt flag automatically │
│ Disable global interrupts (prevent nesting) │
│ Jump to ISR vector │
└──────────────────────────────────────────────────────────┘
│
3. ISR executes ▼
┌──────────────────────────────────────────────────────────┐
│ ISR(TIMER1_COMPA_vect) { │
│ // Your code here │
│ uptime_seconds++; │
│ PORTB ^= (1 << PB5); // Toggle LED │
│ } │
└──────────────────────────────────────────────────────────┘
│
4. RETI (return from interrupt)
┌──────────────────────────────────────────────────────────┐
│ Restore SREG (including I flag) │
│ Pop PC from stack │
│ Resume normal execution │
└──────────────────────────────────────────────────────────┘
Critical ISR Guidelines
ISR Best Practices:
DO: DON'T:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Keep ISR short and fast │ │ Call blocking functions │
│ Use volatile for shared vars│ │ Perform floating point math │
│ Clear flags if needed │ │ Call printf/uart_puts │
│ Set flags for main to check │ │ Loop for long periods │
│ │ │ Allocate memory │
└─────────────────────────────┘ └─────────────────────────────┘
Example - Good ISR:
ISR(TIMER1_COMPA_vect) {
tick_flag = 1; // Fast: just set a flag
milliseconds += 1000; // Fast: simple arithmetic
}
Example - Bad ISR:
ISR(TIMER1_COMPA_vect) {
uart_printf("Tick at %lu ms\n", milliseconds); // SLOW!
_delay_ms(10); // NEVER delay in ISR!
float x = sin(angle); // Floating point is slow!
}
2.6 Common Misconceptions
| Misconception | Reality |
|---|---|
| “PWM creates analog voltage” | PWM is digital switching; the load’s response (averaging) creates the effect |
| “Higher PWM frequency is always better” | Higher frequency increases switching losses and EMI |
| “Timer prescaler affects CPU speed” | Prescaler only divides the timer clock, not the CPU clock |
| “CTC and PWM are mutually exclusive” | Some modes combine both (e.g., Fast PWM with variable TOP) |
| “8-bit timers can’t do long delays” | Combine with overflow counting for arbitrary periods |
| “Phase-correct PWM is always better” | It has half the frequency of Fast PWM; use Fast for LEDs |
| “OCRnA = 0 means 0% duty cycle” | In Fast PWM, OCRnA=0 still gives one clock cycle HIGH |
| “Timer interrupts are instant” | There’s latency; ISR execution takes time |
3. Project Specification
3.1 What You Will Build
A multi-function timer demonstration that showcases all major timer capabilities:
Project Architecture:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Main Application │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────────────┐ │
│ │ Timer1 │ │ Timer0 │ │ Timer2 │ │
│ │ (16-bit) │ │ (8-bit) │ │ (8-bit) │ │
│ │ │ │ │ │ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────────────────────┐ │ │
│ │ │ CTC Mode │ │ │ │ Fast PWM │ │ │ │ CTC Mode + OC2A Toggle │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ 1 Hz │ │ │ │ ~1 kHz │ │ │ │ Variable frequency │ │ │
│ │ │ interrupt│ │ │ │ LED fade │ │ │ │ tone generation │ │ │
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │ └───────────┬─────────────┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │ ▼ │ │
│ │ uptime++ │ │ OC0A pin │ │ OC2A pin │ │
│ │ tick_flag=1 │ │ (PD6) │ │ (PB3) │ │
│ │ │ │ │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Hardware Outputs │ │
│ │ │ │
│ │ Pin 13 (PB5) Pin 6 (OC0A) Pin 11 (OC2A) │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ Onboard │ │ External │ │ Piezo │ │ │
│ │ │ LED │ │ LED │ │ Buzzer │ │ │
│ │ │ (1Hz blink│ │ (PWM fade)│ │ (tones) │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ UART Interface │ │
│ │ │ │
│ │ Commands: │ │
│ │ pwm <0-255> - Set LED brightness │ │
│ │ tone <Hz> - Play tone at frequency │ │
│ │ stop - Stop tone │ │
│ │ melody - Play demo melody │ │
│ │ uptime - Show elapsed time │ │
│ │ status - Show all timer states │ │
│ │ help - Show available commands │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
3.2 Functional Requirements
| ID | Requirement | Priority | Acceptance Criteria |
|---|---|---|---|
| FR1 | 1-second precise timing | Must | LED blinks exactly 60 times per minute |
| FR2 | Millisecond uptime counter | Must | Reported time within 0.1% of actual |
| FR3 | PWM LED fade (0-100%) | Must | Smooth fade visible, no stepping |
| FR4 | UART PWM control | Must | “pwm 128” sets 50% brightness |
| FR5 | Tone generation | Should | A4 (440Hz) matches tuner app |
| FR6 | Variable frequency tones | Should | User-specified Hz plays correctly |
| FR7 | Simple melody playback | Nice | Sequence of notes plays in order |
| FR8 | Multiple simultaneous timers | Must | All three timers work together |
3.3 Non-Functional Requirements
| ID | Requirement | Metric | Test Method |
|---|---|---|---|
| NFR1 | 1-second accuracy | < 0.1% error | Count 60 blinks, compare to stopwatch |
| NFR2 | PWM frequency | > 500 Hz | No visible LED flicker |
| NFR3 | Tone accuracy | < 2% error | Verify with tuner/frequency counter |
| NFR4 | Code size | < 4KB | Check with avr-size |
| NFR5 | Non-blocking operation | Main loop runs | UART responsive during PWM/tone |
3.4 Example Output
$ screen /dev/ttyACM0 115200
=== AVR Timer/PWM Demo ===
Build: Dec 28 2024 14:30:00
Timer1: CTC mode, 1 Hz interrupt
Timer0: Fast PWM, ~976 Hz
Timer2: CTC mode for tones
[0.000s] System started
[1.000s] Tick
[2.000s] Tick
[3.000s] Tick
> help
Available commands:
pwm <0-255> - Set LED brightness (0=off, 255=max)
fade - Start auto-fade animation
tone <Hz> - Play tone (100-10000 Hz)
stop - Stop tone
melody - Play demo melody
uptime - Show uptime in ms
status - Show timer configuration
help - This message
> pwm 0
PWM set to 0 (0%)
LED off
> pwm 128
PWM set to 128 (50%)
> pwm 255
PWM set to 255 (100%)
Maximum brightness
> fade
Starting fade animation...
0% -> 100% -> 0% (3 second cycle)
Press any key to stop.
(LED smoothly fades in and out)
> tone 440
Playing 440 Hz (A4)
OCR2A = 70, Prescaler = 256
Actual frequency: 440.14 Hz
> stop
Tone stopped
> melody
Playing demo melody...
C4 (262 Hz) 250ms
E4 (330 Hz) 250ms
G4 (392 Hz) 250ms
C5 (523 Hz) 500ms
Done!
> uptime
Uptime: 45.237 seconds (45237 ms)
> status
Timer Configuration:
Timer1: CTC mode, OCR1A=15624, prescaler=1024
Interrupt rate: 1.000 Hz
Uptime: 45 seconds
Timer0: Fast PWM, OCR0A=128, prescaler=64
PWM frequency: 976.56 Hz
Duty cycle: 50.2%
Timer2: CTC mode, OCR2A=70, prescaler=256
Tone frequency: 440.14 Hz (A4)
Status: Playing
3.5 Real World Outcome
Upon completion, you will have:
- Precise timing capability - Generate exact delays without burning CPU cycles
- PWM mastery - Control LED brightness, motor speed, or any analog-like output
- Frequency generation - Create audio tones, carrier signals, or timing references
- Multi-timer coordination - Run multiple independent timing operations
- Interview readiness - Explain timer architectures in technical interviews
- RTOS foundation - Understand how operating system tick timers work
4. Solution Architecture
4.1 High-Level Design
┌─────────────────────────────────────────────────────────────────────────────┐
│ main.c │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ int main(void) { │ │
│ │ timer1_init(); // 1 Hz system tick │ │
│ │ pwm_init(); // Timer0 PWM for LED │ │
│ │ tone_init(); // Timer2 for audio │ │
│ │ uart_init(115200); │ │
│ │ sei(); // Enable interrupts │ │
│ │ │ │
│ │ while(1) { │ │
│ │ if (tick_flag) { handle_tick(); } │ │
│ │ if (fade_active) { update_fade(); } │ │
│ │ process_uart_commands(); │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────┬─────────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ timer.c │ │ pwm.c │ │ tone.c │
│ │ │ │ │ │
│ timer1_init() │ │ pwm_init() │ │ tone_init() │
│ get_uptime_ms() │ │ pwm_set_duty() │ │ tone_play(freq_hz) │
│ get_uptime_sec() │ │ pwm_get_duty() │ │ tone_stop() │
│ │ │ fade_start() │ │ tone_is_playing() │
│ ISR(TIMER1_COMPA) │ │ fade_update() │ │ melody_play() │
│ { │ │ │ │ │
│ uptime_ms+=1000; │ │ // Uses Timer0 │ │ // Uses Timer2 │
│ tick_flag=1; │ │ // OC0A on PD6 │ │ // OC2A on PB3 │
│ } │ │ │ │ │
└─────────┬───────────┘ └─────────┬───────────┘ └─────────┬───────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hardware Layer │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Timer1 │ │ Timer0 │ │ Timer2 │ │
│ │ │ │ │ │ │ │
│ │ TCCR1A = 0x00 │ │ TCCR0A = 0x83 │ │ TCCR2A = 0x42 │ │
│ │ TCCR1B = 0x0D │ │ TCCR0B = 0x03 │ │ TCCR2B = 0x06 │ │
│ │ OCR1A = 15624 │ │ OCR0A = duty │ │ OCR2A = tone │ │
│ │ TIMSK1 = 0x02 │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Interrupt→ISR │ │ PWM out→PD6 │ │ Toggle→PB3 │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ LED │ │ Buzzer │ │
│ │ (fade) │ │ (tones) │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 Timer Register Map
Timer1 Registers (16-bit, for system tick):
┌─────────────────────────────────────────────────────────────────────────────┐
│ Register │ Address │ Bits Used │ Value │ Purpose │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR1A │ 0x80 │ WGM11:10 │ 0x00 │ Normal port operation, │
│ │ │ COM1A/B │ │ WGM=0100 (CTC with OCR1A) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR1B │ 0x81 │ WGM13:12 │ 0x0D │ CTC mode (WGM12=1), │
│ │ │ CS12:10 │ │ Prescaler=1024 (CS12,CS10=1) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR1C │ 0x82 │ FOC1A/B │ 0x00 │ Force Output Compare (unused) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCNT1H/L │ 0x85/84 │ 16-bit counter│ 0-TOP │ Current count value │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR1AH/L │ 0x89/88 │ 16-bit compare│ 15624 │ Compare value for 1 Hz │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR1BH/L │ 0x8B/8A │ 16-bit compare│ N/A │ Second compare (unused) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ ICR1H/L │ 0x87/86 │ 16-bit capture│ N/A │ Input capture (unused) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIMSK1 │ 0x6F │ OCIE1A │ 0x02 │ Enable compare A interrupt │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIFR1 │ 0x36 │ OCF1A │ (auto) │ Compare match flag (auto-clr) │
└──────────┴─────────┴───────────────┴────────┴───────────────────────────────┘
Timer0 Registers (8-bit, for PWM):
┌─────────────────────────────────────────────────────────────────────────────┐
│ Register │ Address │ Bits Used │ Value │ Purpose │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR0A │ 0x44 │ COM0A1:0 │ 0x83 │ Non-inverting PWM on OC0A, │
│ │ │ WGM01:00 │ │ Fast PWM mode (WGM=011) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR0B │ 0x45 │ WGM02 │ 0x03 │ Prescaler=64 (CS01,CS00=1) │
│ │ │ CS02:00 │ │ │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCNT0 │ 0x46 │ 8-bit counter │ 0-255 │ Current count value │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR0A │ 0x47 │ 8-bit compare │ 0-255 │ PWM duty cycle │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR0B │ 0x48 │ 8-bit compare │ N/A │ Second PWM channel (unused) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIMSK0 │ 0x6E │ OCIE0A/TOIE0 │ 0x00 │ No interrupts needed for PWM │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIFR0 │ 0x35 │ OCF0A/TOV0 │ (auto) │ Flags (not used) │
└──────────┴─────────┴───────────────┴────────┴───────────────────────────────┘
Timer2 Registers (8-bit, for tones):
┌─────────────────────────────────────────────────────────────────────────────┐
│ Register │ Address │ Bits Used │ Value │ Purpose │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR2A │ 0xB0 │ COM2A1:0 │ 0x42 │ Toggle OC2A on compare, │
│ │ │ WGM21:20 │ │ CTC mode (WGM=010) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCCR2B │ 0xB1 │ WGM22 │ 0x06 │ Prescaler=256 (CS22,CS21=1) │
│ │ │ CS22:20 │ │ │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TCNT2 │ 0xB2 │ 8-bit counter │ 0-OCR │ Current count value │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR2A │ 0xB3 │ 8-bit compare │ varies │ Sets tone frequency │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ OCR2B │ 0xB4 │ 8-bit compare │ N/A │ Second channel (unused) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIMSK2 │ 0x70 │ OCIE2A/TOIE2 │ 0x00 │ No interrupts (pin toggles) │
├──────────┼─────────┼───────────────┼────────┼───────────────────────────────┤
│ TIFR2 │ 0x37 │ OCF2A/TOV2 │ (auto) │ Flags (not used) │
└──────────┴─────────┴───────────────┴────────┴───────────────────────────────┘
4.3 PWM Waveform Analysis
Fast PWM Waveform (Timer0, OCR0A = 191, 75% duty):
TCNT0:
255 ─┬────────────▲────────────────────▲────────────────────
│ ╱│ ╱│
│ ╱ │ ╱ │
191 ─┼─────────╱──┼─────────────────╱──┼─ OCR0A (compare)
│ ╱ │ ╱ │
│ ╱ │ ╱ │
128 ─┼──────╱─────┼──────────────╱─────┼─────────────────────
│ ╱ │ ╱ │
│ ╱ │ ╱ │
64 ─┼───╱────────┼───────────╱────────┼─────────────────────
│ ╱ │ ╱ │
│ ╱ │ ╱ │
0 ─▼────────────└────────▼───────────└─────────────────────
│◄─────── Period ─────►│◄─────── Period ─────►│
│ 256 timer ticks │
│ 256 * 64 / 16MHz │
│ = 1.024 ms │
│ = 976.56 Hz │
OC0A Pin (PD6) Output:
HIGH ┬─────────────────────┐ ┌───────────────────────
│ │ │
│ 191 ticks │ 64 ticks │
│ (74.6%) │ (25.4%) │
│ │ │
LOW ┴─────────────────────└───────────┘───────────────────────
│◄─────── ON ────────►│◄── OFF ──►│
Different Duty Cycles Comparison:
OCR0A = 0 (0.4% duty - almost off but not quite!):
HIGH ┬┐ ┌┐
││ ││
LOW ┴┴───────────────────────────────┴┴───────────────────────
│1│◄──────── 255 ───────────────►│1│
OCR0A = 63 (25% duty):
HIGH ┬─────────┐ ┌─────────┐
│ │ │ │
LOW ┴─────────┴───────────────────────┴─────────┴─────────────
│◄─ 64 ──►│◄────── 192 ──────────►│
OCR0A = 127 (50% duty):
HIGH ┬─────────────────┐ ┌─────────────────┐
│ │ │ │
LOW ┴─────────────────┴───────────────┴─────────────────┴─────
│◄──── 128 ──────►│◄──── 128 ───►│
OCR0A = 255 (100% duty - always on):
HIGH ┬─────────────────────────────────────────────────────────
│
LOW ┴_________________________________________________________
4.4 Timing Calculations
System Tick (1 Hz) Calculation
Goal: 1 Hz interrupt (1 second period)
Given:
F_CPU = 16,000,000 Hz
Desired frequency = 1 Hz
Using Timer1 (16-bit)
Step 1: Try each prescaler
Prescaler 1: Counts = 16,000,000 / 1 = 16,000,000 (exceeds 65536)
Prescaler 8: Counts = 16,000,000 / 8 = 2,000,000 (exceeds 65536)
Prescaler 64: Counts = 16,000,000 / 64 = 250,000 (exceeds 65536)
Prescaler 256: Counts = 16,000,000 / 256 = 62,500 (fits!)
Prescaler 1024: Counts = 16,000,000 / 1024 = 15,625 (fits, smaller number)
Step 2: Choose prescaler 1024 for simplicity
OCR1A = 15,625 - 1 = 15624
Step 3: Verify
Actual frequency = 16,000,000 / (1024 * 15625)
= 16,000,000 / 16,000,000
= 1.000000 Hz (exact!)
Register settings:
TCCR1A = 0; // Normal port operation
TCCR1B = (1 << WGM12) | // CTC mode
(1 << CS12) | (1 << CS10); // Prescaler 1024
OCR1A = 15624; // Compare value
TIMSK1 = (1 << OCIE1A); // Enable compare interrupt
PWM Frequency Calculation
Goal: ~1 kHz PWM for LED dimming (no visible flicker)
Given:
F_CPU = 16,000,000 Hz
Using Timer0 (8-bit Fast PWM)
TOP = 255 (fixed in this mode)
Formula: PWM_freq = F_CPU / (prescaler * 256)
Prescaler options:
Prescaler 1: 16,000,000 / (1 * 256) = 62,500 Hz (very high)
Prescaler 8: 16,000,000 / (8 * 256) = 7,812.5 Hz (good for motors)
Prescaler 64: 16,000,000 / (64 * 256) = 976.56 Hz (good for LEDs!)
Prescaler 256: 16,000,000 / (256 * 256) = 244.14 Hz (visible flicker)
Prescaler 1024: 16,000,000 / (1024 * 256) = 61.04 Hz (very visible flicker)
Choice: Prescaler 64 gives 976.56 Hz
- Above flicker threshold (~100 Hz)
- Low enough for efficient switching
Register settings:
DDRD |= (1 << PD6); // OC0A as output
TCCR0A = (1 << COM0A1) | // Non-inverting PWM
(1 << WGM01) | (1 << WGM00); // Fast PWM mode
TCCR0B = (1 << CS01) | (1 << CS00); // Prescaler 64
OCR0A = 127; // 50% duty cycle
Tone Frequency Calculation
Goal: Generate 440 Hz (A4 musical note)
Given:
F_CPU = 16,000,000 Hz
Using Timer2 (8-bit) in CTC mode with toggle
Square wave: toggle on each compare match
Formula (square wave with toggle):
OCR2A = F_CPU / (2 * prescaler * frequency) - 1
Prescaler analysis for 440 Hz:
Prescaler 1: OCR = 16,000,000 / (2 * 1 * 440) - 1 = 18,180 (exceeds 255)
Prescaler 8: OCR = 16,000,000 / (2 * 8 * 440) - 1 = 2,272 (exceeds 255)
Prescaler 32: OCR = 16,000,000 / (2 * 32 * 440) - 1 = 567 (exceeds 255)
Prescaler 64: OCR = 16,000,000 / (2 * 64 * 440) - 1 = 283 (exceeds 255)
Prescaler 128: OCR = 16,000,000 / (2 * 128 * 440) - 1 = 141 (fits!)
Prescaler 256: OCR = 16,000,000 / (2 * 256 * 440) - 1 = 70 (fits!)
Note: Timer2 has prescalers 1, 8, 32, 64, 128, 256, 1024
Choice: Prescaler 256 with OCR2A = 70
Actual frequency = 16,000,000 / (2 * 256 * 71) = 440.14 Hz
Error = (440.14 - 440) / 440 * 100 = 0.032% (excellent!)
Register settings:
DDRB |= (1 << PB3); // OC2A as output
TCCR2A = (1 << COM2A0) | // Toggle OC2A on compare
(1 << WGM21); // CTC mode
TCCR2B = (1 << CS22) | (1 << CS21); // Prescaler 256
OCR2A = 70; // For 440 Hz
Frequency range with prescaler 256:
Minimum freq (OCR2A = 255): 16,000,000 / (2 * 256 * 256) = 122 Hz
Maximum freq (OCR2A = 1): 16,000,000 / (2 * 256 * 2) = 15,625 Hz
For higher frequencies, use smaller prescaler:
Prescaler 64, OCR2A = 1: 16,000,000 / (2 * 64 * 2) = 62,500 Hz
5. Implementation Guide
5.1 Development Environment
Hardware required:
- Arduino Uno or compatible ATmega328P board
- LED with 220-330 ohm resistor (for PWM output on pin 6)
- Piezo buzzer or small speaker (for tone output on pin 11)
- USB cable
- (Optional) Oscilloscope or logic analyzer for verification
Software setup:
# Verify toolchain (should already be installed from Projects 1-2)
avr-gcc --version
avrdude -?
# Project directory
mkdir -p timer_pwm && cd timer_pwm
5.2 Project Structure
timer_pwm/
├── main.c # Main loop and command processing
├── timer.h # Timer1 system tick declarations
├── timer.c # Timer1 implementation and ISR
├── pwm.h # PWM declarations
├── pwm.c # Timer0 PWM implementation
├── tone.h # Tone generator declarations
├── tone.c # Timer2 tone implementation
├── uart.h # From Project 2
├── uart.c # From Project 2
└── Makefile # Build automation
5.3 The Core Question You’re Answering
“How do I achieve precise timing and analog-like outputs in a bare metal system without wasting CPU cycles?”
The answer: Hardware timers run independently of CPU execution. Configure them once, and they:
- Count clock cycles automatically
- Trigger interrupts at exact intervals
- Control output pins for PWM without CPU intervention
- Provide the foundation for all real-time systems
5.4 Concepts You Must Understand First
Before implementing, answer these self-assessment questions:
| Concept | Question | If Unsure, Review |
|---|---|---|
| Counter registers | What does TCNT1 contain? | Datasheet Section 16.3 |
| Compare match | What happens when TCNT equals OCR? | Datasheet Section 16.9 |
| Prescaler | How does prescaler 64 affect a 16 MHz clock? | Section 2.3 above |
| CTC mode | Why does the counter reset in CTC mode? | Datasheet Section 16.9.2 |
| Fast PWM | When does the output go HIGH/LOW? | Section 2.2 above |
| Interrupts | What must you do before ISRs can fire? | sei() enables global interrupts |
5.5 Questions to Guide Your Design
Timer Architecture Questions
- Which timer should handle the 1-second tick? Why?
- Why is 16-bit Timer1 better for long periods than 8-bit Timer0?
- What happens if you use the same timer for both PWM and CTC interrupt?
PWM Questions
- What PWM frequency eliminates visible LED flicker?
- If OCR0A = 127 in Fast PWM mode, what is the duty cycle percentage?
- Why does setting OCR0A = 0 not give 0% duty cycle in Fast PWM?
Tone Generation Questions
- Why do we toggle the pin rather than use PWM for tones?
- What OCR2A value gives 440 Hz with prescaler 256?
- What’s the maximum frequency achievable with Timer2?
System Integration Questions
- Can Timer1 interrupt while Timer0 PWM is running?
- How do you stop a tone without affecting the system tick?
- How would you add millisecond resolution to the uptime counter?
5.6 Thinking Exercise
Before coding, work through this exercise on paper:
Exercise: Calculate the register values for a 2 Hz LED blink
- Goal: Blink LED at 2 Hz (toggle every 500 ms)
- Choose timer: Timer1 (16-bit for longer periods)
- Calculate required counts for 0.5 second period:
- With prescaler 256: counts = 16,000,000 / (256 * 2) = ?
- With prescaler 1024: counts = 16,000,000 / (1024 * 2) = ?
- Which prescaler fits in 16 bits?
- What is OCR1A?
- Write the register configuration code.
Complete this before looking at the hints.
5.7 Hints in Layers
Hint 1: Starting Point (Conceptual)
Start with Timer1 CTC mode for the system tick. This is the most straightforward:
// Timer1 initialization skeleton
void timer1_init(void) {
// Step 1: Set CTC mode (Clear Timer on Compare)
// WGM12 bit in TCCR1B enables CTC mode
// Step 2: Set prescaler to 1024
// CS12 and CS10 bits in TCCR1B
// Step 3: Set compare value for 1 Hz
// OCR1A = (F_CPU / prescaler / frequency) - 1
// Step 4: Enable compare match interrupt
// OCIE1A bit in TIMSK1
}
// ISR for Timer1 compare match
ISR(TIMER1_COMPA_vect) {
// Increment your counters here
// Set a flag for main loop
// Keep it SHORT!
}
Hint 2: Next Level (PWM Setup)
Add Timer0 Fast PWM after the system tick works:
// PWM initialization
void pwm_init(void) {
// Set OC0A pin (PD6) as output
DDRD |= (1 << PD6);
// Configure TCCR0A:
// - COM0A1=1, COM0A0=0: Clear OC0A on compare (non-inverting)
// - WGM01=1, WGM00=1: Fast PWM mode
// Configure TCCR0B:
// - CS01=1, CS00=1: Prescaler 64
// Set initial duty cycle
OCR0A = 0; // Start with LED off
}
void pwm_set_duty(uint8_t duty) {
OCR0A = duty; // 0=off, 255=full brightness
}
Hint 3: Technical Details (Tone Generation)
Add Timer2 CTC with toggle for tones:
// Calculate OCR2A for a given frequency
uint8_t freq_to_ocr(uint16_t freq_hz) {
// Using prescaler 256
// Formula: OCR = F_CPU / (2 * prescaler * freq) - 1
uint32_t ocr = (F_CPU / (2UL * 256 * freq_hz)) - 1;
if (ocr > 255) ocr = 255;
if (ocr < 1) ocr = 1;
return (uint8_t)ocr;
}
void tone_play(uint16_t freq_hz) {
// Set OC2A (PB3) as output
DDRB |= (1 << PB3);
// CTC mode, toggle OC2A on compare
TCCR2A = (1 << COM2A0) | (1 << WGM21);
// Prescaler 256
TCCR2B = (1 << CS22) | (1 << CS21);
// Set frequency
OCR2A = freq_to_ocr(freq_hz);
}
void tone_stop(void) {
TCCR2A = 0; // Disable timer output
TCCR2B = 0; // Stop timer
PORTB &= ~(1 << PB3); // Ensure pin is LOW
}
Hint 4: Verification Tools
# Verify 1 Hz timing with stopwatch
# Count LED blinks for 60 seconds - should be exactly 60
# Verify PWM duty cycle with multimeter
# Set OCR0A = 127 (50%), measure voltage
# Expected: approximately 2.5V average
# Verify tone with smartphone tuner app
# Play 440 Hz, app should show A4 note
# For precise verification, use oscilloscope or logic analyzer
# sigrok-cli can capture and analyze signals
5.8 The Interview Questions They’ll Ask
- “Explain PWM. How does switching create analog-like voltage?”
- PWM rapidly switches between HIGH and LOW
- The load (LED, motor) averages the voltage due to its time constant
- Duty cycle controls the average: 50% duty = 50% average voltage
- “How do you calculate timer compare values for a specific frequency?”
- Formula: OCR = (F_CPU / (prescaler * frequency)) - 1
- The -1 accounts for the count starting at 0
- Verify the result fits in the timer’s bit width
- “What’s the difference between CTC mode and Fast PWM mode?”
- CTC: Counter clears on compare match, good for precise intervals
- Fast PWM: Counter counts to TOP (255/65535), compare controls output state
- CTC for timing/interrupts, PWM for analog output
- “Why use hardware timers instead of software delay loops?”
- Precision: Hardware timers are crystal-accurate
- Non-blocking: CPU can do other work while timer runs
- Deterministic: Timing unaffected by code complexity
- Power: Can run in low-power modes
- “How would you generate a 1 MHz signal on an AVR?”
- Timer1 with prescaler 1, CTC mode
- OCR1A = (16,000,000 / (1 * 1,000,000 * 2)) - 1 = 7
- Toggle output on compare match
- Actual: 16MHz / (2 * 8) = 1 MHz exactly
- “What determines PWM resolution?”
- Number of distinct duty cycle steps
- 8-bit timer: 256 steps (0.39% per step)
- Trade-off: Higher resolution requires lower frequency (longer period)
5.9 Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| AVR Timers | “Make: AVR Programming” - Elliot Williams | Chapters 6, 9, 10 |
| PWM Theory | “Making Embedded Systems” - Elecia White | Chapter 10 |
| Timer Details | ATmega328P Datasheet | Sections 15 (Timer0), 16 (Timer1) |
| Interrupt Handling | “AVR Workshop” - John Boxall | Chapter 7 |
| Motor Control | “Arduino Cookbook” - Michael Margolis | Chapter 8 |
| Audio Generation | “Arduino Cookbook” - Michael Margolis | Chapter 9 |
5.10 Implementation Phases
Phase 1: System Tick (4 hours)
- Configure Timer1 in CTC mode
- Calculate and set OCR1A for 1 Hz
- Write ISR to increment counter and toggle LED
- Verify with stopwatch (60 blinks = 60 seconds)
Phase 2: Uptime Counter (2 hours)
- Add millisecond counter variable
- Modify ISR or add sub-tick counting
- Implement get_uptime_ms() function
- Test accuracy over 1 minute
Phase 3: PWM Output (4 hours)
- Configure Timer0 for Fast PWM
- Connect LED to pin 6 with resistor
- Implement pwm_set_duty() function
- Test multiple duty cycles with multimeter
Phase 4: Fade Animation (3 hours)
- Add fade state machine (up/down direction)
- Update PWM periodically from main loop
- Use system tick for timing
- Make fade speed adjustable
Phase 5: Tone Generator (4 hours)
- Configure Timer2 in CTC mode with toggle
- Implement freq_to_ocr() calculation
- Add tone_play() and tone_stop()
- Test with tuner app
Phase 6: Integration (3 hours)
- Add UART command parsing
- Integrate all features
- Test simultaneous operation
- Document and clean up code
5.11 Key Implementation Decisions
| Decision | Options | Recommended | Rationale |
|---|---|---|---|
| System tick timer | Timer0/1/2 | Timer1 | 16-bit allows exact 1 Hz |
| PWM timer | Timer0/2 | Timer0 | OC0A on convenient pin |
| Tone timer | Timer0/2 | Timer2 | Keeps Timer0 free for PWM |
| PWM prescaler | 8/64/256 | 64 | ~1 kHz, no flicker |
| Tick prescaler | 256/1024 | 1024 | Simple math (15625) |
| Tone prescaler | 64/128/256 | 256 | Good range for music |
6. Testing Strategy
6.1 Unit Testing Timer Calculations
Before flashing, verify your calculations:
// On PC (not AVR), compile with gcc to test calculations
#include <stdio.h>
#include <stdint.h>
#define F_CPU 16000000UL
uint16_t calc_ocr_for_freq(uint32_t freq, uint16_t prescaler) {
return (F_CPU / (prescaler * freq)) - 1;
}
float calc_actual_freq(uint16_t ocr, uint16_t prescaler) {
return (float)F_CPU / (prescaler * (ocr + 1));
}
int main() {
// Test 1 Hz with prescaler 1024
uint16_t ocr = calc_ocr_for_freq(1, 1024);
float actual = calc_actual_freq(ocr, 1024);
printf("1 Hz: OCR=%u, Actual=%.6f Hz\n", ocr, actual);
// Test 440 Hz with prescaler 256 (for tone)
uint8_t ocr8 = (F_CPU / (2UL * 256 * 440)) - 1;
float actual_tone = (float)F_CPU / (2 * 256 * (ocr8 + 1));
printf("440 Hz: OCR=%u, Actual=%.2f Hz\n", ocr8, actual_tone);
return 0;
}
6.2 Hardware Verification Tests
| Test | Method | Expected Result | Pass Criteria |
|---|---|---|---|
| 1 Hz accuracy | Count blinks vs stopwatch | 60 blinks in 60.0s | +/- 0.5 blinks |
| PWM 0% | Set OCR0A=0 | LED very dim/off | Nearly no light |
| PWM 50% | Set OCR0A=127 | Medium brightness | ~2.5V average |
| PWM 100% | Set OCR0A=255 | Maximum brightness | ~5V DC |
| Tone A4 | Tuner app | Shows A4 | Within 5 cents |
| Multi-timer | All running | No interference | All work simultaneously |
6.3 Oscilloscope/Logic Analyzer Verification
Expected PWM waveform at 50% duty:
┌────────┐ ┌────────┐ ┌────────┐
5V ──┤ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
0V ──┴────────┴────────┴────────┴────────┴────────┴────────
│◄─────►│◄─────►│
~0.5ms ~0.5ms
│◄── Period ──►│
~1.024ms
Expected tone waveform at 440 Hz:
┌──────────┐ ┌──────────┐
5V ──┤ │ │ │
│ │ │ │
│ │ │ │
0V ──┴──────────┴──────────┴──────────┴──────────
│◄───────►│◄───────►│
1.14ms 1.14ms
│◄──── Period ────►│
2.27ms = 440 Hz
7. Common Pitfalls & Debugging
7.1 Timer Not Running
| Symptom | Cause | Fix |
|---|---|---|
| No interrupt | Global interrupts disabled | Add sei() after timer setup |
| No interrupt | Interrupt not enabled | Set OCIE1A in TIMSK1 |
| Timer stuck at 0 | No prescaler set | Set CS bits in TCCRnB |
| Wrong frequency | Calculation error | Re-check formula, +1/-1 |
7.2 PWM Problems
| Symptom | Cause | Fix |
|---|---|---|
| LED always off | Wrong pin | OC0A is PD6 (Arduino pin 6) |
| LED always on | COM bits wrong | Use COM0A1=1, COM0A0=0 |
| LED flickers | PWM freq too low | Use prescaler 64 or lower |
| Only on/off, no dim | Not in PWM mode | Check WGM bits |
| Dim at OCR=0 | Fast PWM quirk | Normal; use OCR=0 carefully |
7.3 Tone Problems
| Symptom | Cause | Fix |
|---|---|---|
| No sound | Wrong pin | OC2A is PB3 (Arduino pin 11) |
| Wrong frequency | Calculation error | Verify OCR2A formula |
| Clicks between notes | Glitch on change | Stop timer before changing OCR |
| Tone doesn’t stop | Register not cleared | Set TCCR2A=0 AND TCCR2B=0 |
7.4 Debugging Code
// Add to your code for debugging
void debug_timers(void) {
uart_printf("\n=== Timer Debug ===\n");
uart_printf("Timer1:\n");
uart_printf(" TCCR1A: 0x%02X\n", TCCR1A);
uart_printf(" TCCR1B: 0x%02X\n", TCCR1B);
uart_printf(" OCR1A: %u\n", OCR1A);
uart_printf(" TCNT1: %u\n", TCNT1);
uart_printf(" TIMSK1: 0x%02X\n", TIMSK1);
uart_printf("Timer0 (PWM):\n");
uart_printf(" TCCR0A: 0x%02X (expect 0x83)\n", TCCR0A);
uart_printf(" TCCR0B: 0x%02X (expect 0x03)\n", TCCR0B);
uart_printf(" OCR0A: %u\n", OCR0A);
uart_printf("Timer2 (Tone):\n");
uart_printf(" TCCR2A: 0x%02X\n", TCCR2A);
uart_printf(" TCCR2B: 0x%02X\n", TCCR2B);
uart_printf(" OCR2A: %u\n", OCR2A);
}
8. Extensions & Challenges
8.1 Easy Extensions
- Multiple LED fade patterns: Breathe, pulse, strobe, heartbeat
- RGB LED color mixing: Three PWM channels for full color control
- Button-controlled brightness: Increment/decrement with buttons
- Alarm clock: Set time via UART, beep at alarm time
8.2 Intermediate Challenges
- Servo motor control: Generate 1-2ms pulses at 50 Hz using Timer1
- Music player: Parse simple notation (C4, D4, etc.) and play melodies
- Frequency counter: Measure input signal frequency using input capture
- Software PWM: Use Timer1 interrupt to PWM multiple pins
8.3 Advanced Challenges
- Sine wave DAC: Use PWM + low-pass filter to generate analog sine waves
- DDS (Direct Digital Synthesis): Generate arbitrary waveforms
- Motor PID control: Use timer for control loop, PWM for motor output
- Real-time scheduler: Use system tick to implement cooperative multitasking
9. Real-World Connections
9.1 Industry Applications
| Application | Timer Use | Why It Matters |
|---|---|---|
| Automotive ECU | PWM for fuel injectors | Precise fuel metering |
| Motor controllers | PWM at 20-50 kHz | Efficient BLDC control |
| LED lighting | PWM dimming | Energy-efficient brightness |
| Audio equipment | Timer for sample rate | CD-quality: 44.1 kHz exactly |
| Industrial PLCs | System tick | Deterministic control loops |
| Servo controllers | 50 Hz PWM | Hobby and industrial servos |
9.2 How This Connects to RTOS
Your Timer Tick: RTOS SysTick:
┌─────────────────┐ ┌─────────────────────────────────┐
│ Timer1 interrupt│ │ SysTick_Handler() │
│ every 1 second │ │ { │
│ │ │ os_tick++; │
│ ISR: uptime++ │ │ scheduler_update(); │
│ tick_flag │ │ if (preempt_needed) │
│ │ │ context_switch(); │
│ │ │ } │
└─────────────────┘ └─────────────────────────────────┘
Your foundation: Enables:
- Periodic code execution - Task scheduling
- Time measurement - Timeouts and delays
- Non-blocking design - Preemptive multitasking
9.3 Professional Code Comparison
// Your bare metal PWM (~50 bytes)
DDRD |= (1 << PD6);
TCCR0A = (1 << COM0A1) | (1 << WGM01) | (1 << WGM00);
TCCR0B = (1 << CS01) | (1 << CS00);
OCR0A = brightness;
// Arduino analogWrite (~200 bytes overhead)
analogWrite(6, brightness);
// Calls into library that:
// - Looks up pin mapping
// - Determines which timer
// - Calls timer-specific function
// - Adds abstraction overhead
// STM32 HAL PWM (~500+ bytes)
TIM_HandleTypeDef htim;
HAL_TIM_PWM_Init(&htim);
HAL_TIM_PWM_ConfigChannel(&htim, &sConfig, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE(&htim, TIM_CHANNEL_1, brightness);
Your code is:
- Smaller
- Faster
- More predictable
- Fully understood
10. Resources
10.1 Essential References
| Resource | Purpose | Location |
|---|---|---|
| ATmega328P Datasheet Section 15 | Timer0 details | Microchip website |
| ATmega328P Datasheet Section 16 | Timer1 details | Microchip website |
| ATmega328P Datasheet Section 18 | Timer2 details | Microchip website |
| AVR Libc Reference | Interrupt macros | nongnu.org/avr-libc |
10.2 Online Resources
| Resource | URL | Purpose |
|---|---|---|
| AVR Timer Tutorial | maxembedded.com/avr-timers | Step-by-step guide |
| PWM Guide | protostack.com.au/pwm | PWM theory |
| Music Note Frequencies | pages.mtu.edu/~suits/notefreqs.html | Note to Hz table |
| AVR Freaks Forum | avrfreaks.net | Community help |
10.3 Musical Note Reference
Octave 4 (Middle C = C4):
Note Frequency (Hz) OCR2A (prescaler 256)
─────────────────────────────────────────────────
C4 261.63 119
C#4 277.18 112
D4 293.66 106
D#4 311.13 100
E4 329.63 94
F4 349.23 89
F#4 369.99 84
G4 392.00 79
G#4 415.30 74
A4 440.00 70
A#4 466.16 66
B4 493.88 62
Octave 5:
C5 523.25 59
D5 587.33 52
E5 659.26 47
G5 783.99 39
A5 880.00 34
11. Self-Assessment Checklist
Knowledge Verification
- I can explain what a timer/counter register does
- I understand the difference between Normal, CTC, and PWM modes
- I can calculate OCR values for any frequency given clock and prescaler
- I know how prescaler selection affects timing range and resolution
- I understand why PWM creates analog-like voltage from digital output
- I can explain timer interrupt flow (flag, enable, ISR, return)
Skill Verification
- I can configure Timer1 for CTC mode from scratch
- I can set up Timer0 for Fast PWM and control duty cycle
- I can generate specific audio frequencies using Timer2
- I can debug timer configuration by reading register values
- I can calculate and verify timing accuracy
Confidence Check
- I could add precise timing to any embedded project
- I could implement motor control using PWM
- I could troubleshoot timer-related bugs systematically
- I could explain timer concepts in a technical interview
- I understand how RTOS schedulers use timer interrupts
12. Completion Criteria
Required (Must complete all)
- 1-second LED blink using Timer1 interrupt
- Not software delay
- Exactly 60 blinks per minute (+/- 0.5)
- PWM LED control via UART
- “pwm 0” turns LED off
- “pwm 255” turns LED to maximum
- “pwm 128” sets 50% brightness (verifiable with multimeter)
- Uptime counter
- Displays elapsed seconds
- Accurate within 0.1% over 1 minute
- Smooth LED fade animation
- Visible breathing effect
- No visible stepping or jumping
Bonus (Complete for deeper understanding)
- Audio tone generation (verify 440 Hz with tuner)
- Multiple tone frequencies / simple melody
- Timer status display showing register values
- Servo control demonstration (if hardware available)
Evidence of Completion
- Video demonstrating LED fade and precise timing
- Screenshot of UART output showing uptime
- avr-size output showing code size (< 4KB)
- (Optional) Oscilloscope capture of PWM waveform
Navigation
Previous Project: P02 - UART Serial Communication
Next Project: P04 - Raspberry Pi Bare Metal
You now have precise timing, analog-like output, and audio capability - all without wasting CPU cycles! Hardware timers are fundamental to every embedded system, from simple toys to spacecraft. Next, we move to the ARM architecture with Raspberry Pi bare metal programming.