Interrupts

The basic function of any computer system is to read some input, perform some calculation, and write some output. The input may be entered on a teletype keyboard, read from a tape or disk, or come from some sensors from a machine. Likewise, the output can be sent to a printer, written to a tape or disk, or send a signal to a motor or transmitter. In many cases, the processor can execute instructions much faster than the input or output device can handle. An efficient system would be able to run those calculations while waiting for the next input to be ready, or for the output to be ready to handle the results of a calculation. Interrupts allow a computer to perform calculations, and be notified when a device requests the attention of the processor.

At any given time, the PDP-11 processor is running at a certain priority level, from zero through seven. This value is kept in the processor status word, alongside the carry, negative, trace, zero, and overflow flags. If an interrupt is raised by a device, and if the processor's priority level is the same or less than the interrupt priority, the processor finishes executing the currently running exception, then starts the interrupt handling process.

The PDP-11 UNIBUS provides five signal lines for interrupts. Four are known as Bus Requests. Each of these lines has a priority value- one of the lines is he priority 4 signal, another line is the priority 5 signal, and similarly for priority 6 and 7. A device can indicate the priority of its requests- a request that is very important to be handled quickly would assert the bus request 7 line. A device that can deal with a potentially slower response time could assert the priority 4 line. The fifth signal line is for "Non Processor Request", which are of the highest priority, and typically used for devices that need to access memory directly.

If a Bus Request signal is asserted, and the signal priority is greater than the processor priority (indicated in the processor status word), then the processor services the interrupt. It does this by pushing the current processor status to the stack, then the next instruction's address. The device that asserted the interrupt signal also provides the memory address of the "interrupt vector": a location that contains the address of a routine that can handle the device's request. These addresses are typically below memory location 400. The processor loads the program counter to the value at the interrupt vector, then sets the processor status word to the value in the address after the interrupt vector. The interrupt handling routine is done when it executes an RTI (Return from Interrupt) instruction, which reverses the process: it pops the next instruction address in to the program counter, and pops the original value of the processor status word.

Interrupt handlers can themselves be interrupted; for example, if an interrupt handler is running with processor priority 5, and an interrupt with priority 6 is asserted, the current processor status word and program counter are pushed to the stack, and the value at the interrupt vector for the new interrupt is put into the program counter. Thus, interrupts can be "nested": higher priority interrupts can interrupt lower priority interrupts.

As an example of interrupt usage, imagine we have a program that runs some sort of calculation, and wants to print out some information to the console as it is working, like intermediate results. The console printer is slow- it can only handle ten characters per second, which is one every 100,000 microseconds. This is much slower than even the slowest PDP-11 processor. If the program is using the polling technique, it would have to waste much of its time checking to see if the console printer is ready.

Instead, a program can designate some of the memory to be a buffer, and have an interrupt routine that would be called when the console printer is ready for another character. The interrupt routine will check for a character in the buffer, and send it to the console transmit register, and then return control to the main program until the printer is ready for another character. This way, the main logic of the program does not need to continually check if the printer is ready for a new character.

        R0=     %0              ; REGISTER 0
        SP=     %6              ; STACK POINTER (REGISTER 6)

        TPVEC=  64              ; TELEPRINTER INTERRUPT VECTOR PC
        TPPSW=  TPVEC+2         ; TELEPRINTER INTERRUPT VECTOR PSW
        TPS=    177564          ; TELEPRINTER STATUS WORD
        TPB=    TPS+2           ; TELEPRINTER BUFFER

        .=      1000            ; START AT 1000
START:  MOV     #.,SP           ; SET UP STACK
        MOV     #WRITE,TPVEC    ; SET UP INTERRUPT HANDLER ROUTINE
        MOV     #200,TPPSW      ; SET UP INTERRUPT PROCESSOR STATUS

        MOV     #MSG,NEXT       ; NEXT CHAR TO WRITE IS MSG

        MOV     #177777,R0      ; SET R0
        BIS     #100,TPS        ; ENABLE INTERRUPTS ON TELEPRINTER
CALC:   DEC     R0              ; MAIN CALCULATION: DECREMENT R0
        BNE     CALC            ; IF NOT ZERO, REPEAT

FLUSH:  TSTB    @NEXT           ; FLUSH THE BUFFER. ARE WE AT THE END?
        BEQ     DONE            ; YES, SKIP AHEAD
        WAIT                    ; NO, WAIT FOR AN INTERRUPT
        BR      FLUSH           ; CHECK THE BUFFER AGAIN

DONE:   BIC     #100,TPS        ; WE'RE DONE. DISABLE INTERRUPTS
        HALT                    ; HALT THE PROCESSOR
        BR      START           ; RESTART ON CONTINUE

WRITE:  MOVB    @NEXT,TPB       ; WRITE THE NEXT CHARACTER
        BEQ     NOINC           ; IF IT WAS NUL, DON'T INCREMENT NEXT
        INC     NEXT            ; POINT TO NEXT CHARACTER
NOINC:  RTI                     ; RETURN FROM INTERRUPT ROUTINE

MSG:    .ASCII   /INTERMEDIATE RESULT/
        .BYTE    12,0           ; NEWLINE, END OF STRING
        .EVEN                   ; ALIGN IF ODD NUMBER OF BYTES
NEXT:   .WORD    0              ; POINTER TO NEXT CHAR TO WRITE

        .END                    ; END OF PROGRAM

The first section of the code sets up some labels: the register labels, the vector location for the DL11 teletype printer control, the location of the processor status word for the interrupt handler, and the teletype printer status register and output buffer.

Next, we initialize the program by setting up the stack pointer, setting the interrupt vector to point to our WRITE routine, and using 200 as the processor status word. This clears the status bits (carry, zero, etc.), but sets the processor priority level to 4 when entering the interrupt routine. This prevents other interrupts of priority 4 from being handled while our routine is running.

The next character to write is set to be the first character of the message.

Register 0 is initalized to 177777, and interrupts are enabled in the teleprinter status register.

The calculation simply decrements R0 until it reaches zero. A real program would do something more interesting. Note that the calculation loop does not need to continually check the printer status.

When the printer is ready, the teletype controller will interrupt the processor and the WRITE routine is called to print the next character in the message. WRITE puts the character into the teleprinter output buffer, increments the next character pointer if it's not the NUL character, and returns from the interrupt back to the main program. Meanwhile, the printer is receiving the byte and performing the mechanical operation of typing the character.

When R0 reaches zero, we make sure all characters in the buffer have been printed. To do this, we check the next character to be printed. If it's not zero, we wait until the next interrupt occurs- when the printer is ready for the next character, it will assert an interrupt, calling WRITE, and we loop, to check if the next character is zero. If it is zero, this means the entire message has been printed, and we can finish the program.

The interrupt flag is cleared, and the processor is halted. The program will start over if the CONT switch is used.

References