Interrupts and Busy-Wait
Introduction
Interrupts are one of the most powerful and useful features in embedded systems—they can make the system more efficient and responsive to critical events, and they can also make the software easier to write and understand. In addition, interrupts can also allow the code to be optimized for low-power consumption, and also ensure that events are always handled with the correct priority.
What is an Interrupt?
An interrupt is an event that occurs during the execution of a program. Interrupts are caused by events external to the processor, which cause the CPU to halt execution of whatever it's currently doing (thereby pausing execution of any code already running) and then transfers control to a fixed location in memory and runs the interrupt handler. The interrupt handler will then:
- Create a trap frame to record the context (save registers, etc.)
- Determine which device caused the interrupt, and then call the Interrupt Service Routine (or ISR)
- Return execution control to the program
Essentially, the control flow would look something like this:
- Program is running
- An interrupt triggers
- Microcontroller stops executing its current program (stops on the instruction it is on)
- Microcontroller enters the code that handles the interrupt (the ISR)
- When the ISR finishes execution, execution flow is returned back to the original program
If you're familiar with how the program stack works, it might be helpful to visualize it this way:
+--------------+ | | | stack frames | | | | +--------------+ | | trap frame | | stack growth +--------------+ | | interrupt | | | handling | | | frames | v +--------------+
Busy-Wait
A "busy loop" (or "busy wait") is an active polling mechanism, where the application is waiting on some event to occur and continuously checks for it. Here's a somewhat contrived example:
while (getSpeed() < MAX_SPEED) { // #yolo, keep accelerating } cruise();
This code is constantly polling whatever provides the speed calculation (asking it for the current speed value). The problem with polling is that the CPU can execute operations at a much faster speed than most I/O devices. Thus, a CPU can get into a busy wait, wasting time checking whether new information has been received. During this time, it cannot process any other instructions.
Note: This isn't to say that busy-wait loops are all that bad. Busy-wait loops can offer very good response time to one particular event stimulus, at the expense of totally ignoring everything else. Using interrupts to handle events offers slightly slower response time than busy-waiting, but will allow the CPU to do other things while waiting for I/O—it may also allow the CPU to go into a low-power sleep mode until the button is pushed. There may also be times when the hardware doesn't support interrupts, and so using a timer to check whether the stimulus has occurred is your only option. This option is far inferior to the other two.
Interrupts on the MSP 430
When an interrupt occurs, a flag is set to indicate to the processor that the interrupt has occurred.
Each interrupt flag has a corresponding enable bit—setting this bit allows the hardware to trigger an interrupt. In addition, most interrupts on the MSP 430 are maskable, meaning they will only interrupt if they are:
- enabled
- the Global Interrupt Enable (GIE) bit is set in the Status Register (SR)
On the MSP 430, each ISR has its own vector stored in a vector table, located at the end of program memory.
Interrupt Vector Table
An interrupt vector table (IVT) is a table that associates a list of interrupt handlers with a list of interrupt requests (represented as function pointers in either flash memory or RAM). To register a vector, we use the #pragma vector= compiler directive.
__enable_interrupt(); TB0CCTL0 = CCIE | CAP | CM_3 | SCS | CCIS_0; // some code #pragma vector=TIMERB0_VECTOR __interrupt void ISR_NAME(void) { // do stuff }
__enable_interrupt() is a compiler-intrinsic (ie. it is specific to the TI tool-chain and compiler), and simply sets the GIE bit in the Status Register to enable interrupts. We use the TB0CCTL0 register to configure and enable the Timer B interrupt. Here's a quick summary of what we're configuring TB0CCTL0 with. In general, the specifics for each interrupt can be found in the MSP 430 Family Datasheet.
Macro | Meaning | Description |
---|---|---|
CCIE | Capture Compare Interrupt Enable | Enable the capture/compare interrupt |
CAP | Capture mode enable | Set to capture mode |
CM_3 | Capture Mode 3 | Capture on rising and falling edge |
SCS | Synchronous capture mode | Synchronise the timer capture with the next input timer clock signal |
CCIS_0 | Capture Compare Input Select | Capture using input 0 (pin 0) |
Gotchas
Working with interrupts can be tricky, since often-times, the program flow is no longer linear. Here's a couple recommendations and gotchas to watch out for.
Keep your ISRs short
You usually want to keep your ISRs short, so that they don't block your main program from executing. If they take too long to execute, or are being triggered too frequently, they may prevent your program from advancing.
// some super-important code here #pragma vector=USCI_A0_VECTOR __interrupt void USCI_A0_ISR(void) { // some super inefficient and long code here // that takes forever to execute }
In this case, if your ISR takes a long time to run, it might not finish before the next one is triggered, meaning that you never actually get to that super-important code. This might also mean that your interrupts will also interrupt each other (depending on interrupt priority).
Also, if you're making a lot of function calls within an ISR, this can lead to excessive stack usage, which may exceed the available stack size limit. In this case, it might be preferable to inline the logic.
Double-Check, Triple-Check Initialization
If your interrupt isn't working, chances are you're not initializing the interrupt correctly. It's probably a good idea to check these first.
- Did you enable interrupts globally?
- Did you enable the interrupt?
- Was the ISR mapped to the correct Interrupt Vector in the interrupt table?
- Is the ISR being called?
- Is the interrupt mapped to the correct hardware pin?
- Is the interrupt flag being cleared?
- Is another interrupt interrupting this interrupt?
The "volatile" keyword for shared variables
The volatile keyword indicates to the compiler that there's a chance that the variable's value could change outside the normal flow of execution of the program. This essentially disables any compiler optimizations, and tells it to load/store the value of the variable each time it needs to be used.
If you're updating a shared value with an interrupt, the variable should probably be set to volatile.
TL;DR
Interrupts are basically a way for the hardware to signal the software that some event has occurred. When an interrupt occurs, it jumps and runs special code to handle the event, which can be registered in the Interrupt Vector Table using the #pragma vector= compiler directive. Once this "event-handler" is complete, the program returns to normal execution.