22C:18, Lecture 35, Fall 1996

Douglas W. Jones
University of Iowa Department of Computer Science

Interrupts for Input/Output

Up to this point, we have discussed traps, and mentioned occasionally that interrupts are similar. Now is the time to distinguish between these:

Most interrupts are used to signal the completion of an input or output operation. So, for example, typing a key on the keyboard, clicking a mouse button, receipt of a character from a modem, or completion of a disk operation will all typically cause the associated devices to request an interrupt from the CPU.

The Keyboard Input Interrupt

The best way to understand interrupts is to look at a typical simple device that can cause an interrupt. The Hawk keyboard input interface is a good example. It is worth noting that keyboard input, input from serial ports, modems and parallel ports all look very similar from a software viewpoint, so the description here is typical of all such devices.

On the Hawk, all device interface registers appear to be 32 bit registers, even if only a few bits have meaning. This simplifies programming because word addressing is easy on this machine. Well behaved interfaces will set the unused bits of these registers to zero. On the hawk, attempting to write a read-only device control register or a read-only bit of a read-write register has no effect; unless the memory management unit intervenes because the memory region is read-only, the write operation is simply ignored.

The Hawk CPU has access to the keyboard interface through a pair of device interface registers:

FF100000 -- the keyboard data register
Contains the most recently entered character typed on the keyboard

FF100004 -- the keyboard status and control register
Contains the current state of the keboard interface
The keyboard data register is a read-only register, writing it has no effect. It contains 8 bits of data representing the most recent key pressed on the keyboard, in ASCII.
Keyboard Data Register
 _______________________________________________________________
|_______________________________________________|_______________|
|31                                            8|7             0|
                    unused                          key code
On full-fledged modern machines, it is common to find input key codes that are not encoded in ASCII; raw scan codes (3 bits to encode keyboard row and 5 bits to encode keyboard column) are fairly common, as are more complex encodings. We will ignore these here!

The keyboard status register is more complex; some bits in it are read-only, while others can be set by writing the register. The basic layout of this register is as follows:

Keyboard Status and Control Register
 _______________________________________________________________
|_______________________________________________|_|_|_________|_|
|31                                            8|7 6|5 4 3 2 1|0|
                    unused                       I E           R
The individual bits are as follows:
R - data ready, read-only
The data ready bit is set when a key is pressed and it is reset by the hardware when a program reads the keyboard data register.

E - error, read-only
The error bit is set if a key is pressed when the data ready bit is one. This indicates that the program failed to read data before new data arrived. This bit is reset when a program reads the keyboard data register.

I - interrupt enable, read-write If set, the hardware will request an interrupt each time a key is pressed.
On most modern machines, the keyboard status register is more complex, for example, offering bits that indicate the state of the control and shift keys, controlling the audible key-click noise with each keypress, or offering the user the option to lock out keyboard input. In addition, many modern machines have additional keyboard control registers; for example, registers allowing the computer to change the mapping between keys and the codes delivered in the data register.

Keyboard Input Software

The basic Hawk monitor includes the following code to read from the keyboard:

;-------------------------------
        INT     KBGETC
KBGETC:                         ; get char from keyboard
                                ; return char in R3
                                ; wipe out R4
                                ; does not use R2
        LIW     R4,KBDSTAT
KBPOLL:                         ; loop awaiting input
        LOADSCC R3,R4           ; test status
        BZS     KBPOLL
        LIW     R4,KBDDATA
        LOADS   R3,R4           ; get char
        JUMPS   R1              ; return
This code begins by repeatedly reading the keyboard status register looking for the ready bit to be set, and then gets the character from the keyboard. The code assumes that interrupts are disabled (as they are, by default), and it assumes that no other keyboard status bits have been defined.

The loop in the above code is called a polling loop. This use of the word polling is closely related to the use of that word in public opinion polling or in describing voting as going to the polling place. When you poll someone, you ask a question. In this case, the CPU is repeatedly asking the device "are you ready yet". When someone hits a key on the keyboard, the answer is yes and execution falls through the bottom of the polling loop and reads the contents of the data register.

The following version of the polling loop tests the ready bit more carefully, allowing a more typical modern keyboard interface with other status and control bits:

KBPOLL:                         ; loop awaiting input
        LOADSCC R3,R4           ; get status
	SRU	R3,1		; shift ready bit into C bit
        BCR     KBPOLL
This routine ignores errors. In the context of a keyboard handler, this is not necessarily a bad thing; other than, perhaps a beep or an unusual keyclick, there is little that can be done about an input error other than, possibly, to ignore the data. Even that may be unreasonable; users should always be able to erase and retype input that was misread by the computer.

