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
- DL11 Asynchronous Line Interface Manual EK-DL11-TM-003