Interrupts

The i/o examples in io.html are what we call a "busy wait loop", where you just keep saying, are we there yet? are we there yet? are we there yet? until finally the sound sample has been output, or the character has been typed or finished being output, and you can process the incoming data or send the next outgoing data.

When you type something to the computer, it has to spend most of its time just waiting for you to press the next key. Compared to a computer, your typing speed is very slow. If the computer is just checking the i/o register over and over in a loop, and you are still thinking about typing something, or your hand is just moving down from your chin to the keyboard, which takes hundreds of millions of nanoseconds, the computer is just wasting processing time when really it could be getting something done.

We want the computer to be able to do something else during this time. e.g. you can be editing your computer program or essay while listening to MP3s (as I write this I'm editing a text file while listening to MP3s...).

The basic idea of interrupts can be described as an "involuntary subroutine call". There is no JSR; the "calling" program isn't aware of the interrupt. It gets interrupted for something else to be dealt with, usually an i/o event.

Like a subroutine call, an interrupt pushes the contents of the PC as a "return address". At the end of the "interrupt service routine", there is an instruction much like RTS which goes back to where the interrupt occurred. The "interrupt service routine" (ISR) is also known as an "interrupt handler".

The ISR needs to be written extremely defensively as far as not modifying anything about the state of the computer. If it changes the contents of any register (and R0 is not special in an ISR) then you can introduce mysterious bugs all over the other computer programs running on that computer, depending upon where they got interrupted!

Condition codes are also an issue. For example, the main program might just have performed an arithmetic operation and be about to do a conditional branch instruction when the interrupt happens. The ISR probably does some operations which set the condition codes. When it returns, we'd better restore the condition codes to what they were when the interrupt occurred. This would be very hard to write in machine language, so we generally have hardware support for assisting with this, by automatically pushing this on the stack along with the PC.

So an interrupt pushes the condition codes on the stack too, and maybe other things. On the PDP-11, what's pushed is the "processor status word" (PSW), which contains, basically, the data we want pushed when an interrupt occurs. The condition codes are four bits of the PSW. The PSW is described on the last page of the PDP-11 handout.

The ISR will end with an "RTI" instruction instead of "RTS" -- "return from interrupt" instead of "return from subroutine". The main difference between these two instructions is that the RTI instruction pops four bytes (two words) from the stack: not just the return address, but also the saved PSW. That is, the two words from the stack go into the PC and the PSW respectively. The RTI instruction also differs from the RTS in not having a linkage register facility (the caller wouldn't be using it; there is no caller, it's part of the hardware that the ISR is called), so you say "RTI" rather than "RTI R7".

There will be a pin on the CPU chip labelled something like IRQ, which stands for interrupt request. You'll also see INTR sometimes. And of course if you see IRQ bar, that simply means that this line is normally 1, and to request an interrupt you set it to zero for a cycle. As well, on CPUs with different interrupt priority levels (which is basically all of them these days), we might have pins IRQ1, IRQ2, etc.

The peripheral hardware will be connected to the appropriate interrupt pin. Then when it signals that an interrupt should happen, the above procedure is followed: We do wait for the current instruction to complete, at least, on most CPUs; then we push the PC and PSW; and then we load the address of the interrupt service routine into the PC thus transferring to it.

Once we have multiple interrupt pins, we get into an issue of interrupt "priority". Some interrupts have priority over others.

Consider the case where the disk drive hardware interrupts to indicate that a block of data from the disk is ready, at about the same time as the user presses a key on the keyboard and that causes an interrupt too.

Which should we deal with first? Well, it's fine for an interrupt service routine to interrupt another interrupt service routine -- it's all on the stack, it will nest just fine. But it would be silly for an ISR always to interrupt any executing ISR; this would just mean that we always deal first with whichever interrupt occurred last.

So we give different interrupts different "priority". The interrupt from the disk drive is more urgent, because if we read the block out of the buffer fast enough and ask it to read the next block, we can catch it before the disk rotation passes the next block. Keeping the disk drive going improves the data transfer rate.

Similarly, in the sound-playing example, sending the new sound byte(s) out to the sound hardware is urgent or we'll have "drop-outs".

By comparison, the human typist is very slow, and even if we really take our time getting around to processing the keystroke, it will get processed before the next keystroke.

Each interrupt has an associated numeric priority. Nomenclature differs, but the most straightforward numbering, and the one used in the PDP-11, is to say that greater numbers are greater priorities. So if we set the sound interrupts to priority level 5, since the keyboard interrupt is priority 4, a sound interrupt will interrupt a keyboard ISR but a keyboard interrupt will not interrupt a sound ISR.

There is a current "CPU priority" stored in the PSW. It's three bits on the PDP-11. This value is normally zero. In an interrupt service routine, we set it to a desired level. When an interrupt occurs, the hardware compares the priority of the interrupt to the CPU priority stored in the PSW, and interrupts if and only if the CPU priority is lower.

Interrupts might also happen when you press an interrupt key of some kind on the keyboard or elsewhere on the computer. The "reset" button on an Apple II was simply wired into the IRQ pin of the CPU; unlike the "reset" button on an ibm-pc-compatible, you could write something to catch this interrupt and do something other than a complete system reset. Sometimes a button like that drops you into the debugger, etc.

When an interrupt occurs, the interrupt handler is called. Usually you can have different interrupt handlers for different interrupt priority levels. How do we implement that?

For each interrupt type (e.g. each INTR pin) we have an "interrupt vector", at a built-in memory address (the actual address is built in to the CPU just like how the use of R6 is built in to the PDP-11 CPU for JSR instructions, etc). For "vector" read "array". On the PDP-11 this is two words: The desired PC and the desired PSW. When the interrupt occurs, we push the current PC and PSW, and we load the two words from the interrupt vector into the PC and PSW. The interrupt vector address tends to be a low address because if that CPU is used for an arbitrary computer, you may not have as much memory as it can address. E.g. for a PDP-11 you might not have all 64K of memory, but if the interrupt vectors are in the two-digit addresses (as they are), you're sure to have some RAM there, even if you don't have RAM all the way up.


Now.
We want to be able to say "start a sound file", then run another program while the sound is playing.

SOUNDPTR = 700
SOUNDLEFT = 702

        MOV #SOUNDDATA, SOUNDPTR
        MOV SOUNDSIZE, SOUNDLEFT
        MOV #SOUNDISR, 74  ; sound interrupt vector
        MOV #240, 76  ; PSW in interrupt vector  (priority five)
        MOVB #200, SOUNDSTATUS   ; enable interrupts
        ....  go ahead with other code ...

elsewhere:

SOUNDISR: MOVB (SOUNDPTR)+, SOUNDSAMPLE
          DEC SOUNDLEFT
          BGT CONT
          MOV #0, SOUNDSTATUS  ; disable interrupts
   CONT:  RTI

We're going to run this ISR at priority five.

Why 5?

The keyboard is priority 4; and we want a sound interrupt to override a keyboard interrupt in progress. More urgent because we'll have a drop-out in the music if we miss samples. The user typing on the keyboard is comparatively slow; it's not going to get backlogged if we preempt a keyboard interrupt, but it might happen coincidentally that two of them occur at the same time, and the sound interrupt is much more urgent.

Why 240?

See PSW layout info in pdp-11 handout. The priority field is the top three bits of the low byte. The other fields in the PSW are not important here; we'll just set them to zero (one of the bits ('T' bit, see later) does something strange and it's important it's zero; but it's not important for the rest, which are the four condition codes).

