Generating PDF…

Preparing…
← Back

PWM & Tone Generation

DE6417 Microcontrollers 2

ATmega328P Timer Configuration and Programming

Arduino Uno

📚 Learning Objectives

Part 1 — Timers & CTC (Review)

  • Understand how timers work in the ATmega328P
  • Configure Timer Control Registers (TCCR1A, TCCR1B)
  • Calculate prescaler and compare match values
  • Implement timer interrupts (CTC mode)
  • Use Clear Timer on Compare Match (CTC) mode

Part 2 — PWM (New)

  • Understand Pulse Width Modulation concepts
  • Configure Fast PWM Mode 14 (ICR1 as TOP)
  • Control frequency and duty cycle via registers
  • Generate audio tones with a piezo buzzer
  • Build an interactive 3-note piano 🎹

⏱️ Timer Fundamentals

  • ATmega328P has 3 timers: Timer0, Timer1, Timer2
  • Timer0 & Timer2: 8-bit (count 0-255)
  • Timer1: 16-bit (count 0-65,535)
  • Each timer can generate interrupts
  • Each timer has dedicated output pins
💡 Naming convention: The number in register names (0, 1, 2) indicates the timer number

ATmega328P Timers

Timer0 (8-bit)
Timer1 (16-bit) ⭐
Timer2 (8-bit)

🔤 Understanding Register Naming Convention

Example: COM1A1 and COM1A0

Part Meaning Example
COM Compare Output Mode Function type
1 Timer Number Timer 1
A Channel Channel A (or B)
1 or 0 Bit Number Bit 1 or Bit 0
⚠️ Don't confuse the timer number with the bit number!

📋 TCCR1A - Timer Control Register A

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10

Compare Output Mode (COM1A1:0)

  • 00: Normal operation, OC1A disconnected
  • 01: Toggle OC1A on compare match
  • 10: Clear OC1A on compare match
  • 11: Set OC1A on compare match
// For normal operation with
// output pins disconnected:
// Set all bits to 0

TCCR1A = 0b00000000;

// Or equivalently:
TCCR1A = 0;

🌊 Waveform Generation Modes

Timer1 has 16 different modes (0-15) controlled by WGM13:10 bits

Mode WGM13 WGM12 WGM11 WGM10 Description TOP Value
0 0 0 0 0 Normal 0xFFFF
4 0 1 0 0 CTC (Clear Timer on Compare) OCR1A
5 0 1 0 1 Fast PWM, 8-bit 0x00FF
14 1 1 1 0 Fast PWM ICR1
✅ For timer interrupts, we'll use Mode 4 (CTC) - Counter resets when it matches OCR1A

📋 TCCR1B - Timer Control Register B

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10

Key Bits:

  • WGM13:12: Upper waveform generation mode bits
  • CS12:10: Clock Select (Prescaler)
// For CTC mode with 256 prescaler:
// WGM12 = 1 (bit 3)
// CS12 = 1 (bit 2)

TCCR1B = 0b00001100;

// Binary breakdown:
// Bit 3 (WGM12) = 1 → CTC mode
// Bit 2 (CS12)  = 1 → 256 prescaler

⚙️ Clock Select (Prescaler) Options

CS12 CS11 CS10 Description Division Factor
0 0 0 No clock (Timer stopped) -
0 0 1 clk/1 (No prescaling) 1
0 1 0 clk/8 8
0 1 1 clk/64 64
1 0 0 clk/256 256
1 0 1 clk/1024 1024
⚠️ CS12:10 = 000 stops the timer! Never use this unless intentional.

🤔 Why Do We Need a Prescaler?

  • Arduino clock: 16 MHz
  • Timer1: 16-bit (max count: 65,535)
  • For a 1 second delay at 16 MHz, we'd need to count to 16,000,000
  • That's way more than 65,535! ❌
Prescaler divides the clock frequency to allow longer delays
💡 With prescaler of 256: 16,000,000 ÷ 256 = 62,500 ✅
System Clock
16 MHz
Prescaler
÷256
Timer Clock
62.5 kHz

🔢 Scientific Notation Review

Essential for timing calculations with very large or small numbers:

Number Scientific Notation Rule
16,000,000 1.6 × 107 Move decimal 7 places left
125,000 1.25 × 105 Move decimal 5 places left
0.000000001 1 × 10-9 Move decimal 9 places right

