Module 6 - Interrupt

1. Introduction to Interrupt

An interrupt is a mechanism used in microcontroller programming to pause the execution of the current program and call a specific routine or function when a particular event occurs. These events, defined by the programmer, can range from a specific condition on an input pin to a timer overflow or other hardware-defined triggers. Once the routine or function finishes executing, the program resumes from the exact point where it was interrupted.

The Arduino Uno (ATMega328P) provides two primary types of interrupts: External Interrupts and Timer Interrupts. While their functions and triggers differ, they share the same goal: interrupting the main program flow to handle specific events immediately.


External Interrupt

An External Interrupt is triggered by a change in the voltage level on specific input pins of the microcontroller. On the Arduino Uno, there are two pins dedicated to external interrupts: Pin 2 and Pin 3. These are the most commonly used pins when implementing interrupts via the Arduino programming language.

External Interrupts: INT0 and INT1

While the Arduino IDE refers to these simply as Pin 2 and Pin 3, the ATMega328P datasheet identifies them as INT0 and INT1. These are hardware-level designations that correspond to specific physical pins.

Trigger Modes

Both INT0 and INT1 can be configured to trigger the Interrupt Service Routine (ISR) based on four specific signal states:

  1. LOW: Triggered whenever the pin is at a logic low level.
  2. CHANGE: Triggered whenever the pin changes value (High to Low or Low to High).
  3. RISING: Triggered specifically when the pin goes from Low to High.
  4. FALLING: Triggered specifically when the pin goes from High to Low.

Internal Interrupt

An Internal Interrupt is triggered by modules located inside the microcontroller itself. In the ATMega328P, these are generated by timers and are referred to as Timer Interrupts. A Timer Interrupt is specifically triggered by a timer overflow. The Arduino Uno features three internal timers: Timer0, Timer1, and Timer2. (For more details on how timers operate, please refer to the previous module).

To expand on the specific hardware components of the ATMega328P (the heart of the Arduino Uno), we need to look at how the microcontroller labels and manages these specific interrupt sources.

Internal Interrupts: Timer0, Timer1, and Timer2

The Arduino Uno has three hardware timers, each capable of generating interrupts. These are essential for tasks that require precise timing without blocking the void loop().

1. Timer0 (8-bit)
2. Timer1 (16-bit)
3. Timer2 (8-bit)

2. Interrupt Handler

On the ATMega328P microcontroller, there are three essential requirements that must met to enable an interrupt:

  1. Global Interrupt Enabled: Interrupts must be allowed to occur globally across any part of the program. While the Arduino environment typically enables this by default, it is important to verify this when using different microcontrollers or during troubleshooting.
  2. Individual Interrupt Enabled: Each specific interrupt must be permitted to occur via dedicated interrupt control registers.
  3. Interrupt Condition Met: The microcontroller must receive a signal (either internal or external) that matches the predefined trigger criteria.

To enable a microcontroller to process interrupts, several steps must be followed. This module organizes these steps according to their placement in the code for easier implementation. The first step involves preparing the Interrupt Handler the specific code that will run when the interrupt is triggered.

Microcontrollers use a specialized memory block called the Interrupt Vector Table. This table stores the memory addresses of the programs to be executed for each specific type of interrupt.

Vector No. Program Address Source Interrupt Definition
1 0x0000 RESET External pin, Power-on Reset, Brown-Out Reset, Watchdog System Reset
2 0x0002 INT0 External Interrupt Request 0
3 0x0004 INT1 External Interrupt Request 1
4 0x0006 PCINT0 Pin Change Interrupt Request 0
5 0x0008 PCINT1 Pin Change Interrupt Request 1
6 0x000A PCINT2 Pin Change Interrupt Request 2
7 0x000C WDT Watchdog Time-out Interrupt
8 0x000E TIMER2_COMPA Timer/Counter2 Compare Match A
9 0x0010 TIMER2_COMPB Timer/Counter2 Compare Match B
10 0x0012 TIMER2_OVF Timer/Counter2 Overflow
11 0x0014 TIMER1_CAPT Timer/Counter1 Capture EVent
12 0x0016 TIMER1_COMPA Timer/Counter1 Compare Match A
13 0x0018 TIMER1_COMPB Timer/Counter1 Compare Match A
14 0x001A TIMER1_OVF Timer/Counter1 Overflow
15 0x001C TIMER0_COMPA Timer/Counter0 Compare Match A
16 0x001E TIMER0_COMPB Timer/Counter0 Compare Match B
17 0x0020 TIMER0_OVF Timer/Counter0 Overflow
18 0x0022 SPI STC SPI Serial Transfer Complete
19 0x0024 USART_RX Usart Rx Complete
20 0x0026 USART_UDRE Usart Data Register Empty
21 0x0028 USART_TX Usart Tx Complete
22 0x002A ADC ADC Conversion Complete
23 0x002C EE READY EEPROM Ready
24 0x002E ANALOG COMP Analog Comparator

These addresses are fixed and cannot be changed. When an interrupt occurs, the microcontroller automatically jumps to the corresponding address in the Interrupt Vector Table to execute the associated program.

