Monday, October 23, 2023

Pin interrupt library for AVR microcontrollers

This is a general-purpose library used to configure interrupt pins and attach a function to be executed once the pin is triggered on an AVR micro-controller. 

Pin interrupt library for AVR devices

In this blog post, I will explain how to use interrupts on AVR micro-controllers. Interrupts are a powerful feature that allow the micro-controller to respond to external or internal events without constantly polling for them. Interrupts can improve the performance and efficiency of your code, as well as enable new functionalities. 

Characteristics

 

Features

  • Supports PCINT and EXTINT interrupt types.

PCINT minimum pulse width

The processing of the PCINT interrupt types has to be done in software to tell which pin changed and what edge trigger it (RISING or FALLING). This minimum processing time leads to a minimum pulse width.

  • Minimum Detectable Pulse: ~4µs (at 16MHz clock speeds).
  • Behavior: If an electrical pulse or bounce spike transitions faster than ~4µs, the ISR will still trigger, but the physical pin will have already reverted to its original state before the MCU can read it. In this scenario, changedbits will evaluate to 0.
  • Best Use Cases: Perfect for rotary encoders, push buttons, switches, and lower-speed sensor square waves. For ultra-high-speed pulse capture (e.g., infrared demodulators or high-RPM motor optical encoders), utilizing the microcontroller's dedicated Input Capture Unit (ICU) hardware timer or EXTINT pins is recommended.

Supported devices

  • Developed on ATmega328PB.
  • Supports Class 1 AVR (UPDI) devices.
 

Contents


What are interrupts?

An interrupt is a signal that causes the micro-controller to temporarily stop its current execution and jump to a special function called an interrupt service routine (ISR). The ISR performs the necessary actions to handle the interrupt, and then returns to the original program flow. The ISR can be triggered by various sources, such as:

  • External pins (EXTINT)
  • Pin changes (PCINT)
  • Timers/Counters (TIMER)
  • Serial communication (USART, SPI, TWI)
  • Analog-to-digital conversion (ADC)
  • Analog comparator (AC)
  • Watchdog timer (WDT)
  • EEPROM ready (EE READY)
  • Store program memory ready (SPM READY)

Each interrupt source has a corresponding vector in the interrupt vector table, which is located at the beginning of the program memory. The vector is a pointer to the address of the ISR. The first vector is always the reset vector, which points to the start of the program. The rest of the vectors are ordered according to their priority, with lower addresses having higher priority. For example, on the ATmega328PB, the second vector is for the external interrupt request 0 (INT0), and the last vector is for the pin change interrupt request 3 (PCINT3).

How to use interrupts

These steps refer to configuring interrupts in general and don't need to be followed in order to use the library. 

To use interrupts, you need to do three things:

1. Define the ISR using the macro ISR(vector_name), where vector_name is the identifier of the interrupt vector. For example, ISR(INT0_vect) defines the ISR for the INT0 interrupt. The code inside the ISR should be as short and simple as possible, to avoid blocking other interrupts or delaying the main program.

2. Enable the interrupt source by setting the appropriate bits in the control registers. For example, to enable the INT0 interrupt, you need to set the INT0 bit in the External Interrupt Mask Register (EIMSK), and also configure the interrupt sense control bits in the External Interrupt Control Register A (EICRA).

3. Enable global interrupts by setting the global interrupt enable bit in the Status Register (SREG), or by calling the sei() function.

Here is an example code that toggles an LED on pin PB5 whenever a button on pin PD2 is pressed:

#include <avr/io.h>
#include <avr/interrupt.h>

#define LED_PIN        PB5
#define BUTTON_PIN     PD2


ISR(INT0_vect) {
  // Toggle LED
  PORTB ^= (1 << LED_PIN);
}