Common Prefixes:

  • MHz: 106
  • kHz: 103
  • ms: 10-3
  • μs: 10-6
  • ns: 10-9

📐 Clock Period Calculation

Period = 1 / Frequency

Example with 16 MHz clock:

T = 1 / f

T = 1 / 16,000,000 Hz

T = 1 / (16 × 106) s

T = 62.5 × 10-9 s = 62.5 ns

💡 Each clock cycle takes 62.5 nanoseconds

Calculator Steps:

1 ÷ 16E6 =

6.25E-8

= 62.5 ns

🧮 Calculating Compare Match Value

Goal: Generate an interrupt every 500 ms (0.5 seconds)

Number of Clocks = Desired Time / Clock Period

Number of Clocks = 500 ms / 62.5 ns

= (500 × 10-3) / (62.5 × 10-9)

= 8,000,000 clocks

⚠️ 8 million won't fit in 16 bits (max 65,535)! We need a prescaler!

🔍 Finding the Right Prescaler

We need 8,000,000 clocks. Let's try different prescalers:

Prescaler Calculation Result Fits in 16-bit?
1 8,000,000 ÷ 1 8,000,000 ❌ Too large
64 8,000,000 ÷ 64 125,000 ❌ Too large
256 8,000,000 ÷ 256 31,250 ✅ Perfect!
1024 8,000,000 ÷ 1024 7,812.5 ⚠️ Not integer
Prescaler 256 gives us 31,250 - a whole number that fits in 16 bits!

🖩 Interactive Timer Calculator

Calculate Compare Match Value:

Compare Match = (Clock Speed × Delay) / (Prescaler × 1000) - 1

💻 Complete Timer Setup Code

  1. Disable global interrupts
  2. Configure TCCR1A (all zeros)
  3. Configure TCCR1B (CTC + prescaler)
  4. Set compare match value in OCR1A
  5. Reset counter to zero
  6. Enable compare match interrupt
  7. Enable global interrupts
void setup() {
  pinMode(8, OUTPUT);
  Serial.begin(9600);
  
  // Step 1: Disable interrupts
  cli();
  
  // Step 2: Configure TCCR1A
  TCCR1A = 0b00000000;
  
  // Step 3: Configure TCCR1B
  // WGM12=1 (CTC), CS12=1 (256)
  TCCR1B = 0b00001100;
  
  // Step 4: Set compare match
  OCR1A = 31250;
  
  // Step 5: Reset counter
  TCNT1 = 0;
  
  // Step 6: Enable interrupt
  TIMSK1 |= (1 << OCIE1A);
  
  // Step 7: Enable interrupts
  sei();
}

📊 OCR1A - Output Compare Register

  • 16-bit register (can hold 0-65,535)
  • Stores the value the timer compares against
  • When TCNT1 matches OCR1A → interrupt triggers
  • In CTC mode, counter resets to 0 after match
When TCNT1 == OCR1A → Interrupt fires!
💡 For 500ms delay with prescaler 256:
OCR1A = 31,250
TCNT1 (Counter) ↑ counts up OCR1A = 31,250 Match = Interrupt!

🎭 TIMSK1 - Interrupt Mask Register

Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
- - ICIE1 - - OCIE1B OCIE1A TOIE1
  • OCIE1A: Output Compare A Match Interrupt Enable
  • OCIE1B: Output Compare B Match Interrupt Enable
  • TOIE1: Timer Overflow Interrupt Enable
  • ICIE1: Input Capture Interrupt Enable
// Enable Output Compare A interrupt
// OCIE1A is bit 1

// Method 1: Using bit shift
TIMSK1 |= (1 << OCIE1A);

// Method 2: Binary literal
TIMSK1 |= 0b00000010;

// Method 3: OR to preserve others
TIMSK1 = TIMSK1 | 0b00000010;

🔧 Bit Manipulation Best Practices

  • Use OR (|=) to set bits without destroying others
  • Use AND (&=) with inverted mask to clear bits
  • Don't overwrite entire registers unless necessary
⚠️ Overwriting registers can cause side effects if other code has set bits you need!
// ✅ GOOD: Set only bit 1
TIMSK1 |= (1 << OCIE1A);

// ✅ GOOD: Clear only bit 1
TIMSK1 &= ~(1 << OCIE1A);

