Adventures in Science: Level Up Your Arduino Code With External Interrupts

Let's continue digging deeper into Arduino to see what's really going on with registers.

Favorited Favorite 2

Last time we looked at Arduino code, we examined how registers worked in the ATmega328P. We continue peeking under the hood to see how to set up external interrupts using direct register writes.

An interrupt is anything that can stop the main execution thread of a program and cause the processor to perform some action before returning to that main thread. In the case of external interrupts, something like a button push can be configured to cause this brief pause to allow some other piece of code to run.

In the ATmega328P, program memory is divided up into three main sections:

  1. Interrupt Vector Table (IVT) --- Tells the processor where to jump to find the interrupt service routine (ISR) for each interrupt source (e.g., Port D, Pin 2 falling edge).
  2. Program Space --- This is where your main program lives. Any ISRs you write will also live here.
  3. Bootloader --- Optional section of code that runs first on a microcontroller that allows you to send new programs to it through nontraditional means (e.g., over the UART port instead of the ISP port). It can be configured to do other things, but being able to load programs over UART is what makes Arduino work so nicely.

When you give power to (or reset) your ATmega328P, the very first instruction that's loaded is at program memory address 0x0000. This just happens to be the RESET vector. Usually, the only instruction there is a jump command to another place in memory (usually 0 x 0034 --- the start of your program space). Your program begins executing sequentially from there. Part of the setup code might be telling the processor that you are open to an external interrupt when Port D, Pin 2 (also known as interrupt source "INT0") experiences a falling edge (logic HIGH to logic LOW).

Note that you generally need three things to happen to have the interrupt trigger:

  1. Global interrupts must be enabled (in AVR, this is accomplished with the sei() function). Note that global interrupts are enabled by default in Arduino.
  2. The individual interrupt must be enabled (usually by setting a bit in a particular register associated with that interrupt).
  3. The interrupt condition must be met (e.g., a falling edge on a particular pin).

Once all three of these conditions are met, the interrupt will trigger. Execution on your main program will stop, any state variables or numbers the processor was working on are saved to memory, and execution jumps to the IVT. Which entry in the table it moves to is based on which interrupt triggered. For example, a falling edge on Port D, Pin 2 is the INT0 interrupt, so execution will jump to address 0 x 0002 (see the Reset and Interrupt Vectors table in the ATmega328P datasheet).

Often, only a single instruction will reside at the interrupt address. This is normally a jump command to somewhere else in program memory, which is where the ISR resides. The memory location of the ISR is known as an "Interrupt Vector." So, our processor will jump to the ISR, perform whatever instructions it finds there, and then jump back to the main program to pick up where it left off (restoring any saved variables it might have previously stored).

How external interrupts work in an Arduino ATmega328p

External interrupts are great for responding to things like button pushes in a timely fashion or waking from a sleep state when a sensor has data to be read.

How else have you used interrupts? Why do you find them so important in microcontroller development?