5 is 1012
so we want 10100000
Group into threes: 10 100 000
or octal 240.

RTI: recall that the RTI instruction pops the PSW, thus also resetting the priority level, i.e. we can have another sound interrupt. That "240" above says that this interrupt service routine is priority 5, so during our interrupt service routine, higher priority interrupts will still get processed (pushing OUR PSW, so that when THEY do the RTI the priority level will return to 5). But during our interrupt routine, lower- or equal-level interrupts will not be processed. Neither will they be relieved. The MOVB (SOUNDPTR)+, SOUNDSAMPLE clears the bit which says that sound data is needed, which causes the high bit of SOUNDSTATUS to go back to 0. If we don't do this before we do the RTI, the RTI, in dropping the cpu priority level, will cause the interrupt to be signalled again.

All these details vary from CPU to CPU, and even between different computer architectures using the same CPU because of how the peripherals are wired up, but there you have the basic concepts.


Disabling interrupts: Sometimes you may want to disable interrupts. You may be doing some particular time-critical memory-mapped-io processing, or data structures may be in an inconsistent state.

Example re linked-list insertion and the interrupt routine attempts to traverse that linked list.

Generally a CPU will provide a way to disable interrupts, or disable interrupts below a certain priority. The mechanism is quite simple; you just raise the priority level in the PSW.

