We love interrupts in our microcontroller projects. If you aren’t already convinced, go read the first installment in this series where we covered the best aspects of interrupts.
But everything is not sunshine and daffodils. Interrupts take place outside of the normal program flow, and indeed preempt it. The microcontroller will put down whatever code it’s running and go off to run your ISR every time the triggering event happens. That power is great when you need it, but recall with Spider-Man’s mantra: With great power comes great responsibility. It’s your responsibility to design for the implicit high priority of ISRs, and to make sure that your main code can still get its work done in between interrupt calls.
Put another way, adding interrupts in your microcontroller code introduces issues of scheduling and prioritization that you didn’t have to deal with before. Let’s have a look at that aspect now, and we’ll put off the truly gruesome side-effects of using interrupts until next time.
Starvation: Long Interrupts
Let’s go back to a concrete example. Imagine that you’ve got some input coming in from an accelerometer, and you’d like to do a bunch of math on it to figure out which way your board is pointing, and then maybe you’d like to send that tilt information out to a serial terminal for debugging.
The naïve approach is to handle all the math and serial stuff inside the interrupt. After all, you only need to process the incoming accelerometer data when it’s new, and the interrupt fires once for each new value. It seems like a perfect match, no?
ISR(INT1_vector){
read_accelerometer();
complicated_math();
wait_for_serial_line_to_clear();
send_results();
}
int main(void){
while(1){
do_stuff();
if (emergency_shutdown_pressed()){
turn_off_killer_laser();
}
}
}
To make the example more drastic, we’ve also implemented an emergency shutdown for a killer laser by polling in the main() loop. Just looking at the main loop, we should be in good shape if do_stuff() runs very quickly, right? The emergency shutdown will get polled quite frequently, right?
Nope. We are hiding away a lot of work in our ISR, and because it’s in an ISR it preempts code running in the main body. So while the ISR is doing heavy math and queuing up data for output, the killer laser is burning through your lab. Clearly, we’ve at least got a priority mismatch here; there’s no way sending numbers to a screen is more important than user safety, right? But even without the priority problem, we’ve got an ISR that does too much.
The problem with heavy-weight interrupts is compounded when you have many inputs handled by interrupts. Your code may end up spending all of its time in ISRs, and have very little time left for the main routine, starving it. If, for instance, you have two long-running ISRs and the second is triggered while the first is still running, and then the first re-triggers while the second is running and so forth, your main loop may never see any CPU time at all.
The golden rule of ISR-writing is this: keep it short and sweet. ISRs are the highest-priority parts of your code, so treat them with respect. Do only the absolute minimum in the interrupt, and allow your code to return to normal as fast as possible.
Trimming your ISRs down to the bare minimum isn’t even very hard, but it requires declaring a few additional variables that can be passed between the ISR and the main flow. That way, instead of handling the once-per-update serial transmission inside the ISR, you can simply flag that it needs handling and let the main routine take care of it. For instance:
ISR(INT0_vector){ /* now handles killer laser shutdown */
turn_off_killer_laser();
}
/* Some volatile variables to pass to/from the ISR */
volatile uint8_t raw_accelerometer_data;
volatile enum {NO, YES} has_accelerometer_data = NO;
ISR(INT1_vector){
raw_accelerometer_data = read_accelerometer();
has_accelerometer_data = YES;
}
int main(void){
while(1){
do_stuff();
if (has_accelerometer_data == YES){
complicated_math();
wait_for_serial_line_to_clear();
send_results();
has_accelerometer_data = NO;
}
}
}
We’ve moved the laser kill switch off to the highest-priority interrupt. You may also be required by law to have a physical kill switch in real life but automated kill switches based on out-of-spec data are often included where danger to humans exists.
Now the accelerometer ISR doesn’t do more than it needs to — records the new data and sets a flag that lets the main body of the code know that it’s got something new to process. We’ve also made the complicated_math() preemptable by putting it in main() instead of an interrupt.
Those of you who followed the interrupt vs. polling debate from the last installment will recognize this as being a hybrid: the interrupt acquires the data, while the main routine polls for new data (in the form of the has_accelerometer_data flag) and does the slower and lower-priority serial handling. This splits up the accelerometer-handling task into low- and high-priority operations, and handles them in the appropriate places. All is well.
Finally, another viable pattern for this example would be to have a text buffer for handling data to be sent out over the serial interface, and poll that buffer each time around the main loop. That way, multiple subroutines can easily share the serial service. For simple situations, the ISR could even write directly to the text buffer without involving flags and so forth. This is one of the nicer services that you get with the Arduino libraries, for instance. We’ll run an article on this very common application soon.
Starvation: Frequent Interrupts
Interrupts are great for handling events fairly speedily, and lightweight ISRs help prevent main loop starvation. But “lightweight” is relative to how frequently the interrupts get called in the first place. No matter how little is going on inside the ISR, there’s always some finite call and return overhead. (Although there are clever, machine-specific ways to minimize this that are out of scope for this article, but that we’d love to cover later.) And the ISR has to do something after all.
If your ISR is going to be called very, very frequently, even a lightweight interrupt can block the main loop. Imagine that it takes a total of 40 cycles just to get into and out of an interrupt, and the interrupt-triggering event ends up happening every 50 cycles. You’re totally blocked if your ISR takes more than ten cycles to run, and your main loop code is reduced to a crawl no matter what. Even the shortest interrupts take some time for call and return.
A sneaky case of too-frequent interrupts can occur with push buttons. On one hand, it’s tempting to handle push button input through interrupts, responding to an infrequent input quite rapidly. But real-world push buttons often “bounce” — making and breaking the circuit many times over a couple of milliseconds as the metal plates inside settle into contact with each other. These glitches can trigger interrupts frequently enough that the ISRs can’t keep up, and thus the main loop gets blocked for a little while.
With a pushbutton, you can be pretty sure that it will only bounce for a few milliseconds, limiting the extent of the main loop starvation, so we view this case as technically flawed, but practically benign. But the same rationale goes for any noisy signal, and if noisy inputs can cause interrupt overload, you’ll need to think about how to deal with that noise. Imagine a loose wire on a model rocket guidance system preventing the main loop from getting its work done. For critical systems, some filtering on the input to keep it from bouncing around is probably a good idea.
Yo Dawg, I Heard You Liked Interrupts…
Compilers like AVR-GCC do you the favor of turning off the global interrupt mask bit upon entering an ISR. Why? Because you probably don’t want interrupts getting interrupted by other interrupts. If they’re intended to be short bits of code anyway, it’s more reasonable to finish up this ISR and then tackle the next. But there’s another reason you should be wary of interrupts inside your interrupts.
Each call to an ISR or any other function starts off with the compiler stashing variables and the current location of the program counter (which keeps track of which instruction the CPU is currently executing) in a memory location called the stack. Nesting functions inside functions pushes more and more variables onto the stack, and when the stack fills up to overflowing, bad things happen. Because microcontrollers often have relatively limited RAM, this can be a real problem.
If you’re the type of programmer who insists on writing recursive functions, you can at least estimate how many times your function will call itself and figure out if you’re likely to smash the stack. But since interrupts are triggered by external events, they’re out of your control, and allowing interrupts to interrupt each other leaves you hoping that they don’t end up nesting too deep. This is the rationale behind turning off the global interrupt enable bit by default when handling an ISR.
Of course, you may be running some lower-priority interrupts that you’d absolutely like to get interrupted by further interrupts. Even on AVR-GCC, there’s a provision for doing this, so it’s not like you can’t. Just be judicious when you let interrupts interrupt other interrupts on the smallest micros.
For instance in our accelerometer-with-killer-laser example above, since we only had two ISRs anyway and they had a very clear priority relationship, we could have either re-enabled the global interrupt bit from within the read_accelerometer() function, or defined the entire ISR to be interruptible. On the AVR platform, the “avr/interrupt.h” lets one define an interruptible interrupt like so:
ISR(INT0_vect, ISR_NOBLOCK) {
...
}
Now you know. Just use it responsibly.
Next Column: The Ugly.
In the next Embed with Elliot, we’ll tackle The Ugly: race conditions and failures of atomicity. The short version of the story is that interrupts will strike at the worst possible times, and you’ll have to adjust your thinking correspondingly, or else mayhem ensues: if() blocks can behave “incorrectly” and variables can take on entirely “wrong” values.
But for now, to avoid “The Bad” of using interrupts, just remember to keep your ISRs short and don’t be afraid to subdivide tasks that need different priority levels. Sounds easy enough, no?
Filed under: Hackaday Columns, Microcontrollers
No comments:
Post a Comment