Using Interrupts on AVRs and AVR-based Arduinos


I haven't spoken to my wife in years. I didn't want to interrupt her.
Rodney Dangerfield

20220529

Excuse Me Sorry to interrupt, but...

These notes and mini-projects are some mid-level pointers on how to use interrupts in projects. Much of the information is useful in general, but it is all written to work on AVR processors, specifically Arduinos that are based on AVRs. More specifically, the ATmega328. That includes the Nano and Uno. That gives us a common base to work from that is perhaps the most popular setup so we can be specific.

This isn't a beginners' tutorial. If you are reading this I expect you already know what interrupts are and why you want them. The techniques I will talk about are intermediate to advanced. Some are common. Some not so common or obvious. All are useful in some context.

Although the code shown will be for Arduinos, it is important to remember that Arduino uses the same GCC and AVR LIBC used by many AVR projects. Arduino hides a lot of that under the hood, but it isn't hard to open the hood and peek inside. That's exactly what we will do to get access to what we need. And of course it is the same AVR processor, so most of the code will transfer easily.

Since we will be using AVR LIBC, you'll need to look at some of its documentation. Here is the main page for AVR Libc.

Let's get started! If you have questions, feel free to interrupt :-)

Periodic Timer Interrupts

It is often useful to have an interrupt at some periodic rate. The Arduino uses this for the millis() function to count time. It is also used in preemptive multi-tasking operating systems. But it can be used for much more. One example is to update and/or multiplex a display. Another is controlling stepper motors at some rate. I published an article in Servo magazine a couple of years ago with a fairly sophisticated program to do that. It can even be used to create a pulse width modulated signal at relatively low frequencies.

Typically this is accomplished by using a timer within the microcontroller to cause an interrupt repeatedly. The ATmega328 has a total of three timers and the Arduino uses one of them (timer 0) to cause an interrupt approximately 976 times per second. Why 976 and not 1000? The timers have fixed resolutions and capabilities. Timer 0 is an 8 bit counter and has a prescaler that can be set to divide by 1, 8, 64, or 1024. The crystal oscillator clock on the Arduino runs at 16 Mhz. In the Arduino system, the prescaler is set to divide by 64 which then feeds 16,000,000 / 64 = 250,000 Hz into the counter. Since the counter is 8 bits, it "rolls over" after 256 input clocks. The counter is set to deliver an interrupt each time the counter rolls over. So 250,000/256 = 976.5625 Hz. That's where we get 976 interrupts per second.

Since the Arduino is already using this timer and has interrupts firing at close to 1000 times per second, it would really be nice if we could "tap into" that interrupt and use it for our own purposes. Alas, the people who created the Arduino system made no provisions for that. In my investigations I've found no way to do that directly without actually modifying the underlying Arduino code. Not a fun task and not a good idea. But! All is not lost! Timer 0 is pretty sophisticated. It can not only generate interrupts when it rolls over, but it also has two "output compare registers" that can be set to any value between 0 and 255 and cause an interrupt when the timer reaches that value. The rollover interrupts will still happen, keeping millis() happy, and we can use the second interrupt for our own purposes. We can, if we want, use BOTH output compare interrupts. There are lots of neat tricks we can play by doing that, but let's keep it simple for now.

So, how do we do it? The AVR Libc interrupt documentation tells us most of what we need to know and the ATmega328 datasheet tells us the rest. The first thing we have to do is create an ISR -- Interrupt Server Routine -- to handle the interrupt when it happens. That's pretty simple. The AVR Libc interrupt page shows us how. They have created some macros that make it simple. Using the macro we create a function that looks very much like a regular function:

      ISR(TIMER0_COMPA_vect)
      {
      // do our interrupt stuff here
      }

    
The first line is really the only thing different from a regular function. ISRs never have a return type, not even void. Instead of a function name we use the name "ISR" and the only "parameter" is the name of the interrupt we want it to act on. The AVR Libc interrupt page gives the correct names for all the interrupts. It's a good idea to familiarize yourself with it.

When writing ISRs there are a few things to keep in mind. First, keep the ISR short and fast. Never do anything inside the ISR that can be done outside the ISR. You should also try to avoid calling other library routines, especially ones that can take a long time. In some cases, calling some other function can cause strange results and even a crash. And remember that the rest of the program doesn't know your ISR is doing stuff behind its back, so accessing data can be problematic. There are ways to work around all of that, of course, and we will have more to say about it later. For now, simple is key.

Let's create an interrupt routine that blinks an LED in the background while our main program runs. We will make it simple; all it will do is blink about once per second. But our main program will continue running without having to worry about it.

First let's talk about timer 0, its capabilities, and how the Arduino system uses it. Timer 0 is an 8 bit counter/timer. That means it can count from 0 to 255 (or 255 to 0) and then start over. As implemented in the ATmega328 (as well as many other AVRs) it has many capabilities. We will focus only on what is relevant to us. Since the Arduino uses the timer internally we are limited in what we can do with it. We need to tap into its power without disturbing its current use.