Polled input and output routines such as the above were very common in the first generation of software written for each generation of hardware. Thus, code written for the mainframes of the 1950's, the minicomputers of the 1960's and the microcomputers of the 1970's usually relied on polling.

Use of Interrupts

Polling is not generally desirable! While the CPU is executing the code in a polling loop, it cannot do anything else. Usually, we prefer our computers to be able to do useful work in parallel with input or output operations. Thus, for example, we expect to have our keyboard input accepted while the computer is executing our programs (the typeahead problem) and we expect our programs to continue executing after producing output, while that output is being printed (the output spooling problem).

Both of these problems can be solved by hardware, but they are easily solved by software, using interrupts. Most modern operating systems, UNIX (born as a minicomputer operating system in the 1970's) or even MS/DOS (a marginal but widely used microcomputer operating system of the 1980's) support interrupt driven input and output.

Consider the typeahead problem. Solutions to this problem allow input data to be buffered, so that the user may type at his or her own speed without attention to whether or not the application program is ready for the typed data. All solutions to this problem have the following basic structure:

    __________     ____________     _________
   |          |   |            |   |         |
   | Keyboard |==>| FIFO Queue |==>| Program |
   |__________|   |____________|   |_________|

The FIFO queue can be built in hardware, but using interrupts, it is possible to solve this in software as follows:
    __________     ____________ 
   |          |   | Keyboard   |
   | Keyboard |==>| Interrupt  |
   |__________|   |_Handler____|
                        | 
                   _____V______     _________
                  |            |   |         |
                  | FIFO Queue |==>| Program |
                  |____________|   |_________|
With this solution, whenever a user presses a key on the keyboard, the keyboard interface requests an interrupt. The keyboard interrupt handler then reads the character from the interface and enqueues it. When the program wants a character, it uses the dequeue operation. We assume here that the dequeue operation will automatically wait when there is nothing in the queue, blocking the user program.

Assuming that procedures named ENQUEUE and DEQUEUE handle access to the FIFO queue, the following keyboard interrupt service routine will work; this is written to conform to the conventions of the trap and interrupt dispatcher discussed in Lecture 31:

;-------------------------------
; format of AR pointed to by R2
; 	  0	; return address
KEYAREA	= 4	; pointer to register save area
KEYARSZ	= 8	; size of AR

KEYBOARD:			; interrupt handler for keyboard
                                ; assumes R3 points to register save area
				; preserves R3, may wipe out others
	STORES	R1,R2
	STORE 	R3,R2,KEYAREA
        LIW     R4,KBDDATA
	ADDI	R2,KEYARSZ
        LOADS   R3,R4           ; get character from keyboard
	JSR	R1,ENQUEUE	; put it on the queue
	ADDI	R2,-KEYARSZ
	LOAD 	R3,R2,KEYAREA
	LOADS	R1,R2
        JUMPS   R1              ; return
Note that the above code does not inspect or modify the keyboard status register! It assumes that interrupts are enabled and should remain enabled, and it assumes that the interrupt itself was a sufficient signal that that keyboard input is available, so there is no need to check the status. For such a simple device, the only reason to check the keyboard status register would be in order to determine if there was an error.

Given this interrupt service routine, user calls to KBGETC can be replaced with calls to DEQUEUE to create a full software implementation of the typeahead buffer. Of course, there remains the problem of writing an appropriate routines for the queue itself!

Writing the enqueue and dequeue routines is straightforward. We begin by designing a data structure for the queue:

;-------------------------------
; FIFO Queue Data Structure

CAPACITY = 40
	COMMON	QUEUE,MAX
PQUEUE	W	QUEUE

; offsets of fields within the queue structure
HEAD	= 0
TAIL	= 4
DATA	= 8
MAX	= DATA + CAPACITY

;-------------------------------
INITQUEUE:	; initialization
		; wipes out R3-R4
	LOAD	R3,PQUEUE
	LEA	R4,R3,DATA
	STORE	R4,R3,HEAD	; head = tail == empty queue
	STORE	R4,R3,TAIL
	JUMPS	R1
