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.
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?
- How to use interrupts
- Pin Change Interrupts on AVR
- Library Usage
- Attaching a function (PCINT)
- Attaching and detaching a pin (PCINT)
- Attaching a pin (EXTINT)
- Detaching a pin (EXTINT)
- C code example for PCINT
- C++ example for PCINT
- C example for EXTINT
- Download
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