Deep dive into hardware timing mechanisms on the ATmega328P
An interrupt is an external or internal event that:
A timer is essentially an extremely sophisticated counter implemented in hardware.
Remember: A microcontroller = microprocessor + peripherals
| Software Approach | Hardware Timer Approach |
|---|---|
| CPU busy counting/timing | CPU free for other tasks |
| Blocking delays | Non-blocking operation |
| Inaccurate timing | Precise, clock-accurate timing |
| Can't multitask | Parallel hardware operation |
| Timer | Bits | PWM Support | Special Features | Arduino Usage |
|---|---|---|---|---|
| Timer 0 | 8-bit | Yes | Basic timer | delay(), millis(), micros() |
| Timer 1 | 16-bit | Yes | Input capture, More resolution | Servo library, tone() |
| Timer 2 | 8-bit | Yes | Asynchronous operation | tone() on some pins |
Arduino API functions use specific timers. Be careful of conflicts!
| Function | Timer Used | Affected Pins |
|---|---|---|
| delay(), millis(), micros() | Timer 0 | Pin 5, 6 |
| analogWrite() on pins 9, 10 | Timer 1 | Pin 9, 10 |
| analogWrite() on pin 11 | Timer 2 | Pin 11 |
| tone() | Timer 1 or 2 | Varies |
| Servo library | Timer 1 | Pin 9, 10 |
Each timer can control specific pins only:
| Timer | Output A | Output B |
|---|---|---|
| Timer 0 | OC0A → Pin 6 (PD6) | OC0B → Pin 5 (PD5) |
| Timer 1 | OC1A → Pin 9 (PB1) | OC1B → Pin 10 (PB2) |
| Timer 2 | OC2A → Pin 11 (PB3) | OC2B → Pin 3 (PD3) |
Basic timer operation:
⟳ Timer counts up → reaches compare value → triggers interrupt → resets to 0
The prescaler divides the system clock to slow down counting:
ATmega328P prescaler options for Timer 1:
| CS12 | CS11 | CS10 | Prescaler | Timer Freq @ 16MHz |
|---|---|---|---|---|
| 0 | 0 | 0 | Timer OFF | - |
| 0 | 0 | 1 | 1 | 16 MHz |
| 0 | 1 | 0 | 8 | 2 MHz |
| 0 | 1 | 1 | 64 | 250 kHz |
| 1 | 0 | 0 | 256 | 62.5 kHz |
| 1 | 0 | 1 | 1024 | 15.625 kHz |
When the counter equals the compare value:
TCNT1 - Timer Counter (current count)OCR1A/OCR1B - Output Compare Register (compare value)| Term | Definition | Value (16-bit) |
|---|---|---|
| BOTTOM | Counter reaches its minimum value | 0x0000 |
| MAX | Counter reaches its maximum value | 0xFFFF (65,535) |
| TOP | Highest value in count sequence (user-defined) | OCR1A, ICR1, or fixed values |
Note: TOP ≠ MAX! TOP is configurable; MAX is the absolute maximum the register can hold.
| Mode | Description | Use Case |
|---|---|---|
| Normal | Counts from 0 to MAX, then overflows to 0 | Simple counting, timing measurements |
| CTC | Clear Timer on Compare match | Generating interrupts at precise intervals |
| Fast PWM | Single-slope PWM (count up only) | High-frequency PWM, LED dimming |
| Phase Correct PWM | Dual-slope PWM (count up and down) | Motor control, audio, avoiding phase errors |
The simplest mode of operation:
Counter clears when it matches OCR1A or ICR1:
Rearranging to find OCR1A:
PWM creates an analog-like signal using digital output:
Single-slope operation:
Dual-slope operation:
| Feature | Fast PWM | Phase Correct PWM |
|---|---|---|
| Count Direction | UP only (single slope) | UP then DOWN (dual slope) |
| Frequency | Higher (2× phase correct) | Lower |
| Resolution | Same | Same |
| Phase Error | Can have discontinuities | Symmetric, no phase error |
| Best For | LED dimming, power regulation | Motor control, audio, precision |
Key components:
| Register | Purpose | Details |
|---|---|---|
TCNT1 |
Current count value (16-bit) | Readable/writable. Holds the live counter value (0–65535). Write to it to preset the counter; read it to snapshot the current count. |
OCR1A |
Compare value, Channel A | When TCNT1 == OCR1A, a compare-match event fires. Used as TOP in CTC & some PWM modes. Controls duty cycle on OC1A (Pin 9). |
OCR1B |
Compare value, Channel B | Independent second compare channel. Controls duty cycle on OC1B (Pin 10). Cannot define TOP. |
ICR1 |
Input Capture Register | Hardware copies TCNT1 into ICR1 on an edge at the ICP1 pin (Pin 8). Also usable as TOP in some PWM modes. |
| Register | Purpose | Details |
|---|---|---|
TCCR1A |
Control Register A | Sets compare-output mode (COM bits) and lower WGM bits. Determines what happens to OC1A/OC1B pins on a match. |
TCCR1B |
Control Register B | Sets clock source/prescaler (CS bits), upper WGM bits, and input-capture edge/noise settings. |
TIMSK1 |
Interrupt Mask | Bit-flags to enable specific interrupts: overflow (TOIE1), compare-match A/B (OCIE1A/B), input capture (ICIE1). |
TIFR1 |
Interrupt Flags | Hardware sets these when an event occurs. Cleared automatically when the ISR runs, or write a 1 to clear manually. |
// --- Quick-reference examples ---
// Read the current timer count
uint16_t now = TCNT1;
// Preset the counter to a known value
TCNT1 = 0;
// Set a compare value (1 Hz with prescaler 1024)
OCR1A = 15624;
// Set a second compare for 50 % duty
OCR1B = 7812;
// Read the captured timestamp after
// an external edge on ICP1 (Pin 8)
uint16_t stamp = ICR1;
// Configure CTC mode, prescaler 1024
TCCR1A = 0; // WGM11:10 = 00
TCCR1B = (1 << WGM12) // CTC mode
| (1 << CS12) | (1 << CS10);// /1024
// Enable compare-match A interrupt
TIMSK1 = (1 << OCIE1A);
// Check if overflow happened (polling)
if (TIFR1 & (1 << TOV1)) {
TIFR1 = (1 << TOV1); // clear flag
}
uint16_t now = TCNT1; — Read the timer's current count (like glancing at a stopwatch).TCNT1 = 0; — Reset the counter back to zero.OCR1A = 15624; — Tell the timer: "trigger an event when you count to 15624." With a 1024 prescaler and 16 MHz clock, this gives exactly 1 second.OCR1B = 7812; — A second, independent trigger point (halfway to 15624 = 50% duty cycle on Channel B).uint16_t stamp = ICR1; — Read the timestamp that was automatically saved when an external signal edge hit Pin 8.TCCR1A = 0; — Clear Control Register A (sets lower waveform bits to 00).TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); — Two things at once: enable CTC mode (WGM12 bit) and set the prescaler to 1024 (CS12 + CS10 bits). The timer now counts in CTC mode, ticking once every 1024 CPU cycles.TIMSK1 = (1 << OCIE1A); — Enable the compare-match A interrupt. Now when TCNT1 reaches OCR1A, the CPU will automatically jump to the ISR.if (TIFR1 & (1 << TOV1)) — Check if the overflow flag is set (polling instead of using an interrupt).TIFR1 = (1 << TOV1); — Clear the overflow flag by writing a 1 to it (this is how AVR flag clearing works — you write 1, not 0).Now that we know the registers, let's see how they work together in three common real-world situations. Each scenario shows a different Timer 1 capability: generating timed interrupts (CTC mode), measuring an external signal (Input Capture), and controlling two outputs at different duty cycles (Fast PWM). Pay attention to which registers are configured and why.
OCR1A = (16 000 000 / 64 / 100) − 1 = 2499ICR1, then switch to falling edge and read again. The difference × tick period = pulse width.
ICR1 = TOP) to set frequency, then write different duty-cycle values to OCR1A and OCR1B for pins 9 & 10 independently.
TCNT1, OCR1A/B, and ICR1 are 16-bit. The compiler handles byte ordering, but in ISRs or inline assembly you must write the high byte first and read the low byte first (the TEMP register mechanism).
// Scenario 1 – 100 Hz interrupt
cli();
TCCR1A = 0;
TCCR1B = (1 << WGM12) // CTC
| (1 << CS11) | (1 << CS10); // /64
OCR1A = 2499;
TIMSK1 = (1 << OCIE1A);
sei();
// Scenario 2 – Pulse width via Input Capture
TCCR1A = 0;
TCCR1B = (1 << ICES1) // rising edge
| (1 << CS11); // /8 → 0.5 µs tick
TIMSK1 = (1 << ICIE1); // capture interrupt
volatile uint16_t riseTime, fallTime;
ISR(TIMER1_CAPT_vect) {
if (TCCR1B & (1 << ICES1)) {
riseTime = ICR1;
TCCR1B &= ~(1 << ICES1); // next: falling
} else {
fallTime = ICR1;
TCCR1B |= (1 << ICES1); // next: rising
}
}
// pulseWidth_us = (fallTime-riseTime)*0.5;
// Scenario 3 – Dual PWM on Pins 9 & 10
TCCR1A = (1 << COM1A1) | (1 << COM1B1)
| (1 << WGM11); // Fast PWM mode 14
TCCR1B = (1 << WGM13) | (1 << WGM12)
| (1 << CS11); // /8
ICR1 = 39999; // TOP → 50 Hz (servo)
OCR1A = 3000; // Pin 9 duty
OCR1B = 1500; // Pin 10 duty
| Register | Name | Purpose |
|---|---|---|
TCNT1 |
Timer/Counter 1 | Current count value (16-bit) |
OCR1A |
Output Compare Register 1A | Compare value for channel A |
OCR1B |
Output Compare Register 1B | Compare value for channel B |
ICR1 |
Input Capture Register 1 | Captures timer value on external event |
TCCR1A |
Timer/Counter Control Register A | Waveform generation, compare output modes |
TCCR1B |
Timer/Counter Control Register B | Clock select, WGM bits |
TIMSK1 |
Timer Interrupt Mask | Enable/disable interrupts |
TIFR1 |
Timer Interrupt Flag | Interrupt status flags |
Timer/Counter1 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 |
COM1A[1:0] / COM1B[1:0] - Compare Output Mode:
| COM1x1 | COM1x0 | Description (Non-PWM) |
|---|---|---|
| 0 | 0 | Normal port operation, OC1x disconnected |
| 0 | 1 | Toggle OC1x on Compare Match |
| 1 | 0 | Clear OC1x on Compare Match (set low) |
| 1 | 1 | Set OC1x on Compare Match (set high) |
Timer/Counter1 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 |
CS1[2:0] - Clock Select (Prescaler):
| CS12 | CS11 | CS10 | Description |
|---|---|---|---|
| 0 | 0 | 0 | No clock (Timer stopped) |
| 0 | 0 | 1 | clk/1 (No prescaling) |
| 0 | 1 | 0 | clk/8 |
| 0 | 1 | 1 | clk/64 |
| 1 | 0 | 0 | clk/256 |
| 1 | 0 | 1 | clk/1024 |
WGM bits are split across TCCR1A and TCCR1B:
| Mode | WGM13 | WGM12 | WGM11 | WGM10 | Mode Name | TOP |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | Normal | 0xFFFF |
| 4 | 0 | 1 | 0 | 0 | CTC (OCR1A) | OCR1A |
| 5 | 0 | 1 | 0 | 1 | Fast PWM, 8-bit | 0x00FF |
| 6 | 0 | 1 | 1 | 0 | Fast PWM, 9-bit | 0x01FF |
| 7 | 0 | 1 | 1 | 1 | Fast PWM, 10-bit | 0x03FF |
| 14 | 1 | 1 | 1 | 0 | Fast PWM (ICR1) | ICR1 |
| 15 | 1 | 1 | 1 | 1 | Fast PWM (OCR1A) | OCR1A |
| Bit | Name | Description |
|---|---|---|
| 5 | ICIE1 | Input Capture Interrupt Enable |
| 2 | OCIE1B | Output Compare B Match Interrupt Enable |
| 1 | OCIE1A | Output Compare A Match Interrupt Enable |
| 0 | TOIE1 | Timer Overflow Interrupt Enable |
| Bit | Name | Description |
|---|---|---|
| 5 | ICF1 | Input Capture Flag |
| 2 | OCF1B | Output Compare B Match Flag |
| 1 | OCF1A | Output Compare A Match Flag |
| 0 | TOV1 | Timer Overflow Flag |
Here we configure Timer 1 to count up to a specific value and then automatically reset to 0, triggering an interrupt each time it matches. This creates a precise, repeating time interval — like a programmable alarm clock inside the chip.
Steps to configure a timer interrupt:
|= (1 << BIT_NAME)
This pattern is how we set a single bit inside a register without changing the other bits. Let's break it down step by step:
WGM12 is just a constant = 3 (it's bit number 3 in TCCR1B).(1 << WGM12) means "shift the number 1 left by 3 positions":00000001 → shift left 3 → 00001000 (this creates a mask with only bit 3 set)TCCR1B |= 00001000 means "OR the register with this mask":TCCR1B: 00000000mask: 00001000result: 00001000 — bit 3 is now ON, all others unchanged.Why |= and not =? Using = would overwrite all 8 bits. Using |= (OR-equals) only turns ON the bits in the mask while leaving everything else untouched.
void setup() {
// Disable interrupts during setup
cli();
// Reset Timer 1 control registers
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0; // Reset counter
// Set compare match value for 1 Hz
// OCR1A = (16MHz / (prescaler * freq)) - 1
// OCR1A = (16000000 / (1024 * 1)) - 1 = 15624
OCR1A = 15624;
// Set CTC mode (WGM12 = 1)
TCCR1B |= (1 << WGM12);
// Set prescaler to 1024
TCCR1B |= (1 << CS12) | (1 << CS10);
// Enable compare match interrupt
TIMSK1 |= (1 << OCIE1A);
// Enable global interrupts
sei();
}
Complete example: Blink LED at 1 Hz using Timer 1
ISR(TIMER1_COMPA_vect) for Timer 1 Compare Match A interrupt
#define LED_PIN 13
volatile bool ledState = false;
// Timer 1 Compare Match A ISR
ISR(TIMER1_COMPA_vect) {
ledState = !ledState;
digitalWrite(LED_PIN, ledState);
}
void setup() {
pinMode(LED_PIN, OUTPUT);
cli(); // Disable interrupts
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 15624; // 1 Hz @ 1024 prescaler
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS12) | (1 << CS10); // 1024
TIMSK1 |= (1 << OCIE1A); // Enable int
sei(); // Enable interrupts
}
void loop() {
// Nothing here - timer handles everything!
}
| Desired Frequency | Prescaler | OCR1A Value | Formula |
|---|---|---|---|
| 1 Hz (1 sec) | 1024 | 15624 | (16M / 1024 / 1) - 1 |
| 10 Hz (100 ms) | 1024 | 1561 | (16M / 1024 / 10) - 1 |
| 100 Hz (10 ms) | 64 | 2499 | (16M / 64 / 100) - 1 |
| 1000 Hz (1 ms) | 64 | 249 | (16M / 64 / 1000) - 1 |
Compare registers (OCR1A/B) are double buffered:
Measure external events with hardware precision:
Convert PWM to analog voltage with an RC filter:
| Arduino API | Direct Register Access | |
|---|---|---|
| Ease of Use | Very easy | Complex |
| Flexibility | Limited | Full control |
| Portability | Works across Arduino boards | Chip-specific |
| Performance | Overhead from abstraction | Maximum efficiency |
| Learning | Quick start | Deep understanding required |
Simplify timer usage without full register knowledge:
// Using TimerOne library
#include <TimerOne.h>
void blinkLED() {
digitalWrite(13, !digitalRead(13));
}
void setup() {
pinMode(13, OUTPUT);
// Initialize Timer1 to 1 second
Timer1.initialize(1000000); // microseconds
// Attach interrupt function
Timer1.attachInterrupt(blinkLED);
}
void loop() {
// Your other code here
}
Adjust the parameters to see how they affect the PWM signal:
Average Voltage: 2.50 V
🤔
Review the ATmega328P datasheet sections on Timer/Counter 1
"Hello!" to the Serial Monitor.
CTC Interrupt Frequency Formula:
TCCR1A — Timer/Counter1 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 |
TCCR1B — Timer/Counter1 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 |
Waveform Generation Mode (WGM) — CTC options
| Mode | WGM13 | WGM12 | WGM11 | WGM10 | Description | TOP |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | Normal | 0xFFFF |
| 4 | 0 | 1 | 0 | 0 | CTC | OCR1A |
| 12 | 1 | 1 | 0 | 0 | CTC | ICR1 |
Clock Select (Prescaler)
| CS12 | CS11 | CS10 | Description |
|---|---|---|---|
| 0 | 0 | 0 | No clock (timer stopped) |
| 0 | 0 | 1 | clk / 1 (no prescaling) |
| 0 | 1 | 0 | clk / 8 |
| 0 | 1 | 1 | clk / 64 |
| 1 | 0 | 0 | clk / 256 |
| 1 | 0 | 1 | clk / 1024 |
TIMSK1 — Timer Interrupt Mask Register
| Bit | Name | Description |
|---|---|---|
| 5 | ICIE1 | Input Capture Interrupt Enable |
| 2 | OCIE1B | Output Compare B Match Interrupt Enable |
| 1 | OCIE1A | Output Compare A Match Interrupt Enable |
| 0 | TOIE1 | Overflow Interrupt Enable |
Using the register tables from the previous slide, complete the code so that "Hello!" prints to Serial every 500 ms.
OCR1A = (16,000,000 / (prescaler × 2)) − 1volatile bool printFlag = false;
ISR(TIMER1_COMPA_vect) {
printFlag = true;
}
void setup() {
Serial.begin(9600);
cli();
// 1. Reset registers
TCCR1A = ___;
TCCR1B = ___;
TCNT1 = ___;
// 2. Set compare value for 500 ms (2 Hz)
// OCR1A = ???
OCR1A = ___;
// 3. Set CTC mode
// Which bit(s) go in TCCR1A? ___
// Which bit(s) go in TCCR1B? ___
TCCR1B |= (1 << ___);
// 4. Set prescaler
// Which CS bits do you need?
TCCR1B |= (1 << ___) | (1 << ___);
// 5. Enable compare match A interrupt
TIMSK1 |= (1 << ___);
sei();
}
void loop() {
if (printFlag) {
printFlag = false;
Serial.println("Hello!");
}
}