int main(void) {

  // Set LED pin as output
  DDRB |= (1 << LED_PIN);

  // Set button pin as input with pull-up resistor
  DDRD &= ~(1 << BUTTON_PIN);
  PORTD |= (1 << BUTTON_PIN);

  // Enable INT0 interrupt on falling edge
  EIMSK |= (1 << INT0);
  EICRA |= (1 << ISC01);
  EICRA &= ~(1 << ISC00);

  // Enable global interrupts
  sei();

  // Main loop
  while (1) {
    // Do nothing
  }
}

Pin Change Interrupts on AVR

Pin change interrupts are a feature of AVR microcontrollers that allow you to trigger an interrupt routine when any of the pins on a port change their state. This is useful when you want to monitor multiple input pins without polling them in a loop. On ATmega328PB as an example, there are two types of pin interrupts: EXTINT (External Interrupts) and PCINT (Pin Change Interrupts) which is also external.

EXTINT has only two pins: INT0 on PD2 and INT1 on PD3. These interrupts are faster than PCINT since they have dedicated hardware and ISR vectors. Also, they can be set to only trigger on a falling edge or rising edge.

PCINT interrupts are not as fast as INT pins but they are many. They are separated into 3 groups, one group for each port.

Group 0 is on PORTB with PCINT[0:7]: PCINT0 is PB0, PCINT1 is on PB1... PCINT7 on PB7.

Group 1 belongs to PORTC including PCINT[8:14] pins.

Group 2 is on PORTD with interrupt pins PCINT[16:23].

Each group has its own interrupt vector that are named: PCINT0_vect, PCINT1_vect and PCINT2_vect. Since any pin on the port could trigger the interrupt, the application needs to keep track of pin states in order to know which pin changed and also verify, if needed, if the pin is High or Low.


Library Usage


Attaching a function (PCINT)

void pinIntAttachFunc(char port, pinIntCallback_t func, void* instance)

Attach a callback function per port that will be called by the ISR when one of the port pins will trigger an interrupt. Called function will receive the associated instance and an array with information about pin and port that caused the trigger.

port

The port to attach the callback function. E.g.: 'B', 'C'.

func

The callback function.

instance

In C++ use the 'this' keyword. In C use NULL

Usage

Create a custom function with an arbitrary name then attach the function to the interrupt. The function type is defined by pinIntCallback_t so first parameter must be a void pointer and the second one a pointer to an unsigned int array. In C++ the 'instance' will be the 'this' used when the function was attached.

void pinIntFunc(void* instance, uint8_t *trigger_pin) {
    char port = trigger_pin[INTPIN_PORT];
    uint8_t changedBits = trigger_pin[INTPIN_CHANGED_PINS];
    uint8_t portState = trigger_pin[INTPIN_PORT_PINS];
	
    if (port == 'B') {
        // Check if pin 5 has changed
        if (changedBits & (1 << 5)) {

            if (!(portState & (1 << 5))) {
                // Pin 5 of port B is LOW
            } 
        }
    } else if (port == 'C') {

    }
}
 
// Attach the callback function per port, passing NULL for C instance
pinIntAttachFunc('B', pinIntFunc, NULL);

The trigger_pin parameter can be accessed using this enumerator.

enum INTPIN_DESCRIPTOR {
    INTPIN_PORT = 0,
    INTPIN_CHANGED_PINS,
    INTPIN_PORT_PINS
};

INTPIN_PORT: the port that can be one of these characters: B, C, D, E. For EXTINT the port will always be 'I' to distinguish between interrupt types.

INTPIN_CHANGED_PINS: changed bits mask (bitmap of ALL pins that changed).

INTPIN_PORT_PINS: port pins (state of all interrupt pins on that port).


Attaching and detaching a pin (PCINT)

The attach function has the following purposes: enable internal pull-up resistor, sets PCMSKn register to trigger an interrupt on pin state change, activates interrupt for the group the pin belongs to, enable global interrupts.

void pinIntAttachPin(char port, uint8_t pin_number);
void pinIntDettachPin(char port, uint8_t pin_number);

Usage:

