--- /dev/null
+;\r
+; *** Listing 2-5 ***\r
+;\r
+; The long-period Zen timer. (LZTIMER.ASM)\r
+; Uses the 8253 timer and the BIOS time-of-day count to time the\r
+; performance of code that takes less than an hour to execute.\r
+; Because interrupts are left on (in order to allow the timer\r
+; interrupt to be recognized), this is less accurate than the\r
+; precision Zen timer, so it is best used only to time code that takes\r
+; more than about 54 milliseconds to execute (code that the precision\r
+; Zen timer reports overflow on). Resolution is limited by the\r
+; occurrence of timer interrupts.\r
+;\r
+; By Michael Abrash 4/26/89\r
+;\r
+; Externally callable routines:\r
+;\r
+; ZTimerOn: Saves the BIOS time of day count and starts the\r
+; long-period Zen timer.\r
+;\r
+; ZTimerOff: Stops the long-period Zen timer and saves the timer\r
+; count and the BIOS time-of-day count.\r
+;\r
+; ZTimerReport: Prints the time that passed between starting and\r
+; stopping the timer.\r
+;\r
+; Note: If either more than an hour passes or midnight falls between\r
+; calls to ZTimerOn and ZTimerOff, an error is reported. For\r
+; timing code that takes more than a few minutes to execute,\r
+; either the DOS TIME command in a batch file before and after\r
+; execution of the code to time or the use of the DOS\r
+; time-of-day function in place of the long-period Zen timer is\r
+; more than adequate.\r
+;\r
+; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.\r
+; PS2 must be set to 1 on PS/2 computers because the PS/2's\r
+; timers are not compatible with an undocumented timer-stopping\r
+; feature of the 8253; the alternative timing approach that\r
+; must be used on PS/2 computers leaves a short window\r
+; during which the timer 0 count and the BIOS timer count may\r
+; not be synchronized. You should also set the PS2 symbol to\r
+; 1 if you're getting erratic or obviously incorrect results.\r
+;\r
+; Note: When PS2 is 0, the code relies on an undocumented 8253\r
+; feature to get more reliable readings. It is possible that\r
+; the 8253 (or whatever chip is emulating the 8253) may be put\r
+; into an undefined or incorrect state when this feature is\r
+; used.\r
+;\r
+; ***************************************************************\r
+; * If your computer displays any hint of erratic behavior *\r
+; * after the long-period Zen timer is used, such as the floppy *\r
+; * drive failing to operate properly, reboot the system, set *\r
+; * PS2 to 1 and leave it that way! *\r
+; ***************************************************************\r
+;\r
+; Note: Each block of code being timed should ideally be run several\r
+; times, with at least two similar readings required to\r
+; establish a true measurement, in order to eliminate any\r
+; variability caused by interrupts.\r
+;\r
+; Note: Interrupts must not be disabled for more than 54 ms at a\r
+; stretch during the timing interval. Because interrupts\r
+; are enabled, keys, mice, and other devices that generate\r
+; interrupts should not be used during the timing interval.\r
+;\r
+; Note: Any extra code running off the timer interrupt (such as\r
+; some memory-resident utilities) will increase the time\r
+; measured by the Zen timer.\r
+;\r
+; Note: These routines can introduce inaccuracies of up to a few\r
+; tenths of a second into the system clock count for each\r
+; code section timed. Consequently, it's a good idea to\r
+; reboot at the conclusion of timing sessions. (The\r
+; battery-backed clock, if any, is not affected by the Zen\r
+; timer.)\r
+;\r
+; All registers and all flags are preserved by all routines.\r
+;\r
+ DOSSEG\r
+ .model small\r
+ .code\r
+ public ZTimerOn, ZTimerOff, ZTimerReport\r
+\r
+;\r
+; Set PS2 to 0 to assemble for use on a fully 8253-compatible\r
+; system; when PS2 is 0, the readings are more reliable if the\r
+; computer supports the undocumented timer-stopping feature,\r
+; but may be badly off if that feature is not supported. In\r
+; fact, timer-stopping may interfere with your computer's\r
+; overall operation by putting the 8253 into an undefined or\r
+; incorrect state. Use with caution!!!\r
+;\r
+; Set PS2 to 1 to assemble for use on non-8253-compatible\r
+; systems, including PS/2 computers; when PS2 is 1, readings\r
+; may occasionally be off by 54 ms, but the code will work\r
+; properly on all systems.\r
+;\r
+; A setting of 1 is safer and will work on more systems,\r
+; while a setting of 0 produces more reliable results in systems\r
+; which support the undocumented timer-stopping feature of the\r
+; 8253. The choice is yours.\r
+;\r
+PS2 equ 1\r
+;\r
+; Base address of the 8253 timer chip.\r
+;\r
+BASE_8253 equ 40h\r
+;\r
+; The address of the timer 0 count registers in the 8253.\r
+;\r
+TIMER_0_8253 equ BASE_8253 + 0\r
+;\r
+; The address of the mode register in the 8253.\r
+;\r
+MODE_8253 equ BASE_8253 + 3\r
+;\r
+; The address of the BIOS timer count variable in the BIOS\r
+; data segment.\r
+;\r
+TIMER_COUNT equ 46ch\r
+;\r
+; Macro to emulate a POPF instruction in order to fix the bug in some\r
+; 80286 chips which allows interrupts to occur during a POPF even when\r
+; interrupts remain disabled.\r
+;\r
+MPOPF macro\r
+ local p1, p2\r
+ jmp short p2\r
+p1: iret ;jump to pushed address & pop flags\r
+p2: push cs ;construct far return address to\r
+ call p1 ; the next instruction\r
+ endm\r
+\r
+;\r
+; Macro to delay briefly to ensure that enough time has elapsed\r
+; between successive I/O accesses so that the device being accessed\r
+; can respond to both accesses even on a very fast PC.\r
+;\r
+DELAY macro\r
+ jmp $+2\r
+ jmp $+2\r
+ jmp $+2\r
+ endm\r
+\r
+StartBIOSCountLow dw ? ;BIOS count low word at the\r
+ ; start of the timing period\r
+StartBIOSCountHigh dw ? ;BIOS count high word at the\r
+ ; start of the timing period\r
+EndBIOSCountLow dw ? ;BIOS count low word at the\r
+ ; end of the timing period\r
+EndBIOSCountHigh dw ? ;BIOS count high word at the\r
+ ; end of the timing period\r
+EndTimedCount dw ? ;timer 0 count at the end of\r
+ ; the timing period\r
+ReferenceCount dw ? ;number of counts required to\r
+ ; execute timer overhead code\r
+;\r
+; String printed to report results.\r
+;\r
+OutputStr label byte\r
+ db 0dh, 0ah, 'Timed count: '\r
+TimedCountStr db 10 dup (?)\r
+ db ' microseconds', 0dh, 0ah\r
+ db '$'\r
+;\r
+; Temporary storage for timed count as it's divided down by powers\r
+; of ten when converting from doubleword binary to ASCII.\r
+;\r
+CurrentCountLow dw ?\r
+CurrentCountHigh dw ?\r
+;\r
+; Powers of ten table used to perform division by 10 when doing\r
+; doubleword conversion from binary to ASCII.\r
+;\r
+PowersOfTen label word\r
+ dd 1\r
+ dd 10\r
+ dd 100\r
+ dd 1000\r
+ dd 10000\r
+ dd 100000\r
+ dd 1000000\r
+ dd 10000000\r
+ dd 100000000\r
+ dd 1000000000\r
+PowersOfTenEnd label word\r
+;\r
+; String printed to report that the high word of the BIOS count\r
+; changed while timing (an hour elapsed or midnight was crossed),\r
+; and so the count is invalid and the test needs to be rerun.\r
+;\r
+TurnOverStr label byte\r
+ db 0dh, 0ah\r
+ db '****************************************************'\r
+ db 0dh, 0ah\r
+ db '* Either midnight passed or an hour or more passed *'\r
+ db 0dh, 0ah\r
+ db '* while timing was in progress. If the former was *'\r
+ db 0dh, 0ah\r
+ db '* the case, please rerun the test; if the latter *'\r
+ db 0dh, 0ah\r
+ db '* was the case, the test code takes too long to *'\r
+ db 0dh, 0ah\r
+ db '* run to be timed by the long-period Zen timer. *'\r
+ db 0dh, 0ah\r
+ db '* Suggestions: use the DOS TIME command, the DOS *'\r
+ db 0dh, 0ah\r
+ db '* time function, or a watch. *'\r
+ db 0dh, 0ah\r
+ db '****************************************************'\r
+ db 0dh, 0ah\r
+ db '$'\r
+\r
+;********************************************************************\r
+;* Routine called to start timing. *\r
+;********************************************************************\r
+\r
+ZTimerOn proc near\r
+\r
+;\r
+; Save the context of the program being timed.\r
+;\r
+ push ax\r
+ pushf\r
+;\r
+; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause\r
+; linear counting rather than count-by-two counting. Also stops\r
+; timer 0 until the timer count is loaded, except on PS/2\r
+; computers.\r
+;\r
+ mov al,00110100b ;mode 2\r
+ out MODE_8253,al\r
+;\r
+; Set the timer count to 0, so we know we won't get another\r
+; timer interrupt right away.\r
+; Note: this introduces an inaccuracy of up to 54 ms in the system\r
+; clock count each time it is executed.\r
+;\r
+ DELAY\r
+ sub al,al\r
+ out TIMER_0_8253,al ;lsb\r
+ DELAY\r
+ out TIMER_0_8253,al ;msb\r
+;\r
+; In case interrupts are disabled, enable interrupts briefly to allow\r
+; the interrupt generated when switching from mode 3 to mode 2 to be\r
+; recognized. Interrupts must be enabled for at least 210 ns to allow\r
+; time for that interrupt to occur. Here, 10 jumps are used for the\r
+; delay to ensure that the delay time will be more than long enough\r
+; even on a very fast PC.\r
+;\r
+ pushf\r
+ sti\r
+ rept 10\r
+ jmp $+2\r
+ endm\r
+ MPOPF\r
+;\r
+; Store the timing start BIOS count.\r
+; (Since the timer count was just set to 0, the BIOS count will\r
+; stay the same for the next 54 ms, so we don't need to disable\r
+; interrupts in order to avoid getting a half-changed count.)\r
+;\r
+ push ds\r
+ sub ax,ax\r
+ mov ds,ax\r
+ mov ax,ds:[TIMER_COUNT+2]\r
+ mov cs:[StartBIOSCountHigh],ax\r
+ mov ax,ds:[TIMER_COUNT]\r
+ mov cs:[StartBIOSCountLow],ax\r
+ pop ds\r
+;\r
+; Set the timer count to 0 again to start the timing interval.\r
+;\r
+ mov al,00110100b ;set up to load initial\r
+ out MODE_8253,al ; timer count\r
+ DELAY\r
+ sub al,al\r
+ out TIMER_0_8253,al ;load count lsb\r
+ DELAY\r
+ out TIMER_0_8253,al ;load count msb\r
+;\r
+; Restore the context of the program being timed and return to it.\r
+;\r
+ MPOPF\r
+ pop ax\r
+ ret\r
+\r
+ZTimerOn endp\r
+\r
+;********************************************************************\r
+;* Routine called to stop timing and get count. *\r
+;********************************************************************\r
+\r
+ZTimerOff proc near\r
+\r
+;\r
+; Save the context of the program being timed.\r
+;\r
+ pushf\r
+ push ax\r
+ push cx\r
+;\r
+; In case interrupts are disabled, enable interrupts briefly to allow\r
+; any pending timer interrupt to be handled. Interrupts must be\r
+; enabled for at least 210 ns to allow time for that interrupt to\r
+; occur. Here, 10 jumps are used for the delay to ensure that the\r
+; delay time will be more than long enough even on a very fast PC.\r
+;\r
+ sti\r
+ rept 10\r
+ jmp $+2\r
+ endm\r
+\r
+;\r
+; Latch the timer count.\r
+;\r
+\r
+if PS2\r
+\r
+ mov al,00000000b\r
+ out MODE_8253,al ;latch timer 0 count\r
+;\r
+; This is where a one-instruction-long window exists on the PS/2.\r
+; The timer count and the BIOS count can lose synchronization;\r
+; since the timer keeps counting after it's latched, it can turn\r
+; over right after it's latched and cause the BIOS count to turn\r
+; over before interrupts are disabled, leaving us with the timer\r
+; count from before the timer turned over coupled with the BIOS\r
+; count from after the timer turned over. The result is a count\r
+; that's 54 ms too long.\r
+;\r
+\r
+else\r
+\r
+;\r
+; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count\r
+; load, which stops timer 0 until the count is loaded. (Only works\r
+; on fully 8253-compatible chips.)\r
+;\r
+ mov al,00110100b ;mode 2\r
+ out MODE_8253,al\r
+ DELAY\r
+ mov al,00000000b ;latch timer 0 count\r
+ out MODE_8253,al\r
+\r
+endif\r
+\r
+ cli ;stop the BIOS count\r
+;\r
+; Read the BIOS count. (Since interrupts are disabled, the BIOS\r
+; count won't change.)\r
+;\r
+ push ds\r
+ sub ax,ax\r
+ mov ds,ax\r
+ mov ax,ds:[TIMER_COUNT+2]\r
+ mov cs:[EndBIOSCountHigh],ax\r
+ mov ax,ds:[TIMER_COUNT]\r
+ mov cs:[EndBIOSCountLow],ax\r
+ pop ds\r
+;\r
+; Read the timer count and save it.\r
+;\r
+ in al,TIMER_0_8253 ;lsb\r
+ DELAY\r
+ mov ah,al\r
+ in al,TIMER_0_8253 ;msb\r
+ xchg ah,al\r
+ neg ax ;convert from countdown\r
+ ; remaining to elapsed\r
+ ; count\r
+ mov cs:[EndTimedCount],ax\r
+;\r
+; Restart timer 0, which is still waiting for an initial count\r
+; to be loaded.\r
+;\r
+\r
+ife PS2\r
+\r
+ DELAY\r
+ mov al,00110100b ;mode 2, waiting to load a\r
+ ; 2-byte count\r
+ out MODE_8253,al\r
+ DELAY\r
+ sub al,al\r
+ out TIMER_0_8253,al ;lsb\r
+ DELAY\r
+ mov al,ah\r
+ out TIMER_0_8253,al ;msb\r
+ DELAY\r
+\r
+endif\r
+\r
+ sti ;let the BIOS count continue\r
+;\r
+; Time a zero-length code fragment, to get a reference for how\r
+; much overhead this routine has. Time it 16 times and average it,\r
+; for accuracy, rounding the result.\r
+;\r
+ mov cs:[ReferenceCount],0\r
+ mov cx,16\r
+ cli ;interrupts off to allow a\r
+ ; precise reference count\r
+RefLoop:\r
+ call ReferenceZTimerOn\r
+ call ReferenceZTimerOff\r
+ loop RefLoop\r
+ sti\r
+ add cs:[ReferenceCount],8 ;total + (0.5 * 16)\r
+ mov cl,4\r
+ shr cs:[ReferenceCount],cl ;(total) / 16 + 0.5\r
+;\r
+; Restore the context of the program being timed and return to it.\r
+;\r
+ pop cx\r
+ pop ax\r
+ MPOPF\r
+ ret\r
+\r
+ZTimerOff endp\r
+\r
+;\r
+; Called by ZTimerOff to start the timer for overhead measurements.\r
+;\r
+\r
+ReferenceZTimerOn proc near\r
+;\r
+; Save the context of the program being timed.\r
+;\r
+ push ax\r
+ pushf\r
+;\r
+; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause\r
+; linear counting rather than count-by-two counting.\r
+;\r
+ mov al,00110100b ;mode 2\r
+ out MODE_8253,al\r
+;\r
+; Set the timer count to 0.\r
+;\r
+ DELAY\r
+ sub al,al\r
+ out TIMER_0_8253,al ;lsb\r
+ DELAY\r
+ out TIMER_0_8253,al ;msb\r
+;\r
+; Restore the context of the program being timed and return to it.\r
+;\r
+ MPOPF\r
+ pop ax\r
+ ret\r
+\r
+ReferenceZTimerOn endp\r
+\r
+;\r
+; Called by ZTimerOff to stop the timer and add the result to\r
+; ReferenceCount for overhead measurements. Doesn't need to look\r
+; at the BIOS count because timing a zero-length code fragment\r
+; isn't going to take anywhere near 54 ms.\r
+;\r
+\r
+ReferenceZTimerOff proc near\r
+;\r
+; Save the context of the program being timed.\r
+;\r
+ pushf\r
+ push ax\r
+ push cx\r
+\r
+;\r
+; Match the interrupt-window delay in ZTimerOff.\r
+;\r
+ sti\r
+ rept 10\r
+ jmp $+2\r
+ endm\r
+\r
+ mov al,00000000b\r
+ out MODE_8253,al ;latch timer\r
+;\r
+; Read the count and save it.\r
+;\r
+ DELAY\r
+ in al,TIMER_0_8253 ;lsb\r
+ DELAY\r
+ mov ah,al\r
+ in al,TIMER_0_8253 ;msb\r
+ xchg ah,al\r
+ neg ax ;convert from countdown\r
+ ; remaining to elapsed\r
+ ; count\r
+ add cs:[ReferenceCount],ax\r
+;\r
+; Restore the context and return.\r
+;\r
+ pop cx\r
+ pop ax\r
+ MPOPF\r
+ ret\r
+\r
+ReferenceZTimerOff endp\r
+\r
+;********************************************************************\r
+;* Routine called to report timing results. *\r
+;********************************************************************\r
+\r
+ZTimerReport proc near\r
+\r
+ pushf\r
+ push ax\r
+ push bx\r
+ push cx\r
+ push dx\r
+ push si\r
+ push di\r
+ push ds\r
+;\r
+ push cs ;DOS functions require that DS point\r
+ pop ds ; to text to be displayed on the screen\r
+ assume ds:_TEXT\r
+;\r
+; See if midnight or more than an hour passed during timing. If so,\r
+; notify the user.\r
+;\r
+ mov ax,[StartBIOSCountHigh]\r
+ cmp ax,[EndBIOSCountHigh]\r
+ jz CalcBIOSTime ;hour count didn't change,\r
+ ; so everything's fine\r
+ inc ax\r
+ cmp ax,[EndBIOSCountHigh]\r
+ jnz TestTooLong ;midnight or two hour\r
+ ; boundaries passed, so the\r
+ ; results are no good\r
+ mov ax,[EndBIOSCountLow]\r
+ cmp ax,[StartBIOSCountLow]\r
+ jb CalcBIOSTime ;a single hour boundary\r
+ ; passed-that's OK, so long as\r
+ ; the total time wasn't more\r
+ ; than an hour\r
+\r
+;\r
+; Over an hour elapsed or midnight passed during timing, which\r
+; renders the results invalid. Notify the user. This misses the\r
+; case where a multiple of 24 hours has passed, but we'll rely\r
+; on the perspicacity of the user to detect that case.\r
+;\r
+TestTooLong:\r
+ mov ah,9\r
+ mov dx,offset TurnOverStr\r
+ int 21h\r
+ jmp short ZTimerReportDone\r
+;\r
+; Convert the BIOS time to microseconds.\r
+;\r
+CalcBIOSTime:\r
+ mov ax,[EndBIOSCountLow]\r
+ sub ax,[StartBIOSCountLow]\r
+ mov dx,54925 ;number of microseconds each\r
+ ; BIOS count represents\r
+ mul dx\r
+ mov bx,ax ;set aside BIOS count in\r
+ mov cx,dx ; microseconds\r
+;\r
+; Convert timer count to microseconds.\r
+;\r
+ mov ax,[EndTimedCount]\r
+ mov si,8381\r
+ mul si\r
+ mov si,10000\r
+ div si ;* .8381 = * 8381 / 10000\r
+;\r
+; Add timer and BIOS counts together to get an overall time in\r
+; microseconds.\r
+;\r
+ add bx,ax\r
+ adc cx,0\r
+;\r
+; Subtract the timer overhead and save the result.\r
+;\r
+ mov ax,[ReferenceCount]\r
+ mov si,8381 ;convert the reference count\r
+ mul si ; to microseconds\r
+ mov si,10000\r
+ div si ;* .8381 = * 8381 / 10000\r
+ sub bx,ax\r
+ sbb cx,0\r
+ mov [CurrentCountLow],bx\r
+ mov [CurrentCountHigh],cx\r
+;\r
+; Convert the result to an ASCII string by trial subtractions of\r
+; powers of 10.\r
+;\r
+ mov di,offset PowersOfTenEnd - offset PowersOfTen - 4\r
+ mov si,offset TimedCountStr\r
+CTSNextDigit:\r
+ mov bl,'0'\r
+CTSLoop:\r
+ mov ax,[CurrentCountLow]\r
+ mov dx,[CurrentCountHigh]\r
+ sub ax,PowersOfTen[di]\r
+ sbb dx,PowersOfTen[di+2]\r
+ jc CTSNextPowerDown\r
+ inc bl\r
+ mov [CurrentCountLow],ax\r
+ mov [CurrentCountHigh],dx\r
+ jmp CTSLoop\r
+CTSNextPowerDown:\r
+ mov [si],bl\r
+ inc si\r
+ sub di,4\r
+ jns CTSNextDigit\r
+;\r
+;\r
+; Print the results.\r
+;\r
+ mov ah,9\r
+ mov dx,offset OutputStr\r
+ int 21h\r
+;\r
+ZTimerReportDone:\r
+ pop ds\r
+ pop di\r
+ pop si\r
+ pop dx\r
+ pop cx\r
+ pop bx\r
+ pop ax\r
+ MPOPF\r
+ ret\r
+\r
+ZTimerReport endp\r
+\r
+ end\r
+\1a
\ No newline at end of file