LABORATORY 04

The Timer Module

Duration: 2 hours Material: Chapter 4 Platform: Arduino Uno / ATmega328P PDF handout RO versiunea română

Timer modules are hardware counters that run independently of the processor. They generate precise intervals, measure durations, and produce PWM signals - without blocking the program with delay().

1Lab objectives

  • Understanding the role of the prescaler and computing the counting frequency
  • Configuring CTC mode to generate a precise time base
  • Using the compare-match interrupt for periodic execution
  • Generating PWM signals and controlling the duty cycle
  • Replacing delay() with non-blocking timing

2Materials needed

  • 1 Arduino Uno
  • 1 Breadboard
  • 2 LEDs + 220 Ω resistors
  • 1 10 kΩ potentiometer
  • 1 Passive buzzer (optional)
  • 6 Jumper wires

3Theoretical background

How it works

A timer is a counter that increases by 1 with every clock pulse it receives. When it reaches its maximum value (255 for 8 bits, 65,535 for 16 bits), it resets and sets an overflow flag. Being implemented in hardware, it counts in parallel with program execution.

Timers available on the ATmega328P

TimerResolutionPWM pinsUsed internally by
Timer08 bitsD5, D6millis(), delay() - avoid modifying
Timer116 bitsD9, D10the Servo library
Timer28 bitsD3, D11tone()

The prescaler

The 16 MHz clock is too fast for long intervals: a 16-bit timer would fill up in just 4.1 ms. The prescaler divides the frequency before it reaches the counter:

Prescaler NCounting frequencyDuration of 1 incrementTime to overflow (16 bits)
116 MHz62.5 ns4.10 ms
82 MHz0.5 µs32.8 ms
64250 kHz4 µs262 ms
25662.5 kHz16 µs1.05 s
102415.625 kHz64 µs4.19 s

Operating modes

NORMAL mode - counts up to MAX, then overflows MAX CTC mode - resets at OCR and toggles the output OCR OC1A output (toggles on every compare match)
Fig. 1 - Comparison between Normal mode (counting up to MAX) and CTC mode (reset at the compare-register value, with output toggling).
The basic formula for CTC mode
OCRn = ( F_CPU / (2 · N · f_desired) ) − 1
For a periodic interrupt (not pin toggling), the factor of 2 disappears:
OCRn = ( F_CPU / (N · f_desired) ) − 1

4Interactive timer calculator

Change the parameters and watch how the period and frequency change. The chart shows the counter climbing to the compare value and the resulting signal at the pin.

Prescaler, OCR and the resulting frequency
CheckFor F_CPU = 16 MHz, prescaler 256 and OCR = 62,499 you get exactly 1 second: (62,499 + 1) · 256 / 16,000,000 = 1.000 s.

5Generating a PWM signal

PWM (Pulse Width Modulation) produces a variable average voltage by switching rapidly between 0 and 5 V. The ratio between the "on" time and the total period is called the duty cycle. An LED driven this way appears to change brightness, and a motor changes speed.

PWM simulator - duty cycle and average voltage
Configuring Timer1 - the control register
The configuration aboveWGM12 = 1 → CTC mode; CS12 = 1, CS11 = 0, CS10 = 0 → prescaler 256; OCIE1A = 1 → the compare-A interrupt is enabled.
How a timer works: prescaler, TCNT, comparison with OCR
Fig. 2 - How a timer works: the clock source passes through the prescaler, feeds the TCNT counter, and the comparator continuously checks it against OCR. This produces two interrupt sources: overflow and equality. Fig. 4.1 in the handout
Block diagram of the 8-bit timer module
Fig. 3 - The complete block diagram of the 8-bit timer module, from the ATmega328P datasheet. Fig. 4.2 in the handout
The TCCR0A register
Fig. 4 - The TCCR0A register: the COM bits control the behavior of the output pins, and WGM01:WGM00 are part of selecting the waveform generation mode. Fig. 4.3 in the handout
The TCCR0B register
Fig. 5 - The TCCR0B register: the CS02:CS00 bits select the prescaler, and WGM02 completes the mode configuration. Fig. 4.4 in the handout
A note on numberingThe figures in the handout show Timer0 (8 bits), while the examples on this page use Timer1 (16 bits). The structure is identical - only the digit in the register names changes (TCCR0A → TCCR1A) along with the counter's range. Timer1 is preferred because it does not interfere with millis().

PWM at the register level - what analogWrite() does

The analogWrite() function hides the configuration of three registers. Written explicitly, the same operation looks like this - and becomes fully controllable: you can choose the frequency, resolution, and polarity, things impossible through the library.