SPL 7 (not an addressing mode; that 7 just goes right in to the instruction, see handout)


The issue of code being reentrant code: can be called at any time, even if already executing and interrupted: refer to linked list example: very different than nested normal subroutine calls (recursion or mutual recursion).

If code is not reentrant, it is unsuitable for use by interrupt handlers. Even if they call something which calls something which calls something..... Usually can't do much fancy in interrupt handler. E.g. os calls, ui stuff.


Direct Memory Access (DMA)

Problem: we have to crank out 44,100 samples per second, and each interrupt service takes more than a 44100th of a second! Just a few instructions, but with the CPU interrupt processing, etc, it ends up being a while.

The solution is to use a mechanism known as direct memory access (DMA). A DMA peripheral has access to the main memory without going through the CPU. You tell it what to read and where in main memory to put it; or you tell it where to write and where in main memory to find the data to be written. You may have one DMA controller for each i/o device, or you may have a DMA controller which is connected on a private bus to several i/o devices which it can control. The DMA hardware interrupts only when it needs more parameters (every hundred thousand samples or something like that).

More than one DMA controller can be doing its thing at once.

How can we have the CPU and the DMA device both operating the memory unit at once? If they both broadcast on the system bus, we'll get garbage on the bus.

There's an idea of "memory cycles", cycles of a clock running the memory unit. May be slower than the CPU clock.

Most of the time, the CPU can use all the memory cycles. But a DMA device does what we call "cycle stealing" -- when it needs to do memory access, either it suppresses the CPU's hearing about a memory cycle, or there can be a general bus arbitration mechanism in which any device, including the CPU, which wishes to do something on the system bus has to access it through some circuitry which makes it wait its turn if necessary.

When the DMA device is done, it signals an interrupt. It may be able to process only so much at once, then interrupt for further instructions; but also, the software needs to know when it's done and we don't want to busy-wait.

may interrupt for parameters at the beginning, depends on architecture


Software interrupts

We've been talking about what we could call "hardware interrupts"; many CPUs provide many more levels of software interrupts than there are interrupt pins on the chip.

op that says "signal an interrupt"

e.g. you press control-C, causes keyboard interrupt, read routine in OS decides it should do the more serious abort-program interrupt and has to signal that somehow.

also used for error handling -- library routine fp divide by zero example: a:=b/c -- no way to return error return codes. But I'm sorta telling this to the wrong audience here because the extension of this is to use "exceptions" for error handling of all sorts too, and that is able to lead to a very elegant architecture such as exceptions in Java. Those won't be implemented using software interrupts, then; consider software interrupts to be the machine-language equivalent of THROWing an exception.

PDP-11: TRAP op:

opcode 104xyz, x>=4: allows a sort of opcode within the trap instruction, so the ISR can implement different traps.

What TRAP does:

various other trap locations used instead of 34: (34 is the interrupt vector)

loc 36 (or whatever) thus can set cpu priority level, and can set a privilege level in the cpu called "supervisor mode" which we don't have time to talk about really but it provides operating system types of privilege -- normal user programs can't manipulate i/o devices or access just any word of memory etc. If the operating system needs the "supervisor mode" privilege to do the appropriate task, it can get it by this PSW setting, in a way which doesn't give the privilege to the user program calling it. RTI then automatically turns it back off by restoring the caller's PSW. More generally, any PSW bit can be set by putting the appropriate word at location 36.

EMT, IOT: "emulator trap", "i/o trap" -- the same, really; different interrupt vector locations, can be used for fake instructions, i/o commands to the OS.

Examples in other architectures:

  • PDP-10: special "CALL" op, parameter is an integer which is looked up in a special table of "OS entry points"
  • "PC-compatible hardware", e.g. MS-DOS and Windows and such, and the majority platform for linux: use "INT" op which causes a (software interrupt) to be signalled; parameters are in specified registers
  • Apple II: no traps, you just JSR to the appropriate address and it does an RTS, not an RTI, to return. Couldn't have done any better because there is only one interrupt vector on the 6502.

    BPT (break-point)

    trace traps, RTT (T bit off in int vec PSW, but what about when you RTI?)

    profiling


    [list of course notes topics available]
    [main course page]