dseg double word variable OldIntVect, you can call the original ISR with the following code:; Presumably, DS points at DSEG at this point. pushf ;Simulate an INT instruction by pushing call OldIntVect ; the flags and making a far call.
Since OldIntVect is a dword variable, this code generates a far call to the routine whose segmented address appears in the OldIntVect variable. This code does not jump to the location of the OldIntVect variable.
Many interrupt service routines do not modify the ds register to point at a local data segment. In fact, some simple ISRs do not change any of the segment registers. In such cases it is common to put any necessary variables (especially the old segment value) directly in the code segment. If you do this, your code could jump directly to the original ISR rather than calling it. To do so, you would just use the code:
MyISR proc near . . . jmp cs:OldIntVect MyISR endp OldIntVect dword ?
This code sequence passes along your ISR's flags and return address as the flag and return address values to the original ISR. This is fine, when the original ISR executes the iret instruction, it will return directly to the interrupted code (assuming it doesn't pass control to some other ISR in the chain).
The OldIntVect variable must be in the code segment if you use this technique to transfer control to the original ISR. After all, when you executing the jmp instruction above, you must have already restored the state of the CPU, including the ds register. Therefore, you have no idea what segment ds is pointing at, and it probably isn't pointing at your local data segment. Indeed, the only segment register whose value is known to you is cs, so you must keep the vector address in your code segment.
The following simple program demonstrates interrupt chaining. This short program patches into the int 1ch vector. The ISR counts off seconds and notifies the main program as each second passes. The main program prints a short message every second. When 10 seconds have expired, this program removes the ISR from the interrupt chain and terminates.
; TIMER.ASM
; This program demonstrates how to patch into the int 1Ch timer interrupt
; vector and create an interrupt chain.
.xlist
.286
include stdlib.a
includelib stdlib.lib
.list
dseg segment para public 'data'
; The TIMERISR will update the following two variables.
; It will update the MSEC variable every 55 ms.
; It will update the TIMER variable every second.
MSEC word 0
TIMER word 0
dseg ends
cseg segment para public 'code'
assume cs:cseg, ds:dseg
; The OldInt1C variable must be in the code segment because of the
; way TimerISR transfers control to the next ISR in the int 1Ch chain.
OldInt1C dword ?
; The timer interrupt service routine.
; This guy increment MSEC variable by 55 on every interrupt.
; Since this interrupt gets called every 55 msec (approx) the
; MSEC variable contains the current number of milliseconds.
; When this value exceeds 1000 (one second), the ISR subtracts
; 1000 from the MSEC variable and increments TIMER by one.
TimerISR proc near
push ds
push ax
mov ax, dseg
mov ds, ax
mov ax, MSEC
add ax, 55 ;Interrupt every 55 msec.
cmp ax, 1000
jb SetMSEC
inc Timer ;A second just passed.
sub ax, 1000 ;Adjust MSEC value.
SetMSEC: mov MSEC, ax
pop ax
pop ds
jmp cseg:OldInt1C ;Transfer to original ISR.
TimerISR endp
Main proc
mov ax, dseg
mov ds, ax
meminit
; Begin by patching in the address of our ISR into int 1ch's vector.
; Note that we must turn off the interrupts while actually patching
; the interrupt vector and we must ensure that interrupts are turned
; back on afterwards; hence the cli and sti instructions. These are
; required because a timer interrupt could come along between the two
; instructions that write to the int 1Ch interrupt vector. This would
; be a big mess.
mov ax, 0
mov es, ax
mov ax, es:[1ch*4]
mov word ptr OldInt1C, ax
mov ax, es:[1ch*4 + 2]
mov word ptr OldInt1C+2, ax
cli
mov word ptr es:[1Ch*4], offset TimerISR
mov es:[1Ch*4 + 2], cs
sti
; Okay, the ISR updates the TIMER variable every second.
; Continuously print this value until ten seconds have
; elapsed. Then quit.
mov Timer, 0
TimerLoop: printf
byte "Timer = %d\n",0
dword Timer
cmp Timer, 10
jbe TimerLoop
; Okay, restore the interrupt vector. We need the interrupts off
; here for the same reason as above.
mov ax, 0
mov es, ax
cli
mov ax, word ptr OldInt1C
mov es:[1Ch*4], ax
mov ax, word ptr OldInt1C+2
mov es:[1Ch*4+2], ax
sti
Quit: ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
TimerISR proc near push ds push ax mov ax, dseg mov ds, ax mov ax, MSEC add ax, 55 ;Interrupt every 55 msec. cmp ax, 1000 jb SetMSEC ; <<<<< Suppose the interrupt occurs at this point >>>>> inc Timer ;A second just passed. sub ax, 1000 ;Adjust MSEC value. SetMSEC: mov MSEC, ax pop ax pop ds jmp cseg:OldInt1C ;Transfer to original ISR. TimerISR endp
Suppose that, on the first invocation of the interrupt, MSEC contains 950 and Timer contains three. If a second interrupt occurs and the specified point above, ax will contain 1005. So the interrupt suspends the ISR and reenters it from the beginning. Note that TimerISR is nice enough to preserve the ax register containing the value 1005. When the second invocation of TimerISR executes, it finds that MSEC still contains 950 because the first invocation has yet to update MSEC. Therefore, it adds 55 to this value, determines that it exceeds 1000, increments Timer (it becomes four) and then stores five into MSEC. Then it returns (by jumping to the next ISR in the int 1ch chain). Eventually, control returns the first invocation of the TimerISR routine. At this time (less than 55 msec after updating Timer by the second invocation) the TimerISR code increments the Timer variable again and updates MSEC to five. The problem with this sequence is that it has incremented the Timer variable twice in less than 55 msec.
Now you might argue that hardware interrupts always clear the interrupt disable flag so it would not be possible for this interrupt to be reentered. Furthermore, you might argue that this routine is so short, it would never take more than 55 msec to get to the noted point in the code above. However, you are forgetting something: some other timer ISR could be in the system that calls your code after it is done. That code could take 55 msec and just happen to turn the interrupts back on, making it perfectly possible that your code could be reentered.
The code between the mov ax, MSEC and mov MSEC, ax instructions above is called a critical region or critical section. A program must not be reentered while it is executing in a critical region. Note that having critical regions does not mean that a program is not reentrant. Most programs, even those that are reentrant, have various critical regions. The key is to prevent an interrupt that could cause a critical region to be reentered while in that critical region. The easiest way to prevent such an occurrence is to turn off the interrupts while executing code in a critical section. We can easily modify the TimerISR to do this with the following code:
TimerISR proc near push ds push ax mov ax, dseg mov ds, ax ; Beginning of critical section, turn off interrupts. pushf ;Preserve current I flag state. cli ;Make sure interrupts are off. mov ax, MSEC add ax, 55 ;Interrupt every 55 msec. cmp ax, 1000 jb SetMSEC inc Timer ;A second just passed. sub ax, 1000 ;Adjust MSEC value. SetMSEC: mov MSEC, ax ; End of critical region, restore the I flag to its former glory. popf pop ax pop ds jmp cseg:OldInt1C ;Transfer to original ISR. TimerISR endp
We will return to the problem of reentrancy and critical regions in the next two chapters of this text.
mov cx, 8192 mov dx, 310h lea bx, Array ;Point bx at storage buffer DataAvailLp: in al, dx ;Read status port. shr al, 1 ;Test bit zero. jnc DataAvailLp ;Wait until data is available. inc dx ;Point at data port. in al, dx ;Read data. mov [bx], al ;Store data into buffer. inc bx ;Move on to next array element. dec dx ;Point back at status port. loop DataAvailLp ;Repeat 8192 times. . . .
This code uses a classical polling loop (DataAvailLp) to wait for each available character. Since there are only three instructions in the polling loop, this loop can probably execute in just under a microsecond[10]. So it might take as much as one microsecond to determine that data is available, in which case the code falls through and by the second instruction in the sequence we've read the data from the device. Let's be generous and say that takes another microsecond. Suppose, instead, we use a interrupt service routine. A well-written ISR combined with a good system hardware design will probably have latencies measured in microseconds.
To measure the best case latency we could hope to achieve would require some sort of hardware timer than begins counting once an interrupt event occurs. Upon entry into our interrupt service routine we could read this counter to determine how much time has passed between the interrupt and its service. Fortunately, just such a device exists on the PC - the 8254 timer chip that provides the source of the 55 msec interrupt.
The 8254 timer chip actually contains three separate timers: timer #0, timer #1, and timer #2. The first timer (timer #0) provides the clock interrupt, so it will be the focus of our discussion. The timer contains a 16 bit register that the 8254 decrements at regular intervals (1,193,180 times per second). Once the timer hits zero, it generates an interrupt on the 8259 IRQ 0 line and then wraps around to 0FFFFh and continues counting down from that point. Since the counter automatically resets to 0FFFFh after generating each interrupt, this means that the 8254 timer generates interrupts every 65,536/1,193,180 seconds, or once every 54.9254932198 msec, which is 18.2064819336 times per second. We'll just call these once every 55 msec or 18 (or 18.2) times per second, respectively. Another way to view this is that the 8254 decrements the counter once every 838 nanoseconds (or 0.838 msec).
The following short assembly language program measures interrupt latency by patching into the int 8 vector. Whenever the timer chip counts down to zero, it generates an interrupt that directly calls this program's ISR. The ISR quickly reads the timer chip's counter register, negates the value (so 0FFFFh becomes one, 0FFFEh becomes two, etc.), and then adds it to a running total. The ISR also increments a counter so that it can keep track of the number of times it has added a counter value to the total. Then the ISR jumps to the original int 8 handler. The main program, in the mean time, simply computes and displays the current average read from the counter. When the user presses any key, this program terminates.
; This program measures the latency of an INT 08 ISR.
; It works by reading the timer chip immediately upon entering
; the INT 08 ISR By averaging this value for some number of
; executions, we can determine the average latency for this
; code.
.xlist
.386
option segment:use16
include stdlib.a
includelib stdlib.lib
.list
cseg segment para public 'code'
assume cs:cseg, ds:nothing
; All the variables are in the code segment in order to reduce ISR
; latency (we don't have to push and set up DS, saving a few instructions
; at the beginning of the ISR).
OldInt8 dword ?
SumLatency dword 0
Executions dword 0
Average dword 0
; This program reads the 8254 timer chip. This chip counts from
; 0FFFFh down to zero and then generates an interrupt. It wraps
; around from 0 to 0FFFFh and continues counting down once it
; generates the interrupt.
;
; 8254 Timer Chip port addresses:
Timer0_8254 equ 40h
Cntrl_8254 equ 43h
; The following ISR reads the 8254 timer chip, negates the result
; (because the timer counts backwards), adds the result to the
; SumLatency variable, and then increments the Executions variable
; that counts the number of times we execute this code. In the
; mean time, the main program is busy computing and displaying the
; average latency time for this ISR.
;
; To read the 16 bit 8254 counter value, this code needs to
; write a zero to the 8254 control port and then read the
; timer port twice (reads the L.O. then H.O. bytes). There
; needs to be a short delay between reading the two bytes
; from the same port address.
TimerISR proc near
push ax
mov eax, 0 ;Ch 0, latch & read data.
out Cntrl_8254, al ;Output to 8253 cmd register.
in al, Timer0_8254 ;Read latch #0 (LSB) & ignore.
mov ah, al
jmp SettleDelay ;Settling delay for 8254 chip.
SettleDelay: in al, Timer0_8254 ;Read latch #0 (MSB)
xchg ah, al
neg ax ;Fix, 'cause timer counts down.
add cseg:SumLatency, eax
inc cseg:Executions
pop ax
jmp cseg:OldInt8
TimerISR endp
Main proc
meminit
; Begin by patching in the address of our ISR into int 8's vector.
; Note that we must turn off the interrupts while actually patching
; the interrupt vector and we must ensure that interrupts are turned
; back on afterwards; hence the cli and sti instructions. These are
; required because a timer interrupt could come along between the two
; instructions that write to the int 8 interrupt vector. Since the
; interrupt vector is in an inconsistent state at that point, this
; could cause the system to crash.
mov ax, 0
mov es, ax
mov ax, es:[8*4]
mov word ptr OldInt8, ax
mov ax, es:[8*4 + 2]
mov word ptr OldInt8+2, ax
cli
mov word ptr es:[8*4], offset TimerISR
mov es:[8*4 + 2], cs
sti
; First, wait for the first call to the ISR above. Since we will be dividing
; by the value in the Executions variable, we need to make sure that it is
; greater than zero before we do anything.
Wait4Non0: cmp cseg:Executions, 0
je Wait4Non0
; Okay, start displaying the good values until the user presses a key at
; the keyboard to stop everything:
DisplayLp: mov eax, SumLatency
cdq ;Extends eax->edx.
div Executions
mov Average, eax
printf
byte "Count: %ld, average: %ld\n",0
dword Executions, Average
mov ah, 1 ;Test for keystroke.
int 16h
je DisplayLp
mov ah, 0 ;Read that keystroke.
int 16h
; Okay, restore the interrupt vector. We need the interrupts off
; here for the same reason as above.
mov ax, 0
mov es, ax
cli
mov ax, word ptr OldInt8
mov es:[8*4], ax
mov ax, word ptr OldInt8+2
mov es:[8*4+2], ax
sti
Quit: ExitPgm ;DOS macro to quit program.
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
On a 66 MHz 80486 DX/2 processor, the above code reports an average value of 44 after it has run for about 10,000 iterations. This works out to about 37 msec between the device signalling the interrupt and the ISR being able to process it[11]. The latency of polled I/O would probably be an order of magnitude less than this!
Generally, if you have some high speed application like audio or video recording or playback, you probably cannot afford the latencies associated with interrupt I/O. On the other hand, such applications demand such high performance out of the system, that you probably wouldn't have any CPU cycles left over to do other processing while waiting for I/O.
Another issue with respect to ISR latency is latency consistency. That is, is there the same amount of latency from interrupt to interrupt? Some ISRs can tolerate considerable latency as long as it is consistent (that is, the latency is roughly the same from interrupt to interrupt). For example, suppose you want to patch into the timer interrupt so you can read an input port every 55 msec and store this data away. Later, when processing the data, your code might work under the assumption that the data readings are 55 msec (or 54.9...) apart. This might not be true if there are other ISRs in the timer interrupt chain before your ISR. For example, there may be an ISR that counts off 18 interrupts and then executes some code sequence that requires 10 msec. This means that 16 out of every 18 interrupts your data collection routine would collect data at 55 msec intervals right on the nose. But when that 18th interrupt occurs, the other timer ISR will delay 10 msec before passing control to your routine. This means that your 17th reading will be 65 msec since the last reading. Don't forget, the timer chip is still counting down during all of this, that means there are now only 45 msec to the next interrupt. Therefore, your 18th reading would occur 45 msec after the 17th. Hardly a consistent pattern. If your ISR needs a consistent latencies, you should try to install your ISR as early in the interrupt chain as possible.
in instruction is probably going to be quite slow because of the wait states associated with external I/O devices.