pinIntAttachPin('B', PB1);

In this example, the pin 1 on port B is set to trigger an interrupt.

On Class 1 AVRs (UPDI devices), the function takes one extra argument used to configure the interrupt pin such as enabling internal pull-up resistor or edge triggering mode. The internal pull-up resistor must be enabled by ORing the interrupt edge type with PORT_PULLUPEN_bm.

void pinIntAttachPin(char port, uint8_t pin_number, uint8_t pin_config)

Usage:

// Interrupt on falling edge with internal pull-up enabled on pin 2 port A
pinIntAttachPin('A', 2, PORT_ISC_FALLING_gc | PORT_PULLUPEN_bm);

Interrupt trigger modes:

PORT_ISC_BOTHEDGES_gc: Trigger interrupt on both rising and falling edges.

PORT_ISC_RISING_gc: Trigger interrupt on rising edge only.

PORT_ISC_FALLING_gc: Trigger interrupt on falling edge only.

PORT_ISC_LEVEL_gc: Trigger interrupt continuously while the pin is at a low level. 

Attaching a pin (EXTINT)

void extIntAttachPin(uint8_t int_num, uint8_t trigger_mode, pinIntCallback_t func, void* instance);

Attach a callback function to an EXTINT pin that will be called by the ISR when the pin will trigger an interrupt. Called function will receive the associated instance and an array with information about the pin. Sets the pins as inputs with internal pull-up resistors enabled.

int_num 

EXTINT pin number. Can be one of the following defines:  

  • EXT_INT_INT0
  • EXT_INT_INT1
  • EXT_INT_INT2 (if available).

trigger_mode 

The trigger mode.  Can be one of the following defines:

  • EXT_INT_LOW: interrupt triggers continuously in a loop as long as the associated interrupt pin is low. Usually used to wake the CPU from deep sleep where the falling or rising edges can't be detected.
  • EXT_INT_ANY: triggers on both rising and falling edges.
  • EXT_INT_FALLING: triggers on falling edge.
  • EXT_INT_RISING: triggers on rising edge.

func

The callback function.

instance

In C++ use the 'this' keyword. In C use NULL.

Detaching a pin (EXTINT)

void extIntDettachPin(uint8_t int_num)

Disable interrupt for specified interrupt pin.

C code example for PCINT

#include "pinInterrupt.h"

void pinIntFunc(void* instance, uint8_t *trigger_pin){
    char port = trigger_pin[INTPIN_PORT];
    uint8_t changedBits = trigger_pin[INTPIN_CHANGED_PINS];
    uint8_t portState = trigger_pin[INTPIN_PORT_PINS];
	
    if(port == 'B'){
        // Check if pin 5 has changed         if (changedBits & (1 << 5)) {
            if (!(portState & (1 << 5))) {
                // Pin 5 of port B is LOW
    
        

        }     }else if(port == 'E'){         // Check if pin 2 has changed         if (changedBits & (1 << 2)) {
            if (portState & (1 << 2)) {
                // Pin 2 of port E is HIGH
    
        

        }     } } int main(void){
    // Attach the callback function per port, passing NULL for C instance pinIntAttachFunc('B', pinIntFunc, NULL); pinIntAttachFunc('E', pinIntFunc, NULL); // Attach the individual hardware pins pinIntAttachPin('B', 5); // Using raw numbers or PB5/PE2 depending on io.h defines pinIntAttachPin('E', 2);     while(1){     }

    return 0; }

C++ example for PCINT

MySensor.h

#ifndef MYSENSOR_H
#define MYSENSOR_H

#include <stdint.h>

class MySensor {
public:
    MySensor(char port, uint8_t pin);
    void begin();
    
    // The actual Object-oriented handler
    void handleInterrupt(uint8_t* triggerPin);

private:
    char _port;
    uint8_t _pin;
};

#endif

MySensor.cpp

#include "MySensor.h"
#include "pinInterrupt.h"

