AVR basics: interrupts

      No Comments on AVR basics: interrupts

[As I may have mentioned somewhere, this blog is about my journey through the worlds of electronics, robotics and retro computing. I’m not an expert in any of these. In fact, I’m learning as I go, and this site is my way of sharing what I’ve learned. So this is the first post in yet another occasional series detailing my personal triumphs and minor epiphanies as I figure out how stuff works.]

I confess I’m really enjoying delving into the secrets of AVR microprocessors. Having used Arduinos for some time now – as well as other AVR-based boards such as the mighty and wonderful Teensy – I came to the conclusion that I’m a master at µproc hacking. Working with ‘raw’ AVR processors quickly disabused me of that notion.

The Arduino ecosphere shields you from a lot of what’s going on. Its programming environment and rich collection of libraries mean you can get stuff done easily and quickly.

When you’re toiling away with the AVR in its native state, though, you find you have to do a lot more work just to enable features you take for granted on the Arduino. And yet, being forced to confront registers and clocks and other low-level stuff also instils a sense of power and possibility. And in burrowing so deeply into the inner workings of the microprocessor, you learn there’s so much more you can do – and that you can often do it a lot faster than you can in the conventional Arduino world.

Interruptions

Using interrupts is quite easy on the AVR – at least at the basic level we’ll be addressing here. Actually, interrupts are complex and powerful things, especially when combined with timers. For our purposes today, though, we’ll deal just with simple ‘pin change’ interrupts. And once you’ve employed them once, you’ll start seeing so many uses for them.

By the way, all the AVR stuff I’ll be doing here is based on 8-bit chips – primarily my chip of choice, the ATMEGA328P (the microprocessor at the heart of the Arduino/Genuino Uno). Most of what I cover will apply to other AVR chips, but you may need to refer to the data sheets to be sure. So, unless I say otherwise, assume that I’m talking about the ATMEGA328P.

Also, I work in Atmel Studio as my IDE (I run a Windows 10 VM on my Macs primarily for this purpose). I’m assuming you’ll be using something similar in which you have access to all the standard AVR libraries. My code always starts with:

#include <avr/io.h>

Interrupt types

There are essentially three different types of interrupt on the AVR.

  • Internal interrupts are triggered by changes in the AVR’s internal hardware.
  • INT0/INT1 external interrupts – these are high-priority interrupts tied to specific pins. On the ATMEGA328P, for example, these interrupts work via pins PD2 and PD3. These interrupts are very flexible – you can, for example, configure them to trigger on a rising edge, a falling edge, both and other conditions. These are the interrupts that are serviced first whenever more than one interrupt is triggered at the same time.
  • Pin Change interrupts. Any pins can be configured as PC interrupts. But they’re much simpler than the INT0/INT1 interrupts – they’re triggered by any change on the configured pins. This is the kind we’re going to deal with here.

Above is the pin-out of the ATMEGA88/168/328 family. Note that each pin (PB0, PC1 etc) also has a separate interrupt name. For example, PC3 is also known as PCINT11. These labels – defined as macros by the standard AVR header files – are pretty much interchangeable; which one you use in your code is dependent on context. If you’re using the pin to trigger interrupts, use the PCINT11 label. If you’re using it as a general-purpose I/O (GPIO) pin, use PC3.

Why an interrupt?

To show you how to use a simple pin change interrupt, let’s take, as an example, the project I’m working on at the moment. It’s a tool designed to show the state of either a 16-bit address bus or an 8-bit data bus in a 4-digit, 7-segment display. Which bus it monitors will be decided via a switch. In the code, I originally just had the AVR poll the state of the switch each time it went through the main event loop.

But that’s actually rather wasteful as the switch state will change fairly infrequently (at least, in the kind of timescales measured by a processor). I figured this is a perfect application for an interrupt: attach the switch to a pin and just run a routine to check the switch state when the state of the pin changes.

In my case, the switch is attached to pin PC3 which, as we’ve already established, is also known as PCINT11.

Getting set up