// ❌ BAD: Overwrites everything
TIMSK1 = 0b00000010;
// This destroys any other bits!

// If you need to set bit 3:
// Shift 1 left by 3 positions
// Binary: 00001000
uint8_t mask = (1 << 3);

⚡ Interrupt Service Routine (ISR)

  • ISR = function called when interrupt fires
  • Must use exact vector name from datasheet
  • Keep ISRs short and fast!
  • Avoid delay(), Serial.print() inside ISR
  • Use volatile for shared variables
💡 The ISR flag is automatically cleared after the interrupt handler executes
// Volatile for ISR-modified variables
volatile int ledState = LOW;
volatile unsigned int intCount = 0;

// Timer1 Compare Match A ISR
ISR(TIMER1_COMPA_vect) {
  // Toggle LED state
  ledState = !ledState;
  
  // Write to LED pin
  digitalWrite(8, ledState);
  
  // Count interrupts
  intCount++;
}

// Vector names from avr/iom328p.h:
// TIMER1_COMPA_vect
// TIMER1_COMPB_vect
// TIMER1_OVF_vect

⏰ Understanding Toggle Timing

Common Bug: Confusing interrupt rate with toggle rate!

500ms 500ms 1 complete cycle = 1 second
⚠️ Toggle divides frequency by 2!
500ms interrupt → 1Hz blink rate (not 2Hz!)

🔄 CTC Mode - Automatic Counter Reset

  • CTC = Clear Timer on Compare
  • Counter automatically resets to 0 when it matches OCR1A
  • No manual reset needed in ISR!
  • Set WGM12 bit in TCCR1B to enable

Without CTC (Normal mode):

Must manually reset: TCNT1 = 0;

With CTC mode:

Counter resets automatically! ✅

// Enable CTC mode
// Set WGM12 (bit 3) in TCCR1B

// Without CTC - need manual reset:
TCCR1B = 0b00000100; // Just prescaler
ISR(TIMER1_COMPA_vect) {
  TCNT1 = 0; // Manual reset needed!
  // ... rest of ISR
}

// With CTC - automatic reset:
TCCR1B = 0b00001100; // CTC + prescaler
ISR(TIMER1_COMPA_vect) {
  // No reset needed!
  // Counter resets automatically
}

📝 Complete Working Example

Blink LED every 500ms using Timer1 interrupt:

Hardware Setup:
• LED on Pin 8
• Resistor to GND
• Arduino Uno
Key Values:
• Prescaler: 256
• OCR1A: 31,250
• Interrupt: every 500ms
#define LED_PIN 8
volatile int ledState = LOW;
volatile unsigned int intCount = 0;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);
  
  cli(); // Disable interrupts
  
  TCCR1A = 0; // Normal port operation
  TCCR1B = 0; // Clear register
  
  // Set CTC mode + 256 prescaler
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS12);
  
  OCR1A = 31250; // Compare value
  TCNT1 = 0;     // Reset counter
  
  TIMSK1 |= (1 << OCIE1A); // Enable int
  
  sei(); // Enable interrupts
}

void loop() {
  Serial.print("Interrupts: ");
  Serial.println(intCount);
  delay(1000);
}

ISR(TIMER1_COMPA_vect) {
  ledState = !ledState;
  digitalWrite(LED_PIN, ledState);
  intCount++;
}

🏎️ Changing Timer Speed

  • To make timer faster: decrease OCR1A value
  • To make timer slower: increase OCR1A value (up to 65,535)
  • Or change the prescaler for bigger changes
OCR1A Value Interrupt Rate LED Blink Rate
31,250 Every 500ms 1 Hz
3,125 Every 50ms 10 Hz
312 Every 5ms 100 Hz
// Original: 500ms interrupt
OCR1A = 31250;

// 10x faster: 50ms interrupt
OCR1A = 31250 / 10;  // = 3125

// 100x faster: 5ms interrupt
OCR1A = 31250 / 100; // = 312

// Formula:
// OCR1A = (F_CPU / Prescaler) 
//         * (delay_ms / 1000) - 1
// For 500ms:
// = (16000000 / 256) * 0.5 - 1
// = 62500 * 0.5 - 1 = 31249 ≈ 31250

📊 Timer1 Block Diagram