Here, in initializing the queue, we establish the convention that the queue is empty whenever the head pointer is equal to the tail pointer. Now, we can code the enqueue routine, called from the keyboard interrupt service routine:
;-------------------------------
ENQUEUE:	; put one char, R3, into the queue
		; wipes out R3-R6
	LOAD	R4,PQUEUE
	LOAD	R5,R4,TAIL	; enqueued data at the tail end
	ADDSI	R5,1		; increment tail before storing
	LEA	R6,R4,MAX	; get maximum tail value
	CMP	R6,R5		; see if at end of buffer
	BNE	ENQOK
	LEA	R5,R4,DATA	; wrap pointer back to start of buffer
ENQOK:
	LOADS	R6,R5
	STUFFB	R6,R3,R5	; stuff byte into queue
	STORES	R6,R5
	STORE	R5,R4,TAIL	; update tail pointer
	JUMPS	R1		; return
Note that this version of enqueue does not check for a full queue. In a real system, it would probably be wise to lock out additional input when the queue filled, or equivalently (in the case of a keyboard), it would be appropriate to simply discard input when the queue is full.

Writing a DEQUEUE routine that matches this version of ENQUEUE is not very difficult:

;-------------------------------
DEQUEUE:	; get one char into R3 from the queue
		; wipes out R3-R6
	LOAD	R4,PQUEUE
	LOAD	R5,R4,HEAD	; enqueued data at the tail end
DEQPOLL:
	LOAD	R6,R4,TAIL	; must see if there is any data
	CMP	R5,R6
	BEQ	DEQPOLL		;   if equal, queue is empty.
	
	ADDSI	R5,1		; increment head before loading
	LEA	R6,R4,MAX	; get maximum head value
	CMP	R6,R5		; see if at end of buffer
	BNE	DEQOK
	LEA	R5,R4,DATA	; wrap pointer back to start of buffer
DEQOK:
	LOADS	R6,R5
	EXTB	R3,R6,R5	; get byte from queue
	STORES	R6,R5
	STORE	R5,R4,HEAD	; update tail pointer
	JUMPS	R1		; return
Note that this version of dequeue includes a polling loop that blocks the program until the head and tail pointers differ. When these pointers are equal, the queue is empty. Once the program enters this loop, it can only break out when the interrupt service routine calls ENQUEUE; with each trip through the loop, TAIL is reloaded from memory, and ENQUEUE is the only piece of code that changes TAIL.

Critical Sections

This code was fairly easy to write only because the ENQUEUE and DEQUEUE routines each update different variables. Had we tried to keep a counter of the number of characters in the queue where ENQUEUE incremented the counter and DEQUEUE decremented it, a new problem would have arisen. Consider adding the following code fragments to the queue access routines:

; added to ENQUEUE
	LOAD	R3,R4,COUNT
	ADDSI	R3,1
	STORE	R3,R4,COUNT

; added to DEQUEUE
	LOAD	R3,R4,COUNT
	ADDSI	R3,-1
	STORE	R3,R4,COUNT
DEQUEUE is called by the user; consider what happens if, while DEQUEUE is executing, between loading COUNT and storing COUNT, there is a keyboard interrupt:
  1. Initially, assume COUNT is 5
  2. DEQUEUE loads 5 into R3
  3. DEQUEUE decrements R3, which is now 4
  4. A Keyboard interrupt arrives
  5. The interrupt service routine calls ENQUEUE
  6. ENQUEUE increments COUNT to 6 and then returns
  7. DEQUEUE stores 4 in COUNT
Here, the final value of COUNT should have been 5, because ENQUEUE and DEQUEUE have each been called exactly once! Unfortunately, because the interrupt occurred between loading and storeing COUNT in DEQUEUE, the effect of the ENQUEUE on COUNT has been lost! This leads us to call the code between the LOAD and STORE in DEQUEUE a critical section because it is critical that no keyboard interrupts occur between these points.

To prevent such interrupts, we can either disable interrupts by changing the LEVEL field of the PSW, or we can use the interrupt enable bit in the keyboard status word. The latter is preferable, since the only interrupt we need to disable is the keyboard interrupt -- other interrupts, for example, from a disk or a network interface, cause no problems with counting characters in the keyboard typeahead buffer. So we must replace the code to maintain COUNT with the following:

; added to ENQUEUE
	LOAD	R3,R4,COUNT
	ADDSI	R3,1
	STORE	R3,R4,COUNT

; added to DEQUEUE
	
	LIW	R5,KBDSTAT
	STORE	0,R5		; disable interrupts
	LOAD	R3,R4,COUNT
	ADDSI	R3,-1
	STORE	R3,R4,COUNT
	LIS	R3,#10
	STORE	R3,R5		; re-enable interrupts

The failure to correctly identify critical sections in operating system code is one of the most common causes of operating system crashes!