Skip to main content

Command Palette

Search for a command to run...

Blinking an LED using Timer Interrupt CTC Method | AVR Bare Metal #4

Should We Wait for Overflow in the Interrupt Method?

Updated
7 min read
Blinking an LED using Timer Interrupt CTC Method | AVR Bare Metal #4
S
Embedded firmware developer....currently learning bare metal programming and sharing my journey.

In the previous articles, we used Timer1 of the ATmega328P to generate delays for blinking the onboard LED.

Till now we have explored two approaches. The first was the Polling Method, where the CPU continuously checked the overflow flag to determine whether the timing event had occurred. The second was the Interrupt Method, where the timer hardware notified the CPU automatically whenever an overflow occurred.

The main advantage of interrupts was that the CPU was no longer busy checking the overflow flag. Instead, it could continue executing other code and only pause briefly to execute the ISR whenever the timer interrupt occurred.

However, both approaches still had one thing in common: they relied on the Timer Overflow Event.

Because of this, we had to calculate and preload the TCNT1 register every time we wanted a specific delay.

That works, but constantly calculating preload values becomes inconvenient. Fortunately, Timer1 provides a better way to generate periodic timing events: CTC Mode.

What is CTC Mode ?

CTC datasheet screenshot

What this simply means is that instead of the Timer Counter TCNT1 counting all the way to 65535 and overflowing, it counts until a specific compare value stored in OCR1A. Once that value is reached, the hardware automatically resets the counter and can trigger an interrupt.

As you can see from the datasheet, there are two things we need to do:

  • First put the Timer1 in CTC Mode.

  • Second, use the OCR1A register to be our TOP value.

I don't remember if I discussed TOP before, but it is basically the maximum value the timer counter can reach before it resets or overflows. In the previous two experiments we put the Timer1 in Normal Mode and the reason it could count till 65535 and then it overflow and the TOV1 flag was set. It worked this way because the mode in which were it worked that way below is the screenshot of the datasheet for the Normal Mode.

Timer Mode Selection

TOP Value is 0xFFFF which in decimal is 65535, and TOV1 Flag set on MAX the MAX value is set to 0xFFFF. That is why the TOV1 Flag is set after 65535.

So for CTC we need to put the Timer1 in CTC mode.

Timer Mode Selection

Mode 4 and Mode 12 both are CTC Mode the only difference is the TOP register. So for this article we are using OCR1A register as the TOP, so we are using Mode 4 for this article.

With this understood that we need to put the Timer1 in Mode 4 to get it into CTC mode. So lets see what all changes we need to from previous interrupt configurations.

  • Firstly, Timer1 Mode changes from Normal Mode to CTC Mode WGM1[3:0] = 0x04 or 0100.

  • Second, previous we were using the overflow Interrupt now we will be using compare match Interrupt

    TIMSK1 Register Description
  • Third, previously in ISR i was putting TIMER1_OVF_vect for Timer Overflow Interrupt but in this we will have to use TIMER1_COMPA_vect

    ATMega 328P Interrupt Vector

So since we finalised on using the Mode 4, OCR1A is the TOP. When the counter reaches OCR1A it will execute the Timer 1 interrupt.

Now that we know OCR1A will act as the new TOP value, we need to calculate what value should be loaded into it to generate a 1-second delay.

The prescaler for the Timer we selected is 1024 and with the Timer frequency becomes 15625 Hz.

$$f_{\text{timer}}=\frac{f_{\text{CPU}}}{\text{Prescaler}}=\frac{16,000,000}{1024}=15,625\ \text{Hz}$$

This means that the timer increments 15625 times per second. Meaning 15625 ticks is equal to 1s.

Previously we used to preload the TCNT register in such a way that when overflow used to happened it was our desired delay.

$$\text{For 1s,}\hspace{0.5cm}TCNT1 = 65536 - 15625 = 49911$$

