Embed With Elliot: Interrupts, The Good…

What’s the biggest difference between writing code for your big computer and a microcontroller? OK, the memory and limited resources, sure. But we were thinking more about the need to directly interface with hardware. And for that purpose, one of the most useful, and naturally also dangerous, tools in your embedded toolchest is the interrupt.

Interrupts do exactly what it sounds like they do — they interrupt the normal flow of your program’s operation when something happens — and run another chunk of code (an interrupt service routine, or ISR) instead. When the ISR is done, the microcontroller picks up exactly where it left off in your main flow.

Say you’ve tied your microcontroller to an accelerometer, and that accelerometer has a “data ready” pin that is set high when it has a new sample ready to read. You can wire that pin to an input on the microcontroller that’s interrupt-capable, write an ISR to handle the accelerometer data, and configure the microcontroller’s interrupt system to run that code when the accelerometer has new data ready. And from then on everything accelerometer-related happens automagically! (In theory.)

This is the first part of a three-part series: Interrupts, the Good, the Bad, and the Ugly. In this column, we’ll focus on how interrupts work and how to get the most out of them: The Good. The second column will deal with the hazards of heavyweight interrupt routines, priority mismatches, and main loop starvation: the Bad side of interrupts. Finally, we’ll cover some of the downright tricky bugs that can crop up when using interrupts, mainly due to a failure of atomicity, that can result in logical failures and corrupted data; that’s certainly Ugly.

The Good Side of Interrupts

Interrupts allow your microcontroller to fire off specific “subroutines” when various events happen. You can use an interrupt almost any time that you’d like some code to automatically and speedily react to an event. All sorts of peripherals, from accelerometers to radios, SD cards to GPS modules, take a bit of time to do whatever it is that they do. Waiting around for the peripheral to be ready (or finished), and then acting, is a surprisingly important embedded programming task, and interrupts are our favorite way to handle these peripheral-generated events and still leave the CPU free to do other stuff in between.

To make this clearer, let’s compare interrupts to the other options. Imagine, for concreteness, that we’re waiting for an accelerometer to present us with a new data sample. Suppose additionally that the accelerometer will signal that it’s got a new sample by raising the logic voltage on a wire.

One possibility is to sit and wait until the ready signal comes in. This is the “blocking wait” approach.

int main(void){
	while(1){
		do_stuff();
		while(line_is_low()){ /* wait here                */
			;                 /* doing nothing            */
		}                     /* until the line goes high */
		handle_new_data();
		carry_on();
	}
}

Another possibility is to keep doing other stuff, but periodically check in on the state of the signal line whenever we get the chance. This is “polling“.

int main(void){
	while(1){
		do_stuff_repeatedly();
		if (line_is_high()){  /* ready yet? */
			handle_new_data();
		}
	}
}

And finally, we can set up the microcontroller’s interrupt handler to trigger when the line goes high, and write an ISR to handle the new data when it does. (The initialization and ISR definition code are specific to the AVR, but the basic ideas aren’t.)

ISR(INT0_vect){
	handle_new_data();
}

int main(void){

	EICRA |= (1<<INT0);                 /* enable individual interrupt             */
	sei();                              /* enable global interrupts                */

	while(1){
		always_do_stuff();
	}
}

As you can see with the interrupt code, there’s a bit of additional configuration burden that will be different across microcontrollers, but then after that there’s no sign of the interrupt in the main while loop. The code itself is defined outside of the main code in what is known as an Interrupt Service Routine. It’s not at all obvious when the ISR is going to run; it runs when it’s triggered.

Blocking v. Polling v. Interrupting

Let’s compare the three approaches. Blocking responds fastest to a new signal, but your micro can’t do anything else until the signal arrives. It’s a reasonable choice when the extremely fast response time is absolutely needed, but only then. What happens if the signal never comes? If a wire gets unplugged or the peripheral’s battery dies? Doesn’t your CPU have better things to do? As a rule of thumb, don’t block unless you really need to or are a really lazy coder working on a non-demanding project. (Who, us?! Sometimes.)

Polling the signal line is viable if the response time isn’t critical, because the time it takes to notice the changed state depends on where the code is inside the do_stuff_continually() function when the signal comes in. On average, the response time will be one-half of the time it takes to do_stuff_continually(), but the worst case is the full run time.

Interrupts combine nearly the best of both worlds without their drawbacks. The response time to the incoming signal won’t be instant, as it is with the blocking wait code, but it’s usually pretty fast: around 20 clock cycles for the AVR and MSP430. (The ARM Cortex chips go to elaborate extents to minimize and regularize interrupt switching times.) Most importantly, though, the rest of your code can execute normally while waiting for the interrupt to occur. It’s very rare that you have an event with such critical timing that it’s worth doing nothing just to wait for it, so interrupts beat blocking almost all the time.