pwm_registers.ino - fast 8-bit PWM on pin 9
void pwmInit() {
  DDRB |= (1 << PB1);              // pin 9 (OC1A) as output

  // COM1A1 = 1, COM1A0 = 0  -> non-inverted output: HIGH at start, LOW at compare
  TCCR1A |= (1 << COM1A1);
  TCCR1A &= ~(1 << COM1A0);

  // WGM13:WGM10 = 0101  -> Fast PWM, 8-bit (TOP = 255)
  TCCR1A |=  (1 << WGM10);
  TCCR1A &= ~(1 << WGM11);
  TCCR1B |=  (1 << WGM12);
  TCCR1B &= ~(1 << WGM13);

  // prescaler 64 -> f_PWM = 16 MHz / (64 * 256) = 976 Hz
  TCCR1B &= ~((1 << CS12) | (1 << CS11) | (1 << CS10));   // clear the field
  TCCR1B |=   (1 << CS11) | (1 << CS10);                   // write 011
}

void pwmSet(uint8_t duty) {
  OCR1A = duty;                    // 0 = always off, 255 = almost always on
}

void setup() {
  pwmInit();
  pwmSet(128);                     // 50% - equivalent to analogWrite(9, 128)
}

void loop() { }
Notice the "clear then write" patternThe prescaler occupies three bits, so it cannot be set with a plain |= - the previous bits would remain active too. First the entire field is cleared with &= ~(...), then the desired value is written. This is the standard idiom for any multi-bit field, explained at length in Laboratory 1B.
Why frequency control mattersanalogWrite() forces 490 Hz on pins 9 and 10. For driving a motor, this frequency falls in the audible band and produces an annoying whine. Through direct configuration you can go above 20 kHz, past the threshold of hearing - a frequent requirement in practice, impossible to meet with the library.
Practical setup for a digital clock with LCD display
Fig. 6 - The setup for the application proposed in the handout: a digital clock, where the one-second time base is generated by the timer's compare interrupt, and the time is shown on an LCD. Fig. 4.10 in the handout

Porting a timer configuration

StepAVR / ATmega328PSTM32ESP32
Enabling the clockimplicitRCC->APB1ENR |= TIM2ENhandled by the SDK
PrescalerTCCR1B, bits CS12:CS10TIMx->PSC (direct value)divider in LEDC configuration
Compare valueOCR1ATIMx->CCR1ledc_set_duty()
Period / TOPICR1 or fixedTIMx->ARRresolution in bits
Enabling the interruptTIMSK1 |= (1<<OCIE1A)TIMx->DIER |= CC1IEregistered callback
The concepts do not changePrescaler, compare value, counting mode, compare-match interrupt - all of these exist on every microcontroller. Only the register names change, along with the fact that on modern controllers the prescaler is a direct numeric value instead of a combination of three bits.

6Source code

A 1-second time base in CTC mode

timer_ctc_1s.ino
volatile bool secondElapsed = false;

ISR(TIMER1_COMPA_vect) {      // runs exactly once every second
  secondElapsed = true;
  PORTB ^= (1 << PB5);        // toggle the LED on pin 13
}

void setup() {
  DDRB |= (1 << PB5);

  cli();                      // stop interrupts while configuring
  TCCR1A = 0;                 // clear the registers
  TCCR1B = 0;
  TCNT1  = 0;                 // reset the counter

  OCR1A = 62499;              // (16,000,000 / 256) - 1 = 1 second
  TCCR1B |= (1 << WGM12);     // CTC mode
  TCCR1B |= (1 << CS12);      // prescaler 256
  TIMSK1 |= (1 << OCIE1A);    // enable the compare interrupt
  sei();                      // re-enable interrupts

  DDRB |= (1 << PB4);         // pin 12: witness for the main loop
}

void loop() {
  if (secondElapsed) {
    secondElapsed = false;
    PORTB ^= (1 << PB4);      // proof that the loop received the event
  }
}

Non-blocking timing - the alternative to delay()

delay() stops the program completely. With millis() you can run several activities with different periods, in parallel:

multitasking_millis.ino
const uint8_t LED_1 = 8, LED_2 = 9;
unsigned long last1 = 0, last2 = 0;
const unsigned long PERIOD_1 = 250;   // ms
const unsigned long PERIOD_2 = 1000;  // ms

void setup() {
  pinMode(LED_1, OUTPUT);
  pinMode(LED_2, OUTPUT);
}

void loop() {
  unsigned long now = millis();

  if (now - last1 >= PERIOD_1) {
    last1 = now;
    digitalWrite(LED_1, !digitalRead(LED_1));
  }

  if (now - last2 >= PERIOD_2) {
    last2 = now;
    digitalWrite(LED_2, !digitalRead(LED_2));
  }

  // the loop stays free for other tasks
}

