Interrupts
Sources of interrupts, polling, the check at the end of each cycle, saving the volatile environment on a stack, and priorities.
Do this lesson in the simulatorA processor runs one instruction after another. So how does it notice a key being pressed, a network packet arriving or a battery running flat, in the middle of whatever program it is running? It could keep checking every device, but that wastes almost all its time. Instead, devices interrupt it. This lesson follows an interrupt through the fetch-decode-execute cycle, and shows why the processor's registers must be saved on a stack first.
What an interrupt is
An interrupt is a signal to the processor, from hardware or software, that something needs attention. The processor stops what it is doing, runs a short piece of code to deal with it, called the interrupt service routine (ISR) or interrupt handler, and then carries on exactly where it left off.
| Source | Examples |
|---|---|
| Hardware devices | a key pressed, data arrived from the network, a disk finished a transfer, a printer out of paper, a sensor has a new reading |
| Timer | a hardware clock interrupts at fixed intervals, so the operating system can share the processor between programs |
| Software (exceptions) | division by zero, an instruction the processor does not recognise, a program asking the operating system for a service |
| Hardware failure | power about to fail, a memory error |
Polling or interrupts?
The alternative to interrupts is polling: the program checks each device, again and again, to see if it needs attention.
| Polling | Interrupts |
|---|---|
| the processor keeps asking every device "do you need me?" | the device tells the processor when it needs attention |
| wastes processor time checking devices that have nothing to report | no time is spent until something happens |
| response time depends on how often it checks | response is fast: at the end of the current instruction |
| simple, predictable, fine for a device that almost always has news | needs extra hardware support and careful handling of the saved state |
Your Python programs on BugBot poll: while distance() > 25: asks the sensor again every time round. The firmware underneath, like almost all firmware on a modern microcontroller, is built on interrupts: its timers, its radio and its serial connections all raise them, which is how it reacts to events while doing everything else.
Interrupts and the fetch-decode-execute cycle
The processor never stops halfway through an instruction. At the end of each fetch-decode-execute cycle, before fetching the next instruction, it checks whether an interrupt is waiting. Interrupt requests are recorded as bits in an interrupt register, one for each kind of interrupt.
If there is one, and interrupts are enabled, and its priority is higher than whatever is running now:
- The volatile environment is saved: the contents of the PC, the status register and the other registers are pushed onto the system stack.
- The address of the right ISR is loaded into the PC. Processors find it in an interrupt vector table, a list of ISR addresses, one for each source.
- The fetch-decode-execute cycle carries on as normal, so the next instruction fetched is the first instruction of the ISR.
- When the ISR finishes, the saved values are popped off the stack back into the registers, including the PC.
- The next fetch continues the interrupted program, which never knows it was interrupted.
If no interrupt is waiting, or the waiting one has a lower priority, the processor simply fetches the next instruction.
Why save the registers?
The ISR is ordinary code. It uses the same accumulator, the same general-purpose registers and the same status flags as the program it interrupted. If they were not saved, the interrupted program would come back to find its accumulator changed and its flags overwritten, and would carry on with wrong values. Saving them on a stack, then restoring them, makes the interruption invisible.
Priorities and nesting
Interrupts have priorities. A power failure matters more than a key press. If a higher-priority interrupt arrives while an ISR is running, the ISR itself is interrupted: its registers are pushed on top of the stack, the more urgent ISR runs, and then everything is popped back in reverse order. A lower-priority interrupt has to wait until the current ISR ends. Because a stack is last in, first out, nested interrupts unwind in exactly the right order.
A program can also disable (mask) some interrupts for a few instructions that must not be disturbed, by changing bits in the status register.
stack = []
reg = {"PC": 17, "ACC": 250, "SR": 0b0100}
def save():
stack.append(dict(reg)) # push a copy of every register
print(" push", stack[-1], " depth", len(stack))
def restore():
reg.update(stack.pop()) # pop: the last saved comes back first
print(" pop ", reg, " depth", len(stack))
def keyboard_isr(): # low priority
save()
reg["PC"], reg["ACC"] = 200, 65 # the ISR uses the same registers
print(" keyboard ISR at PC", reg["PC"], ": a timer interrupt arrives")
timer_isr() # higher priority, so it nests
print(" keyboard ISR carries on at PC", reg["PC"])
restore()
def timer_isr(): # high priority
save()
reg["PC"], reg["ACC"] = 300, 0
print(" timer ISR at PC", reg["PC"])
restore()
print("main program", reg)
keyboard_isr()
print("main program resumes", reg)
Read the output line by line. The main program's registers go on the stack first and come off last.
A timer interrupt for a robot
Embedded systems depend on timer interrupts. A robot's firmware might have the main loop doing slow work, like talking to the radio, while a timer interrupt, hundreds of times a second, runs a short ISR that reads the motion sensors and adjusts the motors. The control loop then runs at a steady rate however busy the main loop is.
Task: a timer interrupt
Simulate a processor that is interrupted by a timer. The main program runs 12 cycles. In each cycle it drives forward 3 cm, adds 3 to acc, and adds 1 to pc, in that order.
At the end of any cycle after which pc is a multiple of 4 (so after cycles 4, 8 and 12), the timer interrupts. Handle it like this:
- Save the volatile environment by pushing
pc, thenacc, onto the liststack, and printsaved PC=4 ACC=12(with the real values). - Run the ISR. It uses the accumulator as its working register: set
accto 200 plus 100 times the number of interrupts so far including this one (300, then 400, then 500), and playaccas a tone for 0.2 seconds. - Restore the registers by popping them off
stackin the reverse order, and printrestored PC=4 ACC=12.
After the 12 cycles, print done PC=12 ACC=36, using the values in the registers.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
pc = 0
acc = 0
stack = []
while pc < 12:
forward(50, distance=3)
acc = acc + 3
pc = pc + 1
print(f"done PC={pc} ACC={acc}")
Challenges
- Leave out the restore step. What does the program print at the end, and why is that wrong?
- Add a second interrupt source, a "bump" with higher priority, that can arrive while the timer ISR is running. Show the stack depth reach 4.
- Explain the difference between polling a sensor and a sensor that raises an interrupt, using a BugBot example.