ESE101: Interrupts and Cows
This week I’ll introduce a new topic in embedded systems: interrupts.
An interrupt is an event that causes a microcontroller to stop whatever it’s currently doing, handle the interrupt event, and then pick up where it left off before the interrupt happened. Interrupts can happen at any time, and are used to handle time-sensitive events that must be dealt with immediately.
Microcontroller interrupts are similar to real-life interrupts: when your boss stops by your desk you’re interrupted: you stop whatever you’re currently doing, talk to your boss, and when your boss leaves you pick up where you left off before you were interrupted.
A few examples of interrupts are:
- When a user pushes a button, a microcontroller might need to take immediate action like triggering a fire alarm, or some other critical activity. The button press should be designed as an interrupt so the microcontroller takes immediate action.
- When new peripheral data comes into a microcontroller it must handle the data immediately, because more data is likely coming soon, and a microcontroller can only have a limited amount of data in-flight. The arrival of new peripheral data should be designed as an interrupt since it may happen at any time, totally asynchronous to whatever else the microcontroller is doing.
- If a power source fails, a microcontroller wants to be notified immediately so it can try to fix the problem or shutdown the system gracefully. This power source failure should be designed as an interrupt.
Without interrupts, a microcontroller is running the code you’ve written for it, one instruction after another, exactly as you’d expect. Let’s use a simple example of four instructions.
MainLoop:
ADD R4, R5
SUB R6, R7
MOV R8, R9
JMP MainLoop
This program runs the ADD instruction, then the SUB instruction, then the MOV instruction, and then it jumps back to the ADD instruction again, forever. Only those four instructions are run, one after the other.
Interrupts change this straight-line execution flow.
When an interrupt happens, the microcontroller finishes the current instruction, and instead of proceeding to the next instruction in the main program, the microcontroller starts running an Interrupt Service Routine (ISR). An ISR is a special function to handle an interrupt. When the ISR is done, the microcontroller returns to where it left off before it was interrupted. A microcontroller does a couple special things with registers at the start and end of an ISR, but we’ll cover that next time. (It’s called saving context and restoring context in case you want to google it.)
An ISR is a function with a special name, but it’s basically like any other code you would write. For example, let’s use this simple ISR:
Simple_ISR:
BIS #1, R10
BIC #2, R11
RETI
The RETI instruction is new to us: it means “return from interrupt” and it must be the last instruction of every ISR. RETI causes the microcontroller to jump back to whatever instruction it was about to run before the ISR started.
Let’s combine this simple ISR with our MainLoop code into a single example:
MainLoop:
ADD R4, R5
SUB R6, R7
MOV R8, R9
JMP MainLoop
Simple_ISR:
BIS #1, R10
BIC #2, R11
RETI
When the program starts running the MainLoop code runs like this:
ADD
SUB
MOV
JMP
ADD
SUB
MOV
JMP
...
This continues forever - until an interrupt occurs! Let’s say an interrupt happens while the SUB instruction is running. The microcontroller finishes the SUB instruction, and then immediately begins running the ISR code, starting with the BIS instruction. So the instruction flow looks like this; I’ve used underlining to show where the interrupt happens and the ISR starts:
ADD
SUB
MOV
JMP
ADD
SUB ; Interrupt occurs while running this instruction
BIS ; First instruction in the ISR
BIC ; Second instruction in the ISR
RETI ; Last instruction in the ISR
The last instruction of the ISR is RETI, which causes the microcontroller to pick up running where it left off before the interrupt happened. The microcontroller goes back to the MOV instruction, since it’s after the SUB instruction that was running when the interrupt happened.
The complete program flow looks like this:
ADD
SUB
MOV
JMP
ADD
SUB ; Interrupt occurs while running this instruction
BIS ; First instruction in the ISR
BIC ; Second instruction in the ISR
RETI ; Last instruction in the ISR
MOV ; The ISR has completed and returns to the main loop where
; it was interrupted.
JMP
ADD
SUB
MOV
JMP
...
The main loop continues running its four instructions forever, until the next interrupt happens.
We’ll dig into more interrupt details next time.
The interrupting cow picture is by Jeroen Bennink, CC BY 2.0.
This post is part of a series. Check out the complete Embedded Software Engineering 101 series here.