PWM brightness control

Here the duty cycle is driven directly from the program, through a ramp. In the fifth lab you will replace it with the value read from a potentiometer, through the analog-to-digital converter.

pwm_ramp.ino
const uint8_t LED_PIN = 9;    // PWM pin (Timer1)

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

void loop() {
  // ramps up: the LED fades in smoothly
  for (uint16_t duty = 0; duty <= 255; duty += 5) {
    analogWrite(LED_PIN, duty);
    delay(25);
  }

  // ramps down: the LED fades out smoothly
  for (uint16_t duty = 255; duty > 0; duty -= 5) {
    analogWrite(LED_PIN, duty);
    delay(25);
  }
}
Worth rememberingThe pin never produces an intermediate voltage: it always switches between 0 and 5 V. It is the eye that averages, because the frequency is well above the threshold at which it can distinguish the flickering. A voltmeter connected to the pin would however show exactly the average, i.e. duty/255 × 5 V.

7Interactive circuit

A timer is a counter that runs on its own. The circuits below show you both of its uses: generating a PWM signal (where you can see the wave and the duty cycle) and measuring an exact interval in CTC mode. Change the prescaler in the registers and watch how the frequency changes.

PWM: the duty cycle drives the LED's brightness
#include <avr/io.h>
#include <util/delay.h>

/* OCR1A decides how much of the period the output stays at 1.
   OCR1A = 0   -> LED off;  OCR1A = 255 -> LED at full brightness. */

int main(void)
{
    DDRB |= (1 << PB1);        // pin 9 = OC1A, output

    /* Fast PWM, 8-bit, TOP = 255, non-inverted output on OC1A */
    TCCR1A = (1 << COM1A1) | (1 << WGM10);
    TCCR1B = (1 << WGM12)  | (1 << CS11);   // prescaler 8

    unsigned int d;

    while (1) {
        for (d = 0; d <= 255; d += 5) {     // ramps up
            OCR1A = d;
            _delay_ms(25);
        }
        for (d = 255; d > 0; d -= 5) {      // ramps down
            OCR1A = d;
            _delay_ms(25);
        }
    }
}
Try thisWatch the wave in the PWM panel while OCR1A climbs and falls: the period stays the same, only the pulse width changes - and the LED follows, even though the pin always switches only between 0 and 5 V. Pause execution and write a value into OCR1A directly from the register panel. Then change CS11 to CS12 (prescaler 256) and watch the frequency shown under the wave: it drops by a factor of 32, and the LED starts to flicker visibly.
Exercise - an exact second in CTC mode
#include <avr/io.h>
#include <avr/interrupt.h>

/* Formula:  OCR1A = F_CPU / (prescaler * desired_frequency) - 1
   At 16 MHz, prescaler 1024, and one toggle per second:
       OCR1A = 16000000 / (1024 * 1) - 1 = 15624              */

ISR(TIMER1_COMPA_vect)
{
    PORTB ^= (1 << PB5);
}

int main(void)
{
    DDRB |= (1 << PB5);

    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10);   // CTC, prescaler 1024
    OCR1A  = 15624;
    TIMSK1 = (1 << OCIE1A);
    sei();

    /* Exercise: change OCR1A so that the LED toggles
       every 250 ms. Calculate the value, do not guess it.
       Check it on the oscilloscope: at 4x speed, one full
       period must last 500 ms.                             */

    while (1) { }
}
Try thisThe speed was set to 4x so you do not have to wait. The oscilloscope shows the real simulated time, so the measurement stays correct. If you put a value greater than 65535 into OCR1A, it does not fit in the 16-bit register - try it and see what happens.

8Work tasks

  • Configure Timer1 in CTC mode to generate an interrupt every second and check the accuracy against a stopwatch over 60 seconds.
  • Calculate and check the OCR1A values for periods of 0.5 s, 2 s, and 5 s. What happens at 5 s with prescaler 256?
  • Write a program that blinks two LEDs with different periods (250 ms and 1 s), without using delay().
  • Drive an LED's brightness through PWM, with the duty cycle set by a potentiometer.
  • Measure the PWM signal's frequency on pin 9 and compare it with the theoretical value of 490 Hz.
  • Create a "breathing" effect: the LED's brightness rises and falls smoothly, continuously.

9Extended application

ExtensionBuild a digital stopwatch with serial output, counting minutes, seconds, and tenths of a second, using only the timer interrupt (without millis()). Add a button on INT0 for start/stop and one on INT1 for reset.

10Review questions

11Resources