16 MHz
System Clock
Prescaler
(1, 8, 64, 256, 1024)
TCNT1
16-bit Counter
Comparator
TCNT1 == OCR1A?
Interrupt!
TIMER1_COMPA_vect

📋 Summary: Timer Setup Steps

  1. cli(); - Disable global interrupts
  2. TCCR1A = 0; - Set normal port operation
  3. TCCR1B = 0; - Clear control register B
  4. Set WGM12 - Enable CTC mode (auto-reset)
  5. Set CS12:10 - Choose prescaler (calculate first!)
  6. OCR1A = value; - Set compare match value
  7. TCNT1 = 0; - Reset counter
  8. Set OCIE1A - Enable compare match interrupt
  9. sei(); - Enable global interrupts

📐 Essential Formulas

Period = 1 / Frequency
Clock Cycles = Desired Time / Clock Period
OCR1A = (Clock Cycles / Prescaler) - 1
OCR1A = (F_CPU × Delay_sec / Prescaler) - 1
Quick Reference for 16 MHz Arduino:
• Clock Period = 62.5 ns
• For 1 second with prescaler 256: OCR1A = 62,499
• For 500ms with prescaler 256: OCR1A = 31,249

📚 Register Quick Reference

Register Purpose Key Bits
TCCR1A Timer Control Register A COM1A1:0, COM1B1:0, WGM11:10
TCCR1B Timer Control Register B WGM13:12, CS12:10 (prescaler)
TCNT1 Timer Counter (16-bit) Current count value
OCR1A Output Compare Register A Compare match value
TIMSK1 Timer Interrupt Mask OCIE1A, OCIE1B, TOIE1
TIFR1 Timer Interrupt Flag OCF1A, OCF1B, TOV1

⚠️ Common Mistakes to Avoid

  • Forgetting cli()/sei() - Setup without disabling interrupts
  • Wrong prescaler - Value doesn't fit in 16 bits
  • Non-integer OCR1A - Prescaler causes fractional result
  • Long ISR - Blocking code in interrupt handler
  • Missing volatile - Variables modified in ISR
  • Toggle confusion - Forgetting toggle divides frequency by 2
  • Overwriting registers - Destroying other bits
// ❌ WRONG: Non-volatile variable
int counter = 0;

// ✅ CORRECT: Volatile for ISR
volatile int counter = 0;

// ❌ WRONG: Long ISR
ISR(TIMER1_COMPA_vect) {
  delay(100);        // Never!
  Serial.println();  // Avoid!
}

// ✅ CORRECT: Short ISR
ISR(TIMER1_COMPA_vect) {
  ledState = !ledState;
  PORTB ^= (1 << PB0); // Fast!
}

📦 Alternative: Timer Libraries

Don't want to manipulate registers directly? Use libraries!

  • TimerOne - Popular library for Timer1
  • MsTimer2 - Timer2 library
  • FlexiTimer2 - Another Timer2 option
💡 Libraries abstract the complexity but understanding registers gives you more control and debugging ability
// Using TimerOne library
#include <TimerOne.h>

void setup() {
  pinMode(8, OUTPUT);
  
  // Initialize timer with 500ms period
  Timer1.initialize(500000); // microseconds
  
  // Attach interrupt function
  Timer1.attachInterrupt(blinkLED);
}

void loop() {
  // Empty - all done by timer!
}

void blinkLED() {
  digitalWrite(8, !digitalRead(8));
}

🛠️ Practical Applications

🔊
Tone Generation
Audio frequencies
💡
PWM Control
LED dimming, motors
⏱️
Precise Timing
Scheduling tasks
📡
Communication
UART, SPI timing
📊
Data Sampling
ADC at fixed rate
🤖
Servo Control
Precise positioning

🔍 Debugging Tips

  • Use oscilloscope to verify timing
  • Toggle a pin at start/end of ISR to measure duration
  • Check interrupt counter to verify ISR is being called
  • Verify register values by reading them back
  • Start simple - get basic blink working first
💡 Use Tinkercad or other simulators for initial testing before hardware!
// Debug: Print register values
void printRegisters() {
  Serial.print("TCCR1A: ");
  Serial.println(TCCR1A, BIN);
  
  Serial.print("TCCR1B: ");
  Serial.println(TCCR1B, BIN);
  
  Serial.print("OCR1A: ");
  Serial.println(OCR1A);
  
  Serial.print("TIMSK1: ");
  Serial.println(TIMSK1, BIN);
}

