2 ; *** Listing 2-5 ***
\r
4 ; The long-period Zen timer. (LZTIMER.ASM)
\r
5 ; Uses the 8253 timer and the BIOS time-of-day count to time the
\r
6 ; performance of code that takes less than an hour to execute.
\r
7 ; Because interrupts are left on (in order to allow the timer
\r
8 ; interrupt to be recognized), this is less accurate than the
\r
9 ; precision Zen timer, so it is best used only to time code that takes
\r
10 ; more than about 54 milliseconds to execute (code that the precision
\r
11 ; Zen timer reports overflow on). Resolution is limited by the
\r
12 ; occurrence of timer interrupts.
\r
14 ; By Michael Abrash 4/26/89
\r
16 ; Externally callable routines:
\r
18 ; ZTimerOn: Saves the BIOS time of day count and starts the
\r
19 ; long-period Zen timer.
\r
21 ; ZTimerOff: Stops the long-period Zen timer and saves the timer
\r
22 ; count and the BIOS time-of-day count.
\r
24 ; ZTimerReport: Prints the time that passed between starting and
\r
25 ; stopping the timer.
\r
27 ; Note: If either more than an hour passes or midnight falls between
\r
28 ; calls to ZTimerOn and ZTimerOff, an error is reported. For
\r
29 ; timing code that takes more than a few minutes to execute,
\r
30 ; either the DOS TIME command in a batch file before and after
\r
31 ; execution of the code to time or the use of the DOS
\r
32 ; time-of-day function in place of the long-period Zen timer is
\r
33 ; more than adequate.
\r
35 ; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
\r
36 ; PS2 must be set to 1 on PS/2 computers because the PS/2's
\r
37 ; timers are not compatible with an undocumented timer-stopping
\r
38 ; feature of the 8253; the alternative timing approach that
\r
39 ; must be used on PS/2 computers leaves a short window
\r
40 ; during which the timer 0 count and the BIOS timer count may
\r
41 ; not be synchronized. You should also set the PS2 symbol to
\r
42 ; 1 if you're getting erratic or obviously incorrect results.
\r
44 ; Note: When PS2 is 0, the code relies on an undocumented 8253
\r
45 ; feature to get more reliable readings. It is possible that
\r
46 ; the 8253 (or whatever chip is emulating the 8253) may be put
\r
47 ; into an undefined or incorrect state when this feature is
\r
50 ; ***************************************************************
\r
51 ; * If your computer displays any hint of erratic behavior *
\r
52 ; * after the long-period Zen timer is used, such as the floppy *
\r
53 ; * drive failing to operate properly, reboot the system, set *
\r
54 ; * PS2 to 1 and leave it that way! *
\r
55 ; ***************************************************************
\r
57 ; Note: Each block of code being timed should ideally be run several
\r
58 ; times, with at least two similar readings required to
\r
59 ; establish a true measurement, in order to eliminate any
\r
60 ; variability caused by interrupts.
\r
62 ; Note: Interrupts must not be disabled for more than 54 ms at a
\r
63 ; stretch during the timing interval. Because interrupts
\r
64 ; are enabled, keys, mice, and other devices that generate
\r
65 ; interrupts should not be used during the timing interval.
\r
67 ; Note: Any extra code running off the timer interrupt (such as
\r
68 ; some memory-resident utilities) will increase the time
\r
69 ; measured by the Zen timer.
\r
71 ; Note: These routines can introduce inaccuracies of up to a few
\r
72 ; tenths of a second into the system clock count for each
\r
73 ; code section timed. Consequently, it's a good idea to
\r
74 ; reboot at the conclusion of timing sessions. (The
\r
75 ; battery-backed clock, if any, is not affected by the Zen
\r
78 ; All registers and all flags are preserved by all routines.
\r
83 public ZTimerOn, ZTimerOff, ZTimerReport
\r
86 ; Set PS2 to 0 to assemble for use on a fully 8253-compatible
\r
87 ; system; when PS2 is 0, the readings are more reliable if the
\r
88 ; computer supports the undocumented timer-stopping feature,
\r
89 ; but may be badly off if that feature is not supported. In
\r
90 ; fact, timer-stopping may interfere with your computer's
\r
91 ; overall operation by putting the 8253 into an undefined or
\r
92 ; incorrect state. Use with caution!!!
\r
94 ; Set PS2 to 1 to assemble for use on non-8253-compatible
\r
95 ; systems, including PS/2 computers; when PS2 is 1, readings
\r
96 ; may occasionally be off by 54 ms, but the code will work
\r
97 ; properly on all systems.
\r
99 ; A setting of 1 is safer and will work on more systems,
\r
100 ; while a setting of 0 produces more reliable results in systems
\r
101 ; which support the undocumented timer-stopping feature of the
\r
102 ; 8253. The choice is yours.
\r
106 ; Base address of the 8253 timer chip.
\r
110 ; The address of the timer 0 count registers in the 8253.
\r
112 TIMER_0_8253 equ BASE_8253 + 0
\r
114 ; The address of the mode register in the 8253.
\r
116 MODE_8253 equ BASE_8253 + 3
\r
118 ; The address of the BIOS timer count variable in the BIOS
\r
121 TIMER_COUNT equ 46ch
\r
123 ; Macro to emulate a POPF instruction in order to fix the bug in some
\r
124 ; 80286 chips which allows interrupts to occur during a POPF even when
\r
125 ; interrupts remain disabled.
\r
130 p1: iret ;jump to pushed address & pop flags
\r
131 p2: push cs ;construct far return address to
\r
132 call p1 ; the next instruction
\r
136 ; Macro to delay briefly to ensure that enough time has elapsed
\r
137 ; between successive I/O accesses so that the device being accessed
\r
138 ; can respond to both accesses even on a very fast PC.
\r
146 StartBIOSCountLow dw ? ;BIOS count low word at the
\r
147 ; start of the timing period
\r
148 StartBIOSCountHigh dw ? ;BIOS count high word at the
\r
149 ; start of the timing period
\r
150 EndBIOSCountLow dw ? ;BIOS count low word at the
\r
151 ; end of the timing period
\r
152 EndBIOSCountHigh dw ? ;BIOS count high word at the
\r
153 ; end of the timing period
\r
154 EndTimedCount dw ? ;timer 0 count at the end of
\r
155 ; the timing period
\r
156 ReferenceCount dw ? ;number of counts required to
\r
157 ; execute timer overhead code
\r
159 ; String printed to report results.
\r
161 OutputStr label byte
\r
162 db 0dh, 0ah, 'Timed count: '
\r
163 TimedCountStr db 10 dup (?)
\r
164 db ' microseconds', 0dh, 0ah
\r
167 ; Temporary storage for timed count as it's divided down by powers
\r
168 ; of ten when converting from doubleword binary to ASCII.
\r
170 CurrentCountLow dw ?
\r
171 CurrentCountHigh dw ?
\r
173 ; Powers of ten table used to perform division by 10 when doing
\r
174 ; doubleword conversion from binary to ASCII.
\r
176 PowersOfTen label word
\r
187 PowersOfTenEnd label word
\r
189 ; String printed to report that the high word of the BIOS count
\r
190 ; changed while timing (an hour elapsed or midnight was crossed),
\r
191 ; and so the count is invalid and the test needs to be rerun.
\r
193 TurnOverStr label byte
\r
195 db '****************************************************'
\r
197 db '* Either midnight passed or an hour or more passed *'
\r
199 db '* while timing was in progress. If the former was *'
\r
201 db '* the case, please rerun the test; if the latter *'
\r
203 db '* was the case, the test code takes too long to *'
\r
205 db '* run to be timed by the long-period Zen timer. *'
\r
207 db '* Suggestions: use the DOS TIME command, the DOS *'
\r
209 db '* time function, or a watch. *'
\r
211 db '****************************************************'
\r
215 ;********************************************************************
\r
216 ;* Routine called to start timing. *
\r
217 ;********************************************************************
\r
222 ; Save the context of the program being timed.
\r
227 ; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
\r
228 ; linear counting rather than count-by-two counting. Also stops
\r
229 ; timer 0 until the timer count is loaded, except on PS/2
\r
232 mov al,00110100b ;mode 2
\r
235 ; Set the timer count to 0, so we know we won't get another
\r
236 ; timer interrupt right away.
\r
237 ; Note: this introduces an inaccuracy of up to 54 ms in the system
\r
238 ; clock count each time it is executed.
\r
242 out TIMER_0_8253,al ;lsb
\r
244 out TIMER_0_8253,al ;msb
\r
246 ; In case interrupts are disabled, enable interrupts briefly to allow
\r
247 ; the interrupt generated when switching from mode 3 to mode 2 to be
\r
248 ; recognized. Interrupts must be enabled for at least 210 ns to allow
\r
249 ; time for that interrupt to occur. Here, 10 jumps are used for the
\r
250 ; delay to ensure that the delay time will be more than long enough
\r
251 ; even on a very fast PC.
\r
260 ; Store the timing start BIOS count.
\r
261 ; (Since the timer count was just set to 0, the BIOS count will
\r
262 ; stay the same for the next 54 ms, so we don't need to disable
\r
263 ; interrupts in order to avoid getting a half-changed count.)
\r
268 mov ax,ds:[TIMER_COUNT+2]
\r
269 mov cs:[StartBIOSCountHigh],ax
\r
270 mov ax,ds:[TIMER_COUNT]
\r
271 mov cs:[StartBIOSCountLow],ax
\r
274 ; Set the timer count to 0 again to start the timing interval.
\r
276 mov al,00110100b ;set up to load initial
\r
277 out MODE_8253,al ; timer count
\r
280 out TIMER_0_8253,al ;load count lsb
\r
282 out TIMER_0_8253,al ;load count msb
\r
284 ; Restore the context of the program being timed and return to it.
\r
292 ;********************************************************************
\r
293 ;* Routine called to stop timing and get count. *
\r
294 ;********************************************************************
\r
296 ZTimerOff proc near
\r
299 ; Save the context of the program being timed.
\r
305 ; In case interrupts are disabled, enable interrupts briefly to allow
\r
306 ; any pending timer interrupt to be handled. Interrupts must be
\r
307 ; enabled for at least 210 ns to allow time for that interrupt to
\r
308 ; occur. Here, 10 jumps are used for the delay to ensure that the
\r
309 ; delay time will be more than long enough even on a very fast PC.
\r
317 ; Latch the timer count.
\r
323 out MODE_8253,al ;latch timer 0 count
\r
325 ; This is where a one-instruction-long window exists on the PS/2.
\r
326 ; The timer count and the BIOS count can lose synchronization;
\r
327 ; since the timer keeps counting after it's latched, it can turn
\r
328 ; over right after it's latched and cause the BIOS count to turn
\r
329 ; over before interrupts are disabled, leaving us with the timer
\r
330 ; count from before the timer turned over coupled with the BIOS
\r
331 ; count from after the timer turned over. The result is a count
\r
332 ; that's 54 ms too long.
\r
338 ; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count
\r
339 ; load, which stops timer 0 until the count is loaded. (Only works
\r
340 ; on fully 8253-compatible chips.)
\r
342 mov al,00110100b ;mode 2
\r
345 mov al,00000000b ;latch timer 0 count
\r
350 cli ;stop the BIOS count
\r
352 ; Read the BIOS count. (Since interrupts are disabled, the BIOS
\r
353 ; count won't change.)
\r
358 mov ax,ds:[TIMER_COUNT+2]
\r
359 mov cs:[EndBIOSCountHigh],ax
\r
360 mov ax,ds:[TIMER_COUNT]
\r
361 mov cs:[EndBIOSCountLow],ax
\r
364 ; Read the timer count and save it.
\r
366 in al,TIMER_0_8253 ;lsb
\r
369 in al,TIMER_0_8253 ;msb
\r
371 neg ax ;convert from countdown
\r
372 ; remaining to elapsed
\r
374 mov cs:[EndTimedCount],ax
\r
376 ; Restart timer 0, which is still waiting for an initial count
\r
383 mov al,00110100b ;mode 2, waiting to load a
\r
388 out TIMER_0_8253,al ;lsb
\r
391 out TIMER_0_8253,al ;msb
\r
396 sti ;let the BIOS count continue
\r
398 ; Time a zero-length code fragment, to get a reference for how
\r
399 ; much overhead this routine has. Time it 16 times and average it,
\r
400 ; for accuracy, rounding the result.
\r
402 mov cs:[ReferenceCount],0
\r
404 cli ;interrupts off to allow a
\r
405 ; precise reference count
\r
407 call ReferenceZTimerOn
\r
408 call ReferenceZTimerOff
\r
411 add cs:[ReferenceCount],8 ;total + (0.5 * 16)
\r
413 shr cs:[ReferenceCount],cl ;(total) / 16 + 0.5
\r
415 ; Restore the context of the program being timed and return to it.
\r
425 ; Called by ZTimerOff to start the timer for overhead measurements.
\r
428 ReferenceZTimerOn proc near
\r
430 ; Save the context of the program being timed.
\r
435 ; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
\r
436 ; linear counting rather than count-by-two counting.
\r
438 mov al,00110100b ;mode 2
\r
441 ; Set the timer count to 0.
\r
445 out TIMER_0_8253,al ;lsb
\r
447 out TIMER_0_8253,al ;msb
\r
449 ; Restore the context of the program being timed and return to it.
\r
455 ReferenceZTimerOn endp
\r
458 ; Called by ZTimerOff to stop the timer and add the result to
\r
459 ; ReferenceCount for overhead measurements. Doesn't need to look
\r
460 ; at the BIOS count because timing a zero-length code fragment
\r
461 ; isn't going to take anywhere near 54 ms.
\r
464 ReferenceZTimerOff proc near
\r
466 ; Save the context of the program being timed.
\r
473 ; Match the interrupt-window delay in ZTimerOff.
\r
481 out MODE_8253,al ;latch timer
\r
483 ; Read the count and save it.
\r
486 in al,TIMER_0_8253 ;lsb
\r
489 in al,TIMER_0_8253 ;msb
\r
491 neg ax ;convert from countdown
\r
492 ; remaining to elapsed
\r
494 add cs:[ReferenceCount],ax
\r
496 ; Restore the context and return.
\r
503 ReferenceZTimerOff endp
\r
505 ;********************************************************************
\r
506 ;* Routine called to report timing results. *
\r
507 ;********************************************************************
\r
509 ZTimerReport proc near
\r
520 push cs ;DOS functions require that DS point
\r
521 pop ds ; to text to be displayed on the screen
\r
524 ; See if midnight or more than an hour passed during timing. If so,
\r
527 mov ax,[StartBIOSCountHigh]
\r
528 cmp ax,[EndBIOSCountHigh]
\r
529 jz CalcBIOSTime ;hour count didn't change,
\r
530 ; so everything's fine
\r
532 cmp ax,[EndBIOSCountHigh]
\r
533 jnz TestTooLong ;midnight or two hour
\r
534 ; boundaries passed, so the
\r
535 ; results are no good
\r
536 mov ax,[EndBIOSCountLow]
\r
537 cmp ax,[StartBIOSCountLow]
\r
538 jb CalcBIOSTime ;a single hour boundary
\r
539 ; passed-that's OK, so long as
\r
540 ; the total time wasn't more
\r
544 ; Over an hour elapsed or midnight passed during timing, which
\r
545 ; renders the results invalid. Notify the user. This misses the
\r
546 ; case where a multiple of 24 hours has passed, but we'll rely
\r
547 ; on the perspicacity of the user to detect that case.
\r
551 mov dx,offset TurnOverStr
\r
553 jmp short ZTimerReportDone
\r
555 ; Convert the BIOS time to microseconds.
\r
558 mov ax,[EndBIOSCountLow]
\r
559 sub ax,[StartBIOSCountLow]
\r
560 mov dx,54925 ;number of microseconds each
\r
561 ; BIOS count represents
\r
563 mov bx,ax ;set aside BIOS count in
\r
564 mov cx,dx ; microseconds
\r
566 ; Convert timer count to microseconds.
\r
568 mov ax,[EndTimedCount]
\r
572 div si ;* .8381 = * 8381 / 10000
\r
574 ; Add timer and BIOS counts together to get an overall time in
\r
580 ; Subtract the timer overhead and save the result.
\r
582 mov ax,[ReferenceCount]
\r
583 mov si,8381 ;convert the reference count
\r
584 mul si ; to microseconds
\r
586 div si ;* .8381 = * 8381 / 10000
\r
589 mov [CurrentCountLow],bx
\r
590 mov [CurrentCountHigh],cx
\r
592 ; Convert the result to an ASCII string by trial subtractions of
\r
595 mov di,offset PowersOfTenEnd - offset PowersOfTen - 4
\r
596 mov si,offset TimedCountStr
\r
600 mov ax,[CurrentCountLow]
\r
601 mov dx,[CurrentCountHigh]
\r
602 sub ax,PowersOfTen[di]
\r
603 sbb dx,PowersOfTen[di+2]
\r
604 jc CTSNextPowerDown
\r
606 mov [CurrentCountLow],ax
\r
607 mov [CurrentCountHigh],dx
\r
616 ; Print the results.
\r
619 mov dx,offset OutputStr
\r