Compared to polling, the response time of an interrupt is very consistent (usually within a cycle or two) and faster than all but the most trivial polling cases. Interrupts, if triggered in the middle of your polling code, will take priority. This means that polling is most suitable for low-priority tasks where you don’t care about the timing. But of course every system has tasks like this; handling things that happen on a human time-scale (within tens of milliseconds) is usually fine in polled code, and it frees up your interrupts to handle the really important stuff.

So in summary: use interrupts for the stuff where timing and consistency matters, especially for communicating with other machines, and don’t hesitate to poll for the events that aren’t critical.

Under the Hood: How Do Interrupts Work?

If you’re going to start using interrupts, it’s worth poking a little bit into how they are actually implemented inside the microcontroller. It’ll give you some important insights into the deeper behaviors of the system.

Each possible event that can trigger an interrupt has a flag (a bit in a special memory register) dedicated to it. Most microcontrollers also have an enable bit for each possible interrupt vector, and additionally a global interrupt enable flag that covers all of the interrupts at once. When an event happens, its flag is set. If the specific interrupt enable flag that corresponds to that event and the global interrupt enable flag are also all set, then the interrupt is good to go! If any of these three conditions fail, naturally, nothing happens.

Notice that this system means that if an event has already happened, and its flag is set, when the global and specific enable bits are set, that interrupt will fire off immediately. As a corollary of this condition, imagine that you’ve enabled a bunch of specific interrupts, but disabled the global interrupt bit. This can happen, for instance, when processing an ISR if you don’t want your interrupts to be interrupted. Anyway, by the time you finally set the global interrupt enable again, many events have already happened. Now the machine is looking at many interrupts to process. Which does it choose first?

When any number of interrupts get triggered at once, AVR and MSP430 microcontrollers have a simple fixed priority system. For instance, on an AVR the external pin interrupts have higher priority, then the timer/counters, and then the USART (serial port). Each of these is handled in turn, moving on to the next-highest priority as the first finishes. If yet-higher priority interrupts happen in the meantime, those get taken care of at the next chance. You can look up the priorities in the datasheets when it matters.

On an ARM system, the priority system is significantly more complicated. The nested vectored interrupts have not one, but two sets of priority levels, and each can be assigned to any interrupt event. The top level controls which interrupts can interrupt each other, and the lower level selects which interrupt from within a given top-level priority group gets executed first. The nested priority systems that you can create with this are mind-blowing, and well beyond the scope of this article. Suffice it to say that if you’ve got two USARTs on a system and you want either USART to interrupt the SPI ISR but not each other, and you’d like USART0 to always get handled before USART1 for instance, you can do it.

Finally, on an AVR or MSP430, each possible interrupt has a fixed location in code memory where it goes to find an instruction to run when that interrupt is triggered. Each of these locations contains a jump command with an address: the memory address of the routine that you’d like associated with the interrupt. By default, this jump table (link) is populated with commands that go nowhere or return the chip to its just-reset state. But by putting the address of some code that you’d like called in just the right place in memory, the chip will start executing that code when the interrupt is triggered.

Other Stupid Interrupt Tricks

We can’t leave the “Good” of interrupts section without mentioning two particular patterns that we use in almost every project with an AVR microcontroller.

Most microcontrollers have a timer/counter type peripheral or two or three. These simply count upwards at a given speed, independent of the CPU. Configuring an interrupt to fire off when a counter overflows or reaches a certain value, and then incrementing a global “system tick” variable inside the ISR provides a very convenient variable timebase that can be slowed down to nearly-human timescales if you wish. For instance, that’s what the Arduino does with millis(). (ST’s ARM chips, for instance, have a dedicated system tick clock, which is super sexy.)

For an AVR, configuring a counter as a system tick clock could be as easy as:

volatile uint16_t ticks;

ISR(TIMER0_OVF_vect){
	++ticks;
}

void initTicks(void){
	TCCR0B |= ((1<<CS02) | (1<<CS00)); /* set largest clock divisor */
	TIMSK0 |= ((1<<TOIE0)); /* enable overflow interrupt */
	sei(); /* enable global interrupts */
}

(Dealing with the ticks variable isn’t entirely straightforward, and we’ll cover why that’s so in the “Ugly” installment.)