// Debug: Verify ISR timing
ISR(TIMER1_COMPA_vect) {
  PORTB |= (1 << PB1);  // Pin high
  // ... your code ...
  PORTB &= ~(1 << PB1); // Pin low
}

Part 2

PWM — Pulse Width Modulation

From precise timing to analog-like output control

⚡🔊💡

What is PWM?

Pulse Width Modulation rapidly switches a digital pin ON and OFF to simulate an analog voltage.

  • Duty Cycle — percentage of time the signal is HIGH
  • Higher duty cycle → higher average voltage
  • The frequency stays constant; only the pulse width changes
Vavg = VCC × Duty Cycle → 50 % on 5 V = 2.5 V average
25 % Duty
50 % Duty
75 % Duty

🛠️ PWM Applications

💡
LED Dimming
Vary brightness smoothly
⚙️
Motor Speed
DC motor control
🔊
Tone / Audio
Frequency = pitch
🤖
Servo Motors
1–2 ms pulse @ 50 Hz
🔋
Power Regulation
Efficient voltage control
📻
DAC Approximation
PWM + RC filter

Timer1 PWM Modes

Timer1 supports several PWM modes via the WGM13:10 bits:

ModeWGM13:10NameTOPBest For
50101Fast PWM, 8-bit0x00FFQuick & simple
60110Fast PWM, 9-bit0x01FFMore resolution
70111Fast PWM, 10-bit0x03FFSmooth fading
141110Fast PWMICR1Custom frequency ⭐
151111Fast PWMOCR1ACustom freq (1 channel)
81000Phase CorrectICR1Motor control
101010Phase CorrectOCR1ASmooth motor
💡 We will use Mode 14 — Fast PWM with ICR1 as TOP. This lets us set any PWM frequency while still using OCR1A for duty cycle control.

Fast PWM Mode 14

How the counter behaves

  1. Counter (TCNT1) counts 0 → TOP (value in ICR1)
  2. When TCNT1 == OCR1A: output pin cleared (goes LOW)
  3. When TCNT1 reaches TOP: counter resets to 0 and pin is set (goes HIGH)
⚠️ Output appears on pin 9 (OC1A) or pin 10 (OC1B). You must set the pin as OUTPUT.
Counter (TCNT1) & OC1A Output TOP 0 OCR1A ICR1 OC1A (pin 9) HIGH LOW Duty Cycle OCR1A match (clear) TOP reached (set + reset)

Register Setup — Mode 14

WGM Bit Mapping

BitRegisterValue
WGM13TCCR1B bit 41
WGM12TCCR1B bit 31
WGM11TCCR1A bit 11
WGM10TCCR1A bit 00

Plus COM1A1:0 = 10 for non-inverting output on OC1A (pin 9).

// Fast PWM Mode 14 — ICR1 as TOP
// Non-inverting output on OC1A (pin 9)

TCCR1A = (1 << COM1A1)   // Non-inverting
       | (1 << WGM11);   // WGM11 = 1

TCCR1B = (1 << WGM13)   // WGM13 = 1
       | (1 << WGM12)   // WGM12 = 1
       | (1 << CS11);   // Prescaler = 8

// WGM13:12:11:10 = 1 1 1 0 → Mode 14 ✓
// CS12:11:10     = 0 1 0   → clk/8   ✓

// Set frequency via ICR1 (TOP)
ICR1 = 39999;  // example

// Set duty cycle via OCR1A
OCR1A = 19999; // 50% duty

COM1A Bits — Output Behaviour

In Fast PWM mode, the COM1A1:0 bits control the OC1A pin:

COM1A1COM1A0OC1A Behaviour
00Normal port operation (OC1A disconnected)
01Toggle on Compare Match (Mode 15 only)
10Non-inverting: clear on match, set at BOTTOM
11Inverting: set on match, clear at BOTTOM
💡 Non-inverting (10) is the standard choice. Duty cycle goes from 0 % (OCR1A = 0) to 100 % (OCR1A = ICR1).

📐 PWM Frequency Formula

fPWM = fclk / ( Prescaler × (1 + TOP) )

Rearranging to find TOP (the value we put in ICR1):

TOP = ( fclk / ( Prescaler × fdesired ) ) − 1

Example: 50 Hz servo signal, prescaler 8