$$\text{For 2s,}\hspace{0.5cm}TCNT1 = 65536 - (2 \cdot 15625) = 34286$$

But with this CTC Mode we dont need to preload the TCNT register. We know 15625 ticks means 1s. So we simply have to do OCR1A = 15625. But the Timer counter starts from 0.

$$\text{For 1s,}\hspace{0.5cm}OCR1A = 15625 - 1= 15624$$

$$\text{For 2s,}\hspace{0.5cm}OCR1A = (2 \cdot 15625) - 1= 31249$$

So this way we can set the Compare Value for the Counter when the TCNT reaches this value it will trigger an interrupt and we can perform our logic there.

With this done, lets move directly to the register summary

Register Summary

Peripheral Register Address Bits Used Purpose
Onboard LED DDRB 0x24 DDB5 = 1 Configure PB5 as output
Onboard LED PORTB 0x25 PORTB5 Toggle PB5 HIGH/LOW to blink LED
Timer1 TCCR1A 0x00 - CTC Mode
Timer1 TCCR1B 0x80 WGM12 = 1 CTC Mode
Timer1 OCR1A 0x88 16 bit value = 15624 Compare Value set for 1s
Timer1 TIMSK1 0x6F OCIEA = 1 Enable Timer 1 Compare A Interrupt
Timer1 TCCR1B 0x80 CS12 = 1, CS11 = 0, CS10 = 1 Select Prescaler 1024
Global Interrupt Control SREG 0x5F I = 1 Enable global interrupts using sei()

Implementation

The implementation is now slightly simpler than the previous interrupt example because we no longer need to preload TCNT1 or reload it inside the ISR.

main.c

#include <avr/interrupt.h>

#define DDRB_LED   (*(volatile uint8_t *)0x24)
#define PORTB_LED  (*(volatile uint8_t *)0x25)
#define TCCR1A     (*(volatile uint8_t *)0x80)
#define TCCR1B     (*(volatile uint8_t *)0x81)
#define TCNT1      (*(volatile uint16_t *)0x84)
#define TIFR1      (*(volatile uint8_t *)0x36)
#define TIMSK1     (*(volatile uint8_t *)0x6F)
#define OCR1A      (*(volatile uint16_t *)0x88)

void setup() {
  DDRB_LED |= (1<<5);           // Configuring led as output
  // Timer 1 config
  TCCR1A = 0x00;                // Timer CTC Mode (No change in this reg) 
  TCCR1B = 0x08;                // CTC Mode
  OCR1A = 15624;                // Compare Value set for 1s
  TIMSK1 |= (1<<1);            // Timer 1 Compare A interrupt enable  
  sei();
  TCCR1B |= (1<<2) | (1<<0);   // Starting timer Clk selection with 1024 Prescaler
}

void loop() {
  // Does Nothing..
}

ISR(TIMER1_COMPA_vect) {
  PORTB_LED ^= (1<<5);      // toggling led
}

No change in the Makefile was needed.

Source Code

If you'd like to experiment with the code yourself, the complete project used in this article is available on GitHub, including the main.c file and Makefile.

GitHub Repository: AVR-Bare-Metal-4-Code

What's Next ?

With this, we have now covered three different ways of using Timer1 to generate delays.

Among the three, the CTC Interrupt Method is the most practical. Unlike polling, it does not keep the CPU busy checking a flag, and unlike the overflow-based interrupt approach, it does not require preloading the timer counter to achieve a desired delay.

Moving forward its very natural to create something with this otherwise there is no meaning of doing such experiments.

The Arduino ecosystem provides two commonly used timing functions:

  • delay() – A blocking delay that pauses execution for a specified amount of time.

  • millis() – A non-blocking timing function that allows the rest of the code to continue running.

In the next article, we will build our own version of millis() (or rather, I should say my_millis()) using Timer1. Once we have that working, we will use it to create a simple cooperative scheduler and run multiple tasks without blocking the CPU.