Interrupts are also a great way to wake up a microcontroller from a power-saving sleep mode. When the CPU isn’t doing anything, you can reduce its power consumption down to a mere trickle (a couple microamps, or even fractions of a microamp for the MSP430) by turning off the CPU clocks and putting the system to sleep. But then how do you wake the chip back up when something happens? Interrupts, naturally.

On the AVR, again, you can even define empty ISRs that don’t do anything, but are used explicitly for the purpose of returning from sleep mode.

EMPTY_INTERRUPT(INT0_vect);         /* instead of ISR()                */

void sleep(void){
	EIMSK |= (1 << INT0);           /* set individual interrupt enable */
	sei();                          /* enable global interrupts        */
	set_sleep_mode(SLEEP_MODE_PWR_DOWN);
	sleep_mode();                   /* now to sleep                    */
}

The sleep() function above makes sure that both the individual interrupt and global interrupt bits are set, tells the processor which sleep mode to enter, and then shuts down. When the external button (attached to INT0) is pressed, the do-nothing ISR is called, the chip wakes up, and the flow of the program returns to the command just following the call to sleep().

Conclusion

And there you have it. Interrupts are great for responding to external devices, creating timebases, waking the chip up from power-save modes, and more. We hope that going through how interrupts work and what they’re good for will help you to think more like the microcontroller, and find the right places to put them into your projects.

Working with interrupts takes the code flow outside of the linear, one-function-after-another main loop that you’ve written. This comes with scheduling issues and some serious potential for glitches, and these are going to be the subject of the next two columns, where we cover the Bad and the Ugly side of using interrupts. Stay tuned, and let us know what you think in the comments.

