The last time I talked about interrupts, I kind of described what interrupts are. I never really got into how to use them, though. In order to use an interrupt, you write an interrupt handler — a piece of code that the microcontroller jumps to when an interrupt occurs. How that interrupt handler is set up depends on which architecture you’re programming for. In any case, when writing it in a language like C, it’s basically a special function that may need some extra code at the beginning and/or end.
The trick with an interrupt handler is that when it’s done running, it needs to leave the processor in exactly the same state it was in before the interrupt occurred. Recall that a single C instruction may break down into multiple assembly instructions that will likely involve modifying values in the microcontroller’s registers. Let’s say we’re incrementing a variable stored in memory. It will turn into three raw instructions. Let’s assume the compiler decides to use register 2 to modify this variable:
- Load the variable from RAM into register 2.
- Add 1 to the value stored in register 2.
- Save register 2 to the variable in RAM.
Let’s do a concrete example using this process. Let’s say that the variable stored in memory contains the value 200. Without worrying about interrupts, here’s what happens:
- Load the variable from RAM (it contains the value 200) into register 2. Now register 2 contains “200”.
- Increment register 2. Now register 2 contains “201”.
- Save register 2 back to RAM. Now the variable in RAM contains “201”.
That’s all fine and dandy. Now let’s say an interrupt occurs between steps 2 and 3, and it doesn’t properly restore the state of the CPU:
- Load the variable from RAM. Now register 2 contains “200”.
- Increment register 2. Now register 2 contains “201”.
- INTERRUPT! The interrupt handler runs, and it did some stuff that used register 2. It didn’t save the original value of register 2, so now register 2 contains whatever the interrupt left it at — let’s assume it’s 1234.
- Save register 2 back to RAM. Now the variable in RAM contains “1234”.
In my first interrupt article, I had a very similar example, but you need to understand why this example is different. In the first article’s example, the main program was busy modifying a variable in memory the exact same way this one was modifying a variable in memory. However, the interrupt handler was also writing to that same variable in memory. Because of the possibility of the interrupt handler changing the variable while the main program was also busy changing it, I had to protect against that possibility by temporarily disabling interrupts whenever I was modifying the variable in the main program.
In this example, however, the interrupt routine didn’t care about the variable in memory. It was doing some arbitrary operation — anything. Whatever the ultimate goal of the interrupt routine, it had to change register 2 to get it done. Unfortunately, it didn’t restore register 2 to the value it originally had. After the interrupt routine, the main program went along happily, totally unaware that the register’s value had changed. In a real-world situation, this kind of a bug would likely screw up several different registers, unless the interrupt routine was very, very simple and didn’t need to use many registers to get its work done.
So could we protect the code by disabling interrupts here, just like in the last scenario? I guess so, but it wouldn’t make any sense to do it that way. In order to protect the code from this kind of a problem, you would need to have interrupts disabled during the entire program! Otherwise, any time you enabled interrupts, you would be at risk of your registers being totally corrupted. Needless to say, disabling interrupts during your entire program would not be a viable solution — what’s the point of having interrupts if they’re disabled the entire time?
So what’s the solution to this kind of a problem?
You have to make sure your interrupt handlers play nicely. The first thing an interrupt handler should do is save the values stored in any registers it knows it’s going to be using. Where does it save them? Generally, it will store them onto the stack. Likewise, the last thing an interrupt handler should do is restore any registers it saved when it first began. Also, it may have to execute a special instruction for returning from interrupts as its last instruction.
So rather than guarding against the interrupt everywhere else, you attack it at the source — the interrupt handler has to be nice enough to play along with the rest of your program.
It turns out that some microcontrollers are actually cool enough to save the registers for you. The Freescale 68HC11 is an example of a microcontroller that pushes all of its registers onto the stack before it jumps to the interrupt handler. That’s nice, but the 68HC11 doesn’t have many registers. On a more complex CPU, automatically saving all the registers just isn’t an option.
Some compilers will do all of this for you if you specify that a function is an interrupt handler. You might do this by adding __interrupt__ to its definition:
__interrupt__ void timer_intHandler(void);
It all depends on the compiler and the CPU architecture. You might even have to manually write the interrupt handler’s prologue and epilogue yourself with assembly.
I’m personally a big fan of the way the ARM Cortex-M3 works with interrupt handlers. Before I can get into it, though, I need to talk about ARM functions.
The Procedure Call Standard for the ARM Architecture states that any time you call a function, the first four registers (R0 through R3) are used to pass arguments to the function, and the function can also use them as scratch registers. So any time you call an ARM function, if something important is in R0 through R3, you need to save it before calling the function, because you’re not guaranteed that it will still be there when it finishes up (in fact, if the function returns something, the return value is stored in R0). You are guaranteed, however, that the other registers will still be intact after the function finishes up. Thus, if a function modifies pretty much any register other than R0-R3, it needs to save the value of it so it can restore it to its original state when finished. ARM C compilers automatically generate code that adheres to this procedure call standard. Sounds a lot like what an interrupt handler has to do, right?
The Cortex-M3 takes advantage of this fact. Before it jumps to an interrupt handler, it saves R0, R1, R2, and R3. Then it jumps to the interrupt handler. The C compiler follows the procedure call standard and makes sure it preserves the other registers it uses by generating code at the beginning of the function to push their values onto the stack (and matching code at the end of the function to pop the values off of the stack and back into the registers). Then, when the interrupt handler is finished, the processor restores R3, R2, R1, and R0. Since it works this way, a Cortex-M3 interrupt handler is nothing more than a normal C function! No special assembly or extra attribute needs to be added to the function. It just works out of the box.
As I said, though, on other architectures that don’t take advantage of rules like this, you will probably need to specify to the compiler that a function is an interrupt handler, and it will take care of all the saving registers mumbo jumbo for you.
There is one more thing I want to talk about. How do you tell the CPU what interrupt handler is for what interrupt? Let’s say your CPU has several interrupts — your timer has an interrupt, there’s an Ethernet controller interrupt, a USB interrupt, and several others. How does the microcontroller know that an interrupt handler belongs with a particular interrupt?
This is handled with what is called a vector table. A vector table is just an list of addresses to jump to. The first one might be the reset vector, which is where the microcontroller should jump to when it first starts. The next one could be for the timer, the next for the Ethernet, and so on. The microcontroller’s data sheet will specify which position in the list is for each interrupt. In high-level C terminology, you could say that a vector table is an array of function pointers pointing to the interrupt handlers.
So you create this vector table and put it in a place where the microcontroller expects it to be (often at the beginning of the program’s code), and then the microcontroller will know where to jump whenever an interrupt occurs. Your IDE may help you set up a vector table, and if it doesn’t, there will be sample code somewhere that will show you how to do it.
That’s enough for today. I’ve hopefully gone into more depth about what an interrupt handler is and why it has to be special (except on the Cortex-M3 and possibly others). I hope I didn’t go too crazy when talking about the Cortex-M3 (it’s a really nice architecture, I couldn’t resist!). I’m not sure exactly what my next article will be about, but I’m thinking I may start talking about some of these other crazy peripherals built into a microcontroller such as SPI.