// Global C++ static wrapper bridge function
void sensorCallbackBridge(void* instance, uint8_t* triggerPin) {
    if (instance != nullptr) {
        // Safe static cast back to our class object layout
        static_cast<MySensor*>(instance)->handleInterrupt(triggerPin);
    }
}

MySensor::MySensor(char port, uint8_t pin) : _port(port), _pin(pin) {}

void MySensor::begin() {
    // Pass 'this' as the instance parameter so the bridge knows who called it
    pinIntAttachFunc(_port, sensorCallbackBridge, this);
    pinIntAttachPin(_port, _pin);
}

void MySensor::handleInterrupt(uint8_t* triggerPin) {
    uint8_t changedBits = triggerPin[INTPIN_CHANGED_PINS];
    uint8_t portState = triggerPin[INTPIN_PORT_PINS];

    if (changedBits & (1 << _pin)) {
        if (portState & (1 << _pin)) {
            // This specific object instance detected a HIGH state on its assigned pin
        }
    }
}

main.cpp

#include "MySensor.h"

// Instantiate two distinct sensor objects on different pins
MySensor doorSensor('B', 5);      // Door switch on Port B, Pin 5
MySensor windowSensor('E', 2);    // Window switch on Port E, Pin 2

int main(void) {
    // Initialize global hardware or drivers here if needed

    // Start each sensor instance. 
    // This internally registers their unique 'this' pointers into the C library
    doorSensor.begin();
    windowSensor.begin();

    while (1) {
        // Your main application logic runs here.
        // The background interrupts handle the state logic 
        // and route events to the specific doorSensor or windowSensor object.
    }

    return 0;
}

C example for EXTINT

#include "pinInterrupt.h"

void extInterruptCallback(void* instance, uint8_t *trigger_pin) {
    char port = trigger_pin[INTPIN_PORT];
    uint8_t changedBits = trigger_pin[INTPIN_CHANGED_PINS];

    if (port == 'I') { // 'I' signals that an External Interrupt triggered
        // Check if INT0 caused the event
        if (changedBits & (1 << 0)) {
            // INT0 physical pin triggered!
        }
        // Check if INT1 caused the event
        if (changedBits & (1 << 1)) {
            // INT1 physical pin triggered!
        }
    }
}

int main(void) {
    // Attach, configure trigger type, and assign callback
    // Configures INT0 to trigger on ANY logical edge change
    extIntAttachPin(0, EXT_INT_ANY, extInterruptCallback, NULL);
    
    // Configures INT1 to trigger exclusively on a FALLING edge
    extIntAttachPin(1, EXT_INT_FALLING, extInterruptCallback, NULL);

    while(1) {
        // Main loop
    }
    return 0;
}



Download

v3.0
pinInterrupt.h
pinInterrupt.c
Changelog
v3.0 (20-07-2026) - Function pinIntAttachPin can now accept an extra argument on Class 1 AVRs used to configure the interrupt pin triggering edge and pull-up resistor.
v2.3 (12-07-2026) - Added extra preprocessor checks to exclude EXTINT for Class 1 MCUs.
v2.2 (28-06-2026) - Added support for a callback function for each port instead of a single callback. In C++ a 'this' instance can be associated with a callback function.
- Added support for EXTINT pins.
v2.1 (15-06-2026) - Added port E.
- Added support for UPDI devices.
- triggerPin now includes changedbits that maps all pins that changed.
- for loop inside the ISR was removing, letting the user to check what pin changed and the state of the pin, by using INTPIN_CHANGED_PINS and INTPIN_PORT_PINS. This reduces execution time and code size.
- Clear PCICR on pin detach if no other pins are used for interrupts.
- Disable pull-up resistors on pin detach.
- Fixed pinIntAttachPin bug by ORing PCICR.
v2.0 (02-01-2025) Simplified usage by using a function to attach an interrupt instead of defines.
v1.0 Release date 23, October, 2023

No comments:

Post a Comment