TOP = (16 000 000 / (8 × 50)) − 1 = 39 999

ICR1 = 39999;

💡 Choose a prescaler that keeps TOP within 16 bits (0–65 535) and gives good resolution.

Duty Cycle Control

Duty % = (OCR1A + 1) / (TOP + 1) × 100 %

Quick reference (TOP = ICR1)

DesiredOCR1A value
0 % (off)0
25 %ICR1 / 4
50 %ICR1 / 2
75 %ICR1 * 3 / 4
~100 %ICR1
// Example: 50% duty cycle
ICR1  = 39999;         // TOP → sets frequency
OCR1A = ICR1 / 2;     // 50% duty

// Fade an LED from 0% to 100%
for (uint16_t i = 0; i <= ICR1; i += 400) {
  OCR1A = i;
  delay(20);
}

🎛️ Interactive: PWM Duty Cycle

Drag the OCR1A slider to see how it changes the duty cycle and output waveform:



Duty Cycle
50.0%
Vavg (5 V)
2.50 V
Frequency
261 Hz
ICR1 = 7644;  OCR1A = 3822;  // 50.0% duty → 2.50 V avg

💡 Code: LED Fade with PWM

  1. Set pin 9 (OC1A) as OUTPUT
  2. Configure TCCR1A for non-inverting + WGM11
  3. Configure TCCR1B for WGM13, WGM12, prescaler 8
  4. Set ICR1 (TOP) for desired frequency
  5. Vary OCR1A in loop to change brightness
💡 Compare this to analogWrite(9, value) — same result but we control the exact frequency!
#define LED_PIN 9  // OC1A

void setup() {
  pinMode(LED_PIN, OUTPUT);

  // Fast PWM Mode 14, non-inverting, prescaler 8
  TCCR1A = (1 << COM1A1) | (1 << WGM11);
  TCCR1B = (1 << WGM13)  | (1 << WGM12)
         | (1 << CS11);

  ICR1  = 39999;  // ~50 Hz
  OCR1A = 0;      // Start at 0% duty
}

void loop() {
  // Fade up
  for (uint16_t i = 0; i <= ICR1; i += 400) {
    OCR1A = i;
    delay(20);
  }
  // Fade down
  for (int32_t i = ICR1; i >= 0; i -= 400) {
    OCR1A = (uint16_t)i;
    delay(20);
  }
}

🔊 PWM for Audio

Generating tones with a piezo buzzer

  • Frequency of the PWM = pitch of the note
  • 50 % duty cycle → loudest square wave (symmetric)
  • Connect a passive piezo buzzer to pin 9 (OC1A) and GND
Arduino
Pin 9 (OC1A)
→ wire →
🔊 Piezo
Buzzer (+)
→ wire →
GND
Set ICR1 to control pitch | Set OCR1A = ICR1 / 2 for loudest tone

🎵 Musical Note Frequencies

Standard tuning — 4th octave (middle C upward):

NoteFrequency (Hz)ICR1 (prescaler 8)OCR1A (50 %)
C4261.6376443822
D4293.6668103405
E4329.6360673033
F4349.2357262863
G4392.0051022551
A4440.0045442272
B4493.8840492024
💡 C4, E4, G4 — the highlighted notes form a C major triad, one of the most common chords in music!

🧮 Calculating TOP for a Note

ICR1 = ( fclk / ( Prescaler × fnote ) ) − 1

Worked examples (fclk = 16 MHz, prescaler = 8)

NoteCalculationICR1
C4 (261.63 Hz) 16 000 000 / (8 × 261.63) − 1 7644
E4 (329.63 Hz) 16 000 000 / (8 × 329.63) − 1 6067
G4 (392.00 Hz) 16 000 000 / (8 × 392.00) − 1 5102

For 50 % duty cycle: OCR1A = ICR1 / 2

⚠️ The result is rounded to the nearest integer. Slight detuning is inaudible — less than 1 Hz error.

🎹 Helper Functions

playNote() — start a tone

  • Accepts the ICR1 (TOP) value for the desired note
  • Configures Timer1 in Fast PWM Mode 14
  • Sets 50 % duty cycle automatically

stopNote() — silence

  • Disconnects OC1A by clearing COM bits
  • Drives pin LOW to silence the buzzer
💡 These two functions are all you need for any melody!
#define BUZZER_PIN 9  // OC1A

