Interrupts

Sources of interrupts, polling, the check at the end of each cycle, saving the volatile environment on a stack, and priorities.

A9.7Computer architectureA level20 min

Do this lesson in the simulator

A 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:

  1. The volatile environment is saved: the contents of the PC, the status register and the other registers are pushed onto the system stack.
  2. 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.
  3. The fetch-decode-execute cycle carries on as normal, so the next instruction fetched is the first instruction of the ISR.
  4. When the ISR finishes, the saved values are popped off the stack back into the registers, including the PC.
  5. 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)

Run this in the simulator

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:

  1. Save the volatile environment by pushing pc, then acc, onto the list stack, and print saved PC=4 ACC=12 (with the real values).
  2. Run the ISR. It uses the accumulator as its working register: set acc to 200 plus 100 times the number of interrupts so far including this one (300, then 400, then 500), and play acc as a tone for 0.2 seconds.
  3. Restore the registers by popping them off stack in the reverse order, and print restored 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

  1. Leave out the restore step. What does the program print at the end, and why is that wrong?
  2. 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.
  3. Explain the difference between polling a sensor and a sensor that raises an interrupt, using a BugBot example.