The timer has a prescaler that can be set to 1, 8, 64, 256, or 1024. It is set to divide by 64 in the Arduino Nano and Uno. Since those boards have a 16 Mhz clock that means the timer is clocked by (16,000,000 / 64) = 250,000 Hz clock. The timer has a cycle of 256 (0 to 255) and will roll over to 0 every 256 clocks. The Arduino uses it to interrupt every time it rolls over to 0, so 250,000 / 256 gives us about 977 interrupts per second. That interrupt counts up the value returned for the millis() function since it happens close to every millisecond. As a side note, by reading the actual value of the counter the system can give you a value for micros() since each count happens approximately every 4 microseconds.

Timer 0 has three different interrupt sources we can use. The rollover interrupt is the one used by millis() and unavailable to us. That leaves the Comparator A Match and Comparator B Match interrupts. We'll use Comparator A. The timer itself is already programmed to roll over about once each millisecond and uses the rollover interrupt for millis(). So all we have to do is set up the conditions for the comparator A to interrupt and create the ISR. The ATmega328 datasheet is our friend for setting up the interrupt. I've already done the dirty work, so here is the code we need:

      void enableCompareAInterrupt(BYTE when)
      {
        OCR0A = when;  // Output Compare Register 0 A set.
        TIMSK |= 2;    // Turn on the OCR0A interrupt.
      }
    
The comparator interrupt can be set to interrupt when the counter is at any value. It already interrupts for millis() when it is at 0. There are a lot of reasons we may have for choosing some particular value, so we make our function able to take a parameter for it and set the comparator to that value. The "BYTE when" is the parameter and we put that value into the OCR0A (Output Compare Register A for timer 0). The second line of the function turns on the interrupt.

Now we need the ISR itself. The AVR Libc interrupt page is where we go to find the interrupt vector name we need. It is unfortunate that there is little standardization for the names. In many cases very similar processors with almost identical interrupt sources will use different names for the interrupt. Alas, we just look it up and move on. (If you dig into the Arduino provided code that has to work on several different processors, you will see what pain that causes.) For us we need the TIMER0_COMPA_vect. Here is our ISR:

     ISR(TIMER0_COMPA_vect)
     {
       static byte on = 0;
       static int skip = SKIPVALUE;

       skip--;            // skip tells us to skip interrupts to slow down blink rate
       if(skip != 0)      // We decrement it and only run the rest of the ISR if it is 
       {                  // is zero.
         return;
       }
       skip = SKIPVALUE;       // When it is zero, we reset it and run the rest.
  
       if(on == 0)
       {
         // if LED is off, turn it on
         PORTB |= (1<<5);
         on = 1;
      
       }
       else
       {
         // If LED is on, turn it off
         PORTB &= ~(1<<5);
         on = 0;
       }
     }
    
The variable "on" just keeps track of whether the LED is currently on or off. Since the interrupt happens about 1000 times per second, that would be be way to fast for our eyes to see if we turned it off or on every time. It would flash 500 times per second. The variable "skip" lets us skip some interrupts to get a good flash rate. We first decrement it and if it isn't zero we exit. If it is zero, we reset it and continue with the interrupt code.

Notice we are not using the digitalWrite() function in the ISR. Remember I said to try to avoid calling most functions from inside your ISR. Some functions are safe, others not. Calling a function that isn't written specifically to be called from within an ISR is dangerous. You never know when an interrupt is going to happen. The function you are trying to call may be in the middle of running because it was called from some other place in your program. Depending on what it does and how it could cause Really Bad Things to happen. Unless you KNOW it is safe, it is best to avoid outside functions.

In addition to that, the Arduino digitalWrite() (and read) family of functions is s-l-o-w. Exactly what we want to avoid in an ISR. Using direct port I/O is much faster. Anytime you want to do things quickly (e.g. in an ISR) you want to use direct port I/O instead of the Arduino functions. It is also much more versatile. If you want to change more than one pin or read more than one pin in a port, it is much easier accessing it directly.

Tha loop() function just prints a number to the console once a second or so to let you know it is still running your program while the ISR blinks the LED. It also shows you that delay() still works properly. You can change the #define SKIPVALUE to change the blink rate.

And that's it. Not too bad. Interrupt programming can be full of surprises. But one of the absolute best ways to keep from getting surprised is to stick to a few simple rules and keep the ISR as short and simple as possible. In later projects we will cover some different problems and ways to handle them.

Periodic Timer Pulse Width Modulation

Links

Under normal circumstances we expect our program to go along its merry way doing its thing, whatever that may be, until it decides to do something else. But in some cases it may be inconvenient for the program to decide exactly what it should be doing. Say, for instance, we want to detect when an input pin changes from high to low so we can take some action. We can read that pin and test its value ever so often. But that takes away from processing whatever else we are doing as well as cluttering up the code with things that are likely irrelevant. It also may not be fast enough. If that input pin going low means a motor has reached its destination we may need to stop it within 20 microseconds or so to keep from damaging the mechanism. It would be almost impossible to do that by testing the input pin all the time. This method is called "polling" and in some cases is the best method. But there is at least one other that is often superior.

driving a nail under construction...



© 2022 William R Cooke