For example if an external interrupt (INT0) is triggered, the microcontroller will jump to address 0x0002 to execute the code defined in that location.

See the program address between INT0 and INT1, which are 0x0002 and 0x0004 respectively. The gap between these addresses is only 2 bytes, which is the size of a single instruction in AVR assembly language. This means that the interrupt handler for INT0 must be concise and fit within this limited space, often requiring the use of a jump instruction to redirect to a larger block of code if necessary.

Example:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.org 0x0002	 ; external interrupt 0 vector
    rjmp toggle_led

.global main

main:
 ; your main program here

toggle_led:
    ; main interrupt logic here
    in r16, PORTB
    eor r16, (1<<PB5)
    out PORTB, r16
    reti ; return from interrupt

From the above example, we have initialized the handler for the interrupt, and then created a routine named toggle_led, which contains the code that will be executed when the interrupt is triggered. When an INT0 occurs (for example, when a button connected to the INT0 pin is pressed), the microcontroller will jump to the toggle_led routine. After performing the necessary actions, we need to inform the microcontroller that the interrupt has been successfully handled and to continue with the main program. We can use the keyword RETI, which stands for Return from Interrupt, to achieve this.

An interrupt will take priority over the main program, meaning that when an interrupt occurs, the microcontroller will temporarily halt the execution of the main program to execute the interrupt handler. Programmer must be cautious when writing interrupt handlers, as they should be efficient and not contain long-running code, to avoid delaying the main program for too long.

Using Vector Names

Instead of using raw addresses for interrupts, AVR assembly language provides predefined vector names that can be used to improve code readability. For example, instead of using .org 0x0002 for the INT0 interrupt, we can use the predefined name INT0_vect as follows:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global INT0_vect ; declare the interrupt vector as global
.global main

main:
 ; your main program here

INT0_vect: ; Toggle LED
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

Pro tip: Just always use Vector Names instead of raw addresses :)

3. External Interrupt Registers

For detailed information about the registers, please refer to the Atmega32p datasheet. here

From the previous section, we have set up the interrupt handler for external interrupt 0. Now, we will set up the registers to setup/initialize this interrupt. Let's say we want to toggle an LED on pin PB5 with falling edge trigger. We can write the following code in our setup section:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main
.global INT0_vect

main:
    ; initialize external interrupt 0
    ldi r16, (1<<ISC01) ; Set ISC01 bit = 1 | trigger on falling edge
    sts EICRA, r16 ; write to EICRA register to set the interrupt trigger condition
    ldi r16, (1<<INT0) ; set INT0 bit = 1 | enable external interrupt 0
    sts EIMSK, r16 ; write to EIMSK register to enable the interrupt

    ; enable global interrupts
    sei

INT0_vect:
    ; main interrupt logic here
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

Let's break it down register by register:

EICRA (External Interrupt Control Register A)

EIMSK (External Interrupt Mask Register)

sei (Set Global Interrupt Enable)

This instruction enables global interrupts. It MUST be called after setting up the individual interrupt configurations to allow the microcontroller to respond to interrupts.


You should get the rough idea for INT1 as well. You just need to set the ISC11 and ISC10 bits in EICRA for the trigger condition and set the INT1 bit in EIMSK to enable it. Read the datasheet for more details on the trigger conditions for INT1.

4. Internal/Timer Interrupts

Internal Interrupts

Now Internal interrupts or Timer interrupts is somewhat more complex then external interrupts. Before going for implementation let's understand on what use case we can use timer interrupts:

For now we will try replacing delay loops with TIMER1 compare match interrupt. We will set up the timer to generate an interrupt every 5 seconds and toggle an LED in the interrupt handler.

#define __SFR_OFFSET 0
#include <avr/io.h>

.global main
.global TIMER1_COMPA_vect ; The linker looks for this specific name

main:
    ; Set PB5 as output
    sbi DDRB, 5

    ; Clear r16 to use as a zero register
    clr r16
    sts TCCR1A, r16

    ; Set prescaler to 1024 and enable CTC mode (Clear Timer on Compare)
    ; CTC mode is better for toggling so you don't have to manually reset TCNT1
    ldi r17, (1 << WGM12) | (1 << CS12) | (1 << CS10)
    sts TCCR1B, r17

    ; Reset timer count
    sts TCNT1H, r16
    sts TCNT1L, r16

    ; Set compare match value (62499 = 0xF423)
    ldi r17, 0xF4
    sts OCR1AH, r17
    ldi r17, 0x23
    sts OCR1AL, r17

    ; Enable timer compare match interrupt
    ldi r17, (1 << OCIE1A)
    sts TIMSK1, r17

    sei ; Enable global interrupts

loop:
    rjmp loop

TIMER1_COMPA_vect:
    ; toggle LED on PB5
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

TCCR1A: Timer/Counter Control Register A for Timer1

TCCR1B: Timer/Counter Control Register B for Timer1

TCNT1L and TCNT1H: Timer/Counter Register for Timer1

OCR1AL and OCR1AH: Output Compare Register for Timer1

TIMSK: Timer/Counter Interrupt Mask Register

sei: Set Global Interrupt Enable


TO DO: Leaern more about timer interrupts (TIMER0, TIMER2) from the official datasheet