The PC interrupts are grouped in the same way as the GPIO pins themselves. The ATMEGA328 has three ports – B, C and D. Each of these has interrupt registers and control bits associated with it.

There is an overall Pine Change Interrupt Control Register (PCICR) that controls which groups of interrupts is enabled. We need to enable the interrupts associated with Port C so we need to set the Pin Change Interrupt Enable (PCIE) bit associated with Port C. Alas, instead of giving this bit the macro name of PCIEC, which would have been logical, Atmel decided to go for PCIE1. Here’s how the pins, ports etc match on up the ATMEGA328.

Port GPIO PINS INTERRUPT PINS NAME PC INT ENABLE PC MASK REG VECTOR
 PORTB  PB0…PB7  PCINT0…PCINT7  INT0  PCIE0  PCMSK0  PCINT0_vect
 PORTC  PC0…PC6  PCINT8…PCINT14  INT1  PCIE1  PCMSK1  PCINT1_vect
 PORTD  PD0…PD7  PCINT16…PCINT23  INT2  PCIE2  PCMSK2  PCINT2_vect

So, to enable PC interrupts on Port C, we set the PCIE1 bit in the PCICR register, thus:

PCICR |= (1 << PCIE1);

This means we can now use any of the Port C pins as interrupt pins.

To tell the processor specifically which pins in that port we want to use, we have to set the Pin Change Mask (PCMSK) register. There are three of these – one for each port. The one for Port C is PCMSK1. To select which pin(s) you want to make ‘active’, you OR this mask with the macro name of the chip. You can use the GPIO pin’s regular name (eg, PC3) or its PCINT name (PCINT11 in this case). So we do this:

PCMSK1 |= (1 << PCINT11);

That’s it for telling the AVR which pins you want to act as interrupts. There’s one final step to carry out to actually make them active, and that’s to call sei() – Set Enable Interrupt – which switches all interrupts on. If you want to turn them off again, call cli() – CLear enable Interrupt.

Calling sei() can be done anywhere in your code before you start needing interrupts. A common approach is to create an interrupt enable function. There’s a good case for calling sei() as late as possible, after you’ve done all your other setting up stuff and just before you enter the main event loop.

So, for our simple example, the interrupt initialisation function looks like this:

void initInterrupt(void)
{
    PCICR |= (1 << PCIE1);     // Enable interrupts on Port C
    PCMSK1 |= (1 << PCINT11);  // Make PCINT11 active
    sei();                     // Set enable interrupts - we're ready to go
}

We call this from main() before entering the event loop. Now, whenever that pin transitions from either high to low or low to high, the interrupt is triggered. Which is all very well, but what do you do about that?

Responding to the interrupt

When an interrupt is triggered, the program looks for an appropriate Interrupt Service Routine (ISR) function. Normal program execution is frozen, with variables preserved. The ISR() function is executed and then the normal program resumes operating from where it left off.

ISR functions are a little special. They return nothing and so have no return type – not even ‘void’. And they take one parameter – a vector name identifying the port for which this function handles interrupts. The vector name is a macro just like the pin names, defined by the AVR headers. As you can see from the table above, as we’re using a pin in Port C, so the appropriate vector name is PCINT1_vect. That means the ISR() function that handles our interrupt will look something like this:

ISR (PCINT1_vect)  // respond to interrupts on Port C pins
{
    // do whatever you want to do in response to the interrupt here
}

As soon as the program enters an ISR function, all interrupts are automatically disabled, allowing the function to get on with its duties untroubled. Interrupts are automatically turned on again immediately before the function exits. If you use a lot of interrupts, it’s a good idea to keep whatever happens inside the function simple and brief.

No doubt you’ll have noticed that the definition of the ISR function only identifies which port it’s handling, not which pin. If you’ve enabled more than one pin from a single port to use interrupts you’ll need to include code inside the ISR() function to poll the pins and work out which one called the interrupt. If your program needs only two or three interrupt pins, it’s better and easier to select one from each port.

And that’s essentially that. There’s a lot more to interrupts, but hopefully this will get you started.

Leave a Reply

Your email address will not be published. Required fields are marked *