void playNote(uint16_t top) {
  // Fast PWM Mode 14, non-inverting OC1A
  TCCR1A = (1 << COM1A1) | (1 << WGM11);
  TCCR1B = (1 << WGM13)  | (1 << WGM12)
         | (1 << CS11);   // prescaler 8

  ICR1   = top;           // Set frequency
  OCR1A  = top / 2;       // 50% duty cycle
}

void stopNote() {
  // Disconnect OC1A (normal port operation)
  TCCR1A = 0;
  TCCR1B = 0;
  digitalWrite(BUZZER_PIN, LOW);
}

🎶 Playing the C Major Triad

Using our helper functions, we can play C4 → E4 → G4:

NoteFrequencyICR1Duration
C4261.63 Hz7644500 ms
E4329.63 Hz6067500 ms
G4392.00 Hz5102500 ms
💡 The silence between notes (stopNote() + delay) prevents them from blurring together.
#define BUZZER_PIN 9
#define NOTE_C4 7644
#define NOTE_E4 6067
#define NOTE_G4 5102

void setup() {
  pinMode(BUZZER_PIN, OUTPUT);
}

void loop() {
  playNote(NOTE_C4);    // C4
  delay(500);
  stopNote();
  delay(100);           // short silence

  playNote(NOTE_E4);    // E4
  delay(500);
  stopNote();
  delay(100);

  playNote(NOTE_G4);    // G4
  delay(500);
  stopNote();
  delay(800);           // pause before repeat
}

// playNote() and stopNote() defined below
void playNote(uint16_t top) {
  TCCR1A = (1 << COM1A1) | (1 << WGM11);
  TCCR1B = (1 << WGM13)  | (1 << WGM12)
         | (1 << CS11);
  ICR1   = top;
  OCR1A  = top / 2;
}

void stopNote() {
  TCCR1A = 0;
  TCCR1B = 0;
  digitalWrite(BUZZER_PIN, LOW);
}

🎹 Practice Time!

Piano Notes with PWM

Build a 3-note piano using Timer1 Fast PWM and a piezo buzzer

⏱️ Estimated time: 1.5 – 2 hours

📋 3 tasks + 1 bonus challenge

🔌 Hardware Setup

Components needed

  • Arduino Uno
  • Passive piezo buzzer
  • 3 × push buttons
  • 3 × 10 kΩ pull-down resistors (or use INPUT_PULLUP)
  • Breadboard & jumper wires

Wiring

ComponentArduino PinNotes
Buzzer (+)Pin 9 (OC1A)Timer1 PWM output
Buzzer (−)GND
Button 1 (C4)Pin 2INPUT_PULLUP
Button 2 (E4)Pin 3INPUT_PULLUP
Button 3 (G4)Pin 4INPUT_PULLUP
Schematic:

Pin 9 ──── Buzzer (+)
       Buzzer (−) ── GND

Pin 2 ──┤ Button 1 ├── GND
Pin 3 ──┤ Button 2 ├── GND
Pin 4 ──┤ Button 3 ├── GND

(Using INPUT_PULLUP —
no external resistors needed)

📝 Task A: Play a Single Note (C4)

Objective

Configure Timer1 in Fast PWM Mode 14 to play C4 (261.63 Hz) on a piezo buzzer connected to pin 9.

Fill in the blanks in the code template →

💡 Hints:
• C4 frequency = 261.63 Hz
• Prescaler = 8, fclk = 16 MHz
• ICR1 = (16 000 000 / (8 × 261.63)) − 1 = ?
• For 50% duty: OCR1A = ICR1 / 2
#define BUZZER 9  // OC1A pin

void setup() {
  pinMode(BUZZER, OUTPUT);

  // Fast PWM Mode 14, non-inverting OC1A
  TCCR1A = (1 << _____) | (1 << _____);
  TCCR1B = (1 << _____) | (1 << _____)
         | (1 << _____);  // prescaler 8

  // Set TOP for C4 (261.63 Hz)
  ICR1  = _____;   // ← calculate this!

  // 50% duty cycle
  OCR1A = _____;   // ← half of ICR1
}

void loop() {
  // Nothing needed — hardware generates
  // the tone continuously!
}

📝 Task B: Play the C Major Triad

Objective