Comments 15 comments

  • Thanks Shawn. To me, the main value of this series is for education. It helps to bridge the gap between the hobbyist community and the skills we need to teach students for industry. We need to show how microcontrollers work beyond the Arduino library so that students can go into industry using a wide variety of microcontroller families. Love the work you are doing!

    BTW it would also be nice to show how to use the pin change interrupts in addition to the 2 external interrupts. With a bit of code you can effectively make an interrupt on every pin using the pin change interrupts. I've use this Arduino library for that in the past I only mention it because with student projects they regularly like using more than just 2 pins as interrupts.

    • Thank you! I am hoping to help the people who want to move from the hobby level with Arduino to more product-level development. I've heard from several people who have worked with early startups; they always say "get rid of the Arduino and program the microcontroller directly."

      I'll keep the request for pin change interrupts in mind. As I'm covering interrupts, I'm really trying to just show one to get people started. Time permitting, I would like to cover more, but I know there are lots of interrupts available and many ways to use each one.

  • Very cool video! Why do you not have to de-bounce the button? Does the interrupt code not get called multiple times, due to the multiple flanks, at each button press?

    • The interrupt totally gets called multiple times due to contact bounce. However, I was purposely leaving it out in this video with the hopes that I might create a "debouncing" video in the future explaining the various ways one might debounce a button :)

  • Shawn,

    Another fine video, despite the "annoyingly long" 500mS delay (at about 5:00 into the video) turning into about 10 minutes due to "buffering"... :-(

    A few comments, though: First, back in the 70s when I was first studying interrupts, the "thought experiment" described was this: Imagine you're reading a book, and the doorbell rings (raising the interrupt). You grab your bookmark and put it into the book (so you can return to where you were), close the book, and go answer the door (processing the interrupt). After you run off the salesclown (possibly selling their weird religion), you close the door, and return to your chair, picking up the book and opening to where the bookmark is.

    Second comment is that if your switch is a, uh, "less expensive" one, you might experience "contact bounce", and so might get some weird results. (One way to deal with this is to look to see if "millis()" has changed more than some value (say 10) since the last time you changed the LED state, and if not, you ignore the switch and simply return from the interrupt. (You can also deal with the issue in hardware.)

    Third thought: Although the AVR can only handle one interrupt at a time, other processors (e.g., some of the PICs) can deal with "nested interrupts", with the return addresses being pushed onto the program stack. (One implication of this is you need to always make sure there's some "extra space" on the stack, especially if you're flirting with "full RAM usage".)

    Fourth thought: When you need to do more portable "register twiddling" code, make use of #ifdef blocks. Also, #define can be very useful.

    • I couldn't have asked for better timing on that delay :P

      The doorbell analogy is a good one, and definitely helps describe how one's state might be saved after the ISR. I'll have to remember it :)

      Definitely yes to debounce, but I'm hoping to make another video in the future talking about the various ways you can handle debounce (rather than try to give a partial answer in this video).

      You are correct in that the AVR can only handle one interrupt at a time. My understanding is that the I-bit is automatically cleared when entering into an ISR, which just means global interrupts are disabled. In theory, you could call sei() in an ISR to allow for nested interrupts--although that seems to go against good AVR programming practices :P

      I'm a big fan of #define and #ifdef. They're really helpful if you want to have a "timer configuration" section in code and manually set the registers depending on which architecture you might be using. It's a pain to keep up with changes when you start dealing with more than 3 architectures, though...

      • A couple of further thoughts this morning: I'm NOT an expert on AVR, and really haven't looked at the datasheet in any detail, but I am familiar with several other processors (dating back to Z80s), and it is fairly common to have a "Return From Interrupt" instruction that restores certain critical registers (such as PC and status), including the interrupt enable state. On processors that store this info on the stack, it's usually considered "good form" to only leave interrupts disabled in the ISR while doing things that are really critical, unless the ISR is VERY short (as is the one in the example you presented).

        On the dealing with multiple architectures, a useful idea is to put the handling of such things into separate files, and use a #include after the appropriate #ifdef. This has the added advantage of being able to "reuse" the file in other programs. (Just make sure that the files are appropriately documented, and I'd advise a naming scheme that has an obvious indication of which processor, such as "MyTimerConfig_AVR.h".)

  • This comment has me concerned "any state variables or numbers the processor was working on are saved to memory". Unless the Arduino has something under the hood that saves this information, the program is going to jump directly to the interrupt vector and then your ISR. Any saving of data must be preformed in your code, Even the status register must be preserved if your ISR alters it.

  • A few reminders for home gamers:

    • Writing directly to registers is a valid way to speed up your code, but at the cost of code portability (which is one of the benefits of using the specialized Arduino functions). i.e. your code is locked into the specific processor (family, or often specific model). It would require more porting effort to use the same code on a different platform even within the Arduino compatible range of processors. I'm not saying not to use direct register writing, but just be aware of the trade off of speed vs. portability.

    • Because the AVR processors can't handle an interrupt within an interrupt, the available functions that will work in an ISR is reduced. Any function that uses the timers won't work within an ISR. This includes anything that accesses the the UART (no debugging output to the serial console), and any delay functions. Probably other I/O functions. This was (and probably still is) a newby trap on the Arduino Forums back several years ago when I was active there.

    • Also note that the internal clock timer (used by delay, millis, and micros) doesn't increment during ISR handling on the AVR. So this is further reason to keep your ISRs as short as possible.

    When I have a project connected to an external RTC (either Chronodot style or GPS) that has a PPS (Pulse Per Second) line on it, I often attach the PPS to an interrupt. I use the set a flag method and then any operation that I want to take place at any integer multiple of a second will get reliably fired every second without relying on the non-precise timer within the AVR.

    • All good points, so thank you for these! I would like to dig into it further, but I believe you can set up nested interrupts on an AVR. I do see how that could lead to complications, though, and why you probably shouldn't. :)

      • Generally, from what I remember with out MCU types, the best way to handle a case where you might have multiple interrupts is to MUX the IRQ and then have it check what type of interrupt it is via port or code/data. This way you just build a LUT of data instead of over complicate the matter.

      • I haven't tried this myself, but the people on the Arduino forums who know much more than I do about the AVR interrupt systems advise that the interrupt system only has a single interrupt buffer.

        For example, lets say you have 3 types of interrupts configured called A, B, and C. If Interrupt A is being serviced and interrupt B is triggered, once interrupt A is finished being serviced, interrupt B will be serviced. But, if while interrupt A is being serviced and interrupt B and then interrupt C are triggered, only one of the two will be serviced once the ISR for A exits. I'm not clear on if the most recent trigger will be remembered, or if only the first trigger will be remembered. (So in my case above I'm not sure if B or C will be serviced, but I've been lead to understand that only one of them will be serviced.)

        • Using Reddit instead of the datasheet is a bad idea. There is not really an "interrupt buffer"; there are, however, event triggered interrupt flags for each peripheral which will pend until they are handled. So assuming interrupts A, B, and C are event triggered interrupts they will all be remembered and handled. Another type of interrupt is level interrupts, which will only trigger if interrupts are enabled at the time the interrupt signal is present; so a level interrupt will not be remembered and will not trigger if another interrupt is in progress.

          • As I said, I didn't try it out myself so I admit that I don't fully understand them... "Buffer" is probably the wrong word for a single bit flag. But the effect can be similar to a buffer if something else toggles the flag and it maintains that state until the process looks at the value.

            That said, thank you for your explanation.

            Also, the forums that I were referring to wasn't Reddit... I was referring to the "official" Arduino forums on Granted, all the people posting there are community volunteers. But, the people that were talking about interrupts with authority have all a history in the forums of knowing what they are talking about as they assist those of us who don't know as much. Specifically this forum:

Related Posts

Recent Posts


All Tags