Monday, October 23, 2023

Pin interrupt library for AVR microcontrollers

This library can help to easily configure a pin interrupt on AVR microcontrollers without the need to check the datasheet every time. At the moment only PCINT pins are supported.

Pin interrupt library for AVR devices

In this blog post, I will explain how to use interrupts on AVR microcontrollers. Interrupts are a powerful feature that allow the microcontroller 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.

What are interrupts?

An interrupt is a signal that causes the microcontroller 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?

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.

There are also PCINT on port E but I couldn't find in the datasheet a register for them. If you know more, leave a comment.


Library Usage

Defining the pins

Inside the pinInterrupt.h file define the pin(s) that you want to enable interrupt for.

// PCINT Group 0 (PORTB - PCINT[0:7])
#define PCINT_PINS_ON_PORTB			1		// 0 or 1 (true or false) if there are used pins in this group
#define PCINT_PINS_0				(1 << PCINT0)	// what pins are used. E.g: ((1 << PCINT0) | (1 << PCINT1))

// PCINT Group 1 (PORTC - PCINT[8:14])
#define PCINT_PINS_ON_PORTC			0		// 0 or 1 (true or false) if there are used pins in this group
#define PCINT_PINS_1				(1 << PCINT8)	// what pins are used. E.g: ((1 << PCINT10) | (1 << PCINT14))

// PCINT Group 2 (PORTD - PCINT[16:23])
#define PCINT_PINS_ON_PORTD			0		// 0 or 1 (true or false) if there are used pins in this group
#define PCINT_PINS_2				(1 << PCINT16)	// what pins are used. E.g: ((1 << PCINT16) | (1 << PCINT17))

These settings could be set using function arguments but as I will show later on, the code inside the ISR still needs to be manually modified according to your needs. I could have added a function pointer for each pin such as 'attachInterrupt()' but that would require lots of function pointers increasing code size needlessly. Using macros on the other hand, the code is included only for the necessary pins and also allows for greater flexibility.

Setup Function

The setup function has the following purposes: set the pins as inputs with their internal pull-up resistors enabled, sets PCMSKn register to trigger an interrupt on pin state change, activates interrupt for the group the pin belongs to, enable global interrupts.

void pinInterruptSetup(void)

Interrupt Vectors

In the file pinInterrupt.c there are three ISRs, one for each PCINT group. Here is the ISR for group 0:

ISR(PCINT0_vect){
    uint8_t portB = PINB;
    uint8_t changedbits = portB ^ group0_history;
    group0_history = portB;

    if(changedbits & (1 << PB0)){
        // PCINT0 changed
	if(portB & (1 << PB0)){
	    // Pin is High
	}else{
	    // Pin is Low
	}
    }
}

First the state of the port pins is saved in a local variable for faster access and to ensure that the pin state is consistent in case it changes in the meantime.

Each bit of the variable changedbits represents a pin on that port. If the bit for that pin is 1 that means the pin changed state. The pin could be High or Low but the bit will be 1 in both cases since it indicates a state change and not a logical state. Here is an online service that you can use to see how bits change using the bitwise exclusive XOR ^ bitwisecmd.com.

Next the state of port pins is saved in a global variable named group0_history. Below this line is where you can add/edit the code according to your needs.

Check what pin changed state:


if(changedbits & (1 << PB0))

PB0 should be replaced with the desired pin.

Check if pin is High or Low:


if(portB & (1 << PB0)){
    // Pin is High
}else{
    // Pin is Low
}

Here is where you can place code to run when the pin is High or Low. 

To add more pins, simply duplicate this block of code and replace PB0 with PB1...PB7.



Download

v1.0 pinInterrupt.h
v1.0 pinInterrupt.c
Changelog
v1.0 release date 23, October, 2023

No comments:

Post a Comment