Write a function playNote(float frequency) that:

  1. Calculates ICR1 from the frequency
  2. Sets OCR1A to 50 % duty
  3. Configures Timer1 in Fast PWM Mode 14

Then play C4 → E4 → G4 in loop(), 500 ms each note, 100 ms silence between.

⚠️ Remember to write stopNote() to silence the buzzer between notes!

Target frequencies

NoteHz
C4261.63
E4329.63
G4392.00
#define BUZZER 9

void playNote(float freq) {
  uint16_t top = _____________ ;  // calculate!
  // Configure Timer1 Mode 14...
  // Set ICR1 and OCR1A...
}

void stopNote() {
  // Stop the timer and silence buzzer
}

void setup() {
  pinMode(BUZZER, OUTPUT);
}

void loop() {
  playNote(261.63);  // C4
  delay(500);
  stopNote();
  delay(100);

  playNote(329.63);  // E4
  delay(500);
  stopNote();
  delay(100);

  playNote(392.00);  // G4
  delay(500);
  stopNote();
  delay(800);        // longer pause
}

📝 Task C: Interactive 3-Note Piano

Objective

Connect 3 push buttons (pins 2, 3, 4) — each button plays a different note:

ButtonPinNote
Button 12C4 (261.63 Hz)
Button 23E4 (329.63 Hz)
Button 34G4 (392.00 Hz)

Behaviour:

  • Press & hold a button → note plays
  • Release → silence
  • If multiple buttons pressed → play the lowest-numbered button's note
#define BUZZER 9
#define BTN_C  2
#define BTN_E  3
#define BTN_G  4

void setup() {
  pinMode(BUZZER, OUTPUT);
  pinMode(BTN_C, INPUT_PULLUP);
  pinMode(BTN_E, INPUT_PULLUP);
  pinMode(BTN_G, INPUT_PULLUP);
}

void loop() {
  // INPUT_PULLUP → LOW when pressed
  if (digitalRead(BTN_C) == LOW) {
    playNote(261.63);
  }
  else if (digitalRead(BTN_E) == LOW) {
    playNote(329.63);
  }
  else if (digitalRead(BTN_G) == LOW) {
    playNote(392.00);
  }
  else {
    stopNote();
  }
}

// Reuse your playNote() & stopNote()
// from Task B!

⭐ Bonus Challenge

Play "Twinkle Twinkle Little Star"

Using your playNote() function, play the first phrase of the melody:

Beat1234567
NoteC4C4G4G4A4A4G4
Hz261.63261.63392.00392.00440.00440.00392.00
Duration400 ms400 ms400 ms400 ms400 ms400 ms800 ms

Hints:

  • A4 = 440 Hz → ICR1 = (16 000 000 / (8 × 440)) − 1 = 4544
  • Use arrays for notes and durations to keep the code clean
  • Add a short silence (50–100 ms) between notes for articulation
💡 Extension: Can you play the entire first verse? Add F4 (349.23 Hz), D4 (293.66 Hz), and E4 (329.63 Hz) to complete it!

❓ Quick Quiz

  1. How many bits is Timer1 on the ATmega328P?
    Answer: 16 bits (0–65,535)
  2. What does CTC stand for?
    Answer: Clear Timer on Compare
  3. If OCR1A = 31,250 with prescaler 256, what's the interrupt period?
    Answer: 500 milliseconds
  4. Why do we use volatile for ISR variables?
    Answer: Prevents compiler optimization that might cache the variable
  5. What WGM13:10 value selects Fast PWM with ICR1 as TOP?
    Answer: 1110 (Mode 14)
  6. Calculate ICR1 for A4 (440 Hz) with prescaler 8:
    Answer: (16 000 000 / (8 × 440)) − 1 = 4544
  7. What does COM1A1:0 = 10 do in Fast PWM mode?
    Answer: Non-inverting — clear OC1A on compare match, set at BOTTOM

Thank You! 🎉

Questions?

Key Takeaways:

  • Timers provide precise, hardware-based timing
  • Prescalers allow longer delays with limited counter bits
  • CTC mode auto-resets the counter on compare match
  • Keep ISRs short and use volatile for shared variables
  • Fast PWM Mode 14 uses ICR1 to set any frequency
  • OCR1A controls duty cycle (50 % = loudest tone)
  • Musical notes are just specific PWM frequencies!
Practice makes perfect! Try extending your piano with more notes and melodies. 🎶