32 thoughts on “Embed With Elliot: Interrupts, The Good…

    1. “Recent processors have interrupts too.”
      What do you mean?
      interrupt on microprocessor have always existed. The 6502, 8080, z80, etc had it. And obviously recent CPU have it too. If refering to general purpose CPU used in phones, tablet and PC, only the kernel can access the interrupts, from userland only a syscall() can access low level resources like interrupts.

  1. I’ve been helping out over at arduino.stackexchange.com for a little bit now and interrupts and timers are one of the most common problems that come up. I’m not sure if this falls under bad or ugly, but the source of most of those problems is that the Arduino library hides away how it configures the timers and interrupts for all of its library functions(e.g. millis() etc,). One of the most common questions is: Can I use A, B, and C with an Uno/Due/Whatever? The answer invariably is yes, you have enough pins to do that, but no, because the libraries used to support those devices are using conflicting timers. One change I think (my opinion of course) would help improve the Arduino ecosystem is to bring the timers and interrupt configuration out of the backend and more into the forefront so that hobbyists can be more conscious about how libraries utilize the resources of the microcontroller, because they are a constraint just as much as only having X number of digital pins or Y number of analog pins.

    1. @jcreedon I would agree with you that this is a good idea.

      I too have seen this multiple times, and found that users are often frustrated by things which conceptually are simple: “I want to use ‘this’ and ‘that’ together, but when I try and compile, I get a ton of errors which I don’t understand…”

      I also thought of another possible solution, via some C macro magic. Imagine that each library has to declare/define the resources it uses. If one includes two libraries which use the same resources, the macros would throw some type of warnings/exceptions which would point the user to where to start troubleshooting.

      This would of course require that everyone writing a library use a commonly-defined set of macros which would match a specific target platform.

    2. I disagree – if you want that then use just use the avr libraries!

      The whole attraction of Arduino is that it hides some of the complexity from the programmer. If you need low level access to interrupts, etc. then you can get it – with the proviso that “here be dragons” …

  2. This is great. Just about done (I think) with my first substantial MSP430 project, a simple musical toy for my 2yo, which is almost entirely interrupt-driven. So I’m eagerly, if a little worriedly, looking forward to the next two installments!

  3. Nice article.
    Timer-Interrupt pair is very useful tool in real-time applications, even the simple LED display multiplex is hard to control without it. I am currently in the project which controls five stepper motors, under different (and, in some cases, variable) speeds, with sensor status checks and “start-stop-change_direction-adjust_speed” for each motor after almost each step. It’s a piece of cake with five timers and five interrupts, but I wonder is it possible without them.

    1. I once did 4 PWM 50 kHz H-bridge controllers using a single timer, and 16 GPIO pins. The interrupt handler used a prepared table with GPIO output value, and next timer compare value, so it only required a few instructions to handle a change. The table would be recalculated at a much lower frequency (10Hz) by another part of the firmware every time the PWM values changed.

  4. A nice hybrid approach is a blended interupt+polling technique.

    In this case you setup a timer to trigger an interupt every millisecond or so and the ISR updates all your counters and also does a single pass through check on inputs, flags, etc that may’ve been set elsewhere and processes them. This works well for things that do not require absolute clock cycle precision such as copying a text buffer out to a display, checking or setting I/O bits, etc.

    1. I set a flag from the interrupt routine to spend minimum amount of time inside an interrupt.
      The main loop simply polls the flag and run the task (for cooperative multitasking). At the end of the big task loop, the processor executes a sleep instruction to be woken up by the next interrupt.
      It works reasonably well for 8-bit with little memory space. Each of those tasks end up being a state machine remembering where they were from the state variables. It is painful for some code like UI that doesn’t fit too well in that model.

    1. I, likewise, have 16 PWM signals simultaneously without interrupts, and without any CPU operation either. Because the PSoC rocks :)

      (in other words, way too many ways to get the same task done in embedded, it’s all about one’s vendor religion)

  5. It’s probably worth pointing out, though, that even with nested interrupts it’s generally a good idea to not dilly-dally in ISRs. Moving data in and out of hardware buffers or setting boolean flags are usually the best applications.

    Also, if you’re using an interrupt just to wake up the processor, there’s usually no need to define an ISR, even if it’s empty.

    1. Totally agree about keeping ISRs short. It’s one of the few programming disciplines that I adhere to (most of the time) even when I don’t need to.

      The reason to define an empty ISR in AVR-GCC is that the default is set to the “bad interrupt” vector which then aliases over to the chip-reset ISR, so that calls to an undefined ISR will reset the chip instead of going off into undefined memory and just running stuff.

      If you want the chip reset on wakeup, that’s cool. But if you want it to pick up where it left off, you want to define an empty interrupt to avoid the reset.

  6. “ST’s ARM chips, for instance, have a dedicated system tick clock, which is super sexy”. Actually, every ARM Cortex-M microcontroller has this timer, not just ST’s. It’s called SysTick and can be thought as being part of the CPU core, not the peripherals. It has been put there by ARM to support context switches in RTOS environments.

    1. Cortex-M has loads and loads of hardware specifically designed to support RTOSes, e.g. the PendSV event (like an interrupt, but manually scheduled and always runs AFTER all pending interrupts; the dispatcher goes in there), dual stack pointers (one for user threads, one for privileged OS threads), usually a memory protection unit, the ability to selectively disable interrupts below a threshold priority, atomic-modify instructions, etc.

      One side effect of all this hardware support is that there is basically only one way to write the dispatcher correctly but you have a tiny tiny quantity of code to write to get a working preemptive RTOS. I think mine is about 400 lines all-up of C++ (headers and source including loads of comments) for blockable threads, scheduler and dispatcher. The dispatcher is just 8 lines of assembler and 1 line of C (or 5 if you include the GPIO toggling that shows CPU load on an LED).

  7. I only understood 5% of what was written! I’m not complaining though. I’m here to learn. I have a Arduino and Pi and I’m very interested in learning how to use micrcontrollers as a hobby. Articles like this will go a long way with helping me learn and understand them better. Thanks!

    1. Think of it this way – on a “big” system, like a PC (ha!) – it is a concert of processors all running on separate clock signals that allow them to co-exist and access the main address and data bus together – but on a slight delay from each other, and only yelling “Hey!!” to the main CPU (or visa-versa) when they have something to say. Video processors, Math Co-processors, Direct Memory Access chips, Serial I/O chips, ETC. – they all have priorities within the system and get requests serviced based on their needs within the rules of that system. Since interrupts can be masked not to fire (which is usually the first thing you do in a service interrupt so you are not re-interrupted), you need to make sure you don’t spend a lot of time in them. So it’s important to count your clock cycles and keep code very tight. Other things to remember are that the interrupts are absolute from the main code – so whatever stack you are using needs to be switched to another internal area, or you can mess up things pretty badly, best programming practices is to program so that the interrupt is not noticed by the main system code you are running, unless you want it that way – so flag registers, standard registers, counters, stacks, anything – is all preserved immediately on entry to the interrupt (things the interrupt will use), and restored the last thing on exit. In some systems you cannot access the kernel where the interrupts have free run of things, or you would be able to change and run any other process – a protection violation. It is of note however, that virtually ALL of the processors that make up these systems while separate in the system, can be re-programmed via in/out commands (depending on the system) or direct memory access (also depending on the system) to do their function by you, if you have the privileges – they are after all, processors. Most of you, with the exception of the old-timers, have never programmed in Assembly – or for that matter hand coded a chip directly for the processor – those people are more accustomed to the needs of things getting along.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.