;
; *** Listing A ***
; The C-near-callable version of the long-period Zen timer
;       (LZTIMER.ASM)
;
; The long-period Zen timer. (PCLTNEAR.ASM)
; Uses the 8253 timer and the BIOS time-of-day count to time the
; performance of code that takes less than an hour to execute.
; Because interrupts are left on (in order to allow the timer
; interrupt to be recognized), this is less accurate than the
; precision Zen timer, so it is best used only to time code that takes
; more than about 54 milliseconds to execute (code that the precision
; Zen timer reports overflow on). Resolution is limited by the
; occurrence of timer interrupts.
;
; By Michael Abrash
;
; Externally callable routines:
;
;  ZTimerOn: Saves the BIOS time of day count and starts the
;	long-period Zen timer.
;
;  ZTimerOff: Stops the long-period Zen timer and saves the timer
;	count and the BIOS time-of-day count.
;
;  ZTimerReport: Prints the time that passed between starting and
;	stopping the timer.
;
; Note: If either more than an hour passes or midnight falls between
;	calls to ZTimerOn and ZTimerOff, an error is reported. For
;	timing code that takes more than a few minutes to execute,
;	either the DOS TIME command in a batch file before and after
;	execution of the code to time or the use of the DOS
;	time-of-day function in place of the long-period Zen timer is
;	more than adequate.
;
; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
;	PS2 must be set to 1 on PS/2 computers because the PS/2's
;	timers are not compatible with an undocumented timer-stopping
;	feature of the 8253; the alternative timing approach that
;	must be used on PS/2 computers leaves a short window
;	during which the timer 0 count and the BIOS timer count may
;	not be synchronized. You should also set the PS2 symbol to
;	1 if you're getting erratic or obviously incorrect results.
;
; Note: When PS2 is 0, the code relies on an undocumented 8253
;	feature to get more reliable readings. It is possible that
;	the 8253 (or whatever chip is emulating the 8253) may be put
;	into an undefined or incorrect state when this feature is
;	used.
;
;     ***************************************************************
;     * If your computer displays any hint of erratic behavior	    *
;     *	after the long-period Zen timer is used, such as the floppy *
;     *	drive failing to operate properly, reboot the system, set   *
;     *	PS2 to 1 and leave it that way!				    *
;     ***************************************************************
;
; Note: Each block of code being timed should ideally be run several
;	times, with at least two similar readings required to
;	establish a true measurement, in order to eliminate any
;	variability caused by interrupts.
;
; Note: Interrupts must not be disabled for more than 54 ms at a
;	stretch during the timing interval. Because interrupts
;	are enabled, keys, mice, and other devices that generate
;	interrupts should not be used during the timing interval.
;
; Note: Any extra code running off the timer interrupt (such as
;	some memory-resident utilities) will increase the time
;	measured by the Zen timer.
;
; Note: These routines can introduce inaccuracies of up to a few
;	tenths of a second into the system clock count for each
;	code section timed. Consequently, it's a good idea to
;	reboot at the conclusion of timing sessions. (The
;	battery-backed clock, if any, is not affected by the Zen
;	timer.)
;
; All registers and all flags are preserved by all routines.
;

_TEXT	segment word public 'CODE'
	assume	cs:_TEXT, ds:nothing
	public	_ZTimerOn, _ZTimerOff, _ZTimerReport

;
; Set PS2 to 0 to assemble for use on a fully 8253-compatible
; system; when PS2 is 0, the readings are more reliable if the
; computer supports the undocumented timer-stopping feature,
; but may be badly off if that feature is not supported. In
; fact, timer-stopping may interfere with your computer's
; overall operation by putting the 8253 into an undefined or
; incorrect state.  Use with caution!!!
;
; Set PS2 to 1 to assemble for use on non-8253-compatible
; systems, including PS/2 computers; when PS2 is 1, readings
; may occasionally be off by 54 ms, but the code will work
; properly on all systems.
;
; A setting of 1 is safer and will work on more systems,
; while a setting of 0 produces more reliable results in systems
; which support the undocumented timer-stopping feature of the
; 8253. The choice is yours.
;
PS2	equ	1
;
; Base address of the 8253 timer chip.
;
BASE_8253		equ	40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253		equ	BASE_8253 + 0
;
; The address of the mode register in the 8253.
;
MODE_8253		equ	BASE_8253 + 3
;
; The address of the BIOS timer count variable in the BIOS
; data segment.
;
TIMER_COUNT		equ	46ch
;
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts remain disabled.
;
MPOPF macro 
	local	p1, p2
	jmp short p2
p1:	iret			;jump to pushed address & pop flags
p2:	push	cs		;construct far return address to
	call	p1		; the next instruction
	endm

;
; Macro to delay briefly to ensure that enough time has elapsed
; between successive I/O accesses so that the device being accessed
; can respond to both accesses even on a very fast PC.
;
DELAY	macro
	jmp	$+2
	jmp	$+2
	jmp	$+2
	endm

StartBIOSCountLow	dw	?	;BIOS count low word at the
					; start of the timing period
StartBIOSCountHigh	dw	?	;BIOS count high word at the
					; start of the timing period
EndBIOSCountLow		dw	?	;BIOS count low word at the
					; end of the timing period
EndBIOSCountHigh	dw	?	;BIOS count high word at the
					; end of the timing period
EndTimedCount		dw	?	;timer 0 count at the end of
					; the timing period
ReferenceCount		dw	?	;number of counts required to
					; execute timer overhead code
;
; String printed to report results.
;
OutputStr	label	byte
		db	0dh, 0ah, 'Timed count: '
TimedCountStr	db	10 dup (?)
		db	' microseconds', 0dh, 0ah
		db	'$'
;
; Temporary storage for timed count as it's divided down by powers
; of ten when converting from doubleword binary to ASCII.
;
CurrentCountLow		dw	?
CurrentCountHigh	dw	?
;
; Powers of ten table used to perform division by 10 when doing
; doubleword conversion from binary to ASCII.
;
PowersOfTen	label	word
	dd	1
	dd	10
	dd	100
	dd	1000
	dd	10000
	dd	100000
	dd	1000000
	dd	10000000
	dd	100000000
	dd	1000000000
PowersOfTenEnd	label	word
;
; String printed to report that the high word of the BIOS count
; changed while timing (an hour elapsed or midnight was crossed),
; and so the count is invalid and the test needs to be rerun.
;
TurnOverStr	label	byte
	db	0dh, 0ah
	db	'****************************************************'
	db	0dh, 0ah
	db	'* Either midnight passed or an hour or more passed *'
	db	0dh, 0ah
	db	'* while timing was in progress. If the former was  *'
	db	0dh, 0ah
	db	'* the case, please rerun the test; if the latter   *'
	db	0dh, 0ah
	db	'* was the case, the test code takes too long to    *'
	db	0dh, 0ah
	db	'* run to be timed by the long-period Zen timer.    *'
	db	0dh, 0ah
	db	'* Suggestions: use the DOS TIME command, the DOS   *'
	db	0dh, 0ah
	db	'* time function, or a watch.                       *'
	db	0dh, 0ah
	db	'****************************************************'
	db	0dh, 0ah
	db	'$'

;********************************************************************
;* Routine called to start timing.				    *
;********************************************************************

_ZTimerOn	proc	near

;
; Save the context of the program being timed.
;
	push	ax
	pushf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also stops
; timer 0 until the timer count is loaded, except on PS/2
; computers.
;
	mov	al,00110100b	;mode 2
	out	MODE_8253,al
;
; Set the timer count to 0, so we know we won't get another
; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed.
;
	DELAY
	sub	al,al
	out	TIMER_0_8253,al		;lsb
	DELAY
	out	TIMER_0_8253,al		;msb
;
; In case interrupts are disabled, enable interrupts briefly to allow
; the interrupt generated when switching from mode 3 to mode 2 to be
; recognized. Interrupts must be enabled for at least 210 ns to allow
; time for that interrupt to occur. Here, 10 jumps are used for the
; delay to ensure that the delay time will be more than long enough
; even on a very fast PC.
;
	pushf
	sti
	rept 10
	jmp	$+2
	endm
	MPOPF
;
; Store the timing start BIOS count.
; (Since the timer count was just set to 0, the BIOS count will
; stay the same for the next 54 ms, so we don't need to disable
; interrupts in order to avoid getting a half-changed count.)
;
	push	ds
	sub	ax,ax
	mov	ds,ax
	mov	ax,ds:[TIMER_COUNT+2]
	mov	cs:[StartBIOSCountHigh],ax
	mov	ax,ds:[TIMER_COUNT]
	mov	cs:[StartBIOSCountLow],ax
	pop	ds
;
; Set the timer count to 0 again to start the timing interval.
;
	mov	al,00110100b		;set up to load initial
	out	MODE_8253,al		; timer count
	DELAY
	sub	al,al
	out	TIMER_0_8253,al		;load count lsb
	DELAY
	out	TIMER_0_8253,al		;load count msb
;
; Restore the context of the program being timed and return to it.
;
	MPOPF
	pop	ax
	ret

_ZTimerOn	endp

;********************************************************************
;* Routine called to stop timing and get count.			    *
;********************************************************************

_ZTimerOff proc	near

;
; Save the context of the program being timed.
;
	pushf
	push	ax
	push	cx
;
; In case interrupts are disabled, enable interrupts briefly to allow
; any pending timer interrupt to be handled. Interrupts must be
; enabled for at least 210 ns to allow time for that interrupt to
; occur. Here, 10 jumps are used for the delay to ensure that the
; delay time will be more than long enough even on a very fast PC.
;
	sti
	rept	10
	jmp	$+2
	endm

;
; Latch the timer count.
;

if PS2

	mov	al,00000000b
	out	MODE_8253,al		;latch timer 0 count
;
; This is where a one-instruction-long window exists on the PS/2.
; The timer count and the BIOS count can lose synchronization;
; since the timer keeps counting after it's latched, it can turn
; over right after it's latched and cause the BIOS count to turn
; over before interrupts are disabled, leaving us with the timer
; count from before the timer turned over coupled with the BIOS
; count from after the timer turned over. The result is a count
; that's 54 ms too long.
;
				
else

;
; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count
; load, which stops timer 0 until the count is loaded. (Only works
; on fully 8253-compatible chips.)
;
	mov	al,00110100b		;mode 2
	out	MODE_8253,al
	DELAY
	mov	al,00000000b		;latch timer 0 count
	out	MODE_8253,al

endif

	cli				;stop the BIOS count
;
; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won't change.)
;
	push	ds
	sub	ax,ax
	mov	ds,ax
	mov	ax,ds:[TIMER_COUNT+2]
	mov	cs:[EndBIOSCountHigh],ax
	mov	ax,ds:[TIMER_COUNT]
	mov	cs:[EndBIOSCountLow],ax
	pop	ds
;
; Read the timer count and save it.
;
	in	al,TIMER_0_8253		;lsb
	DELAY
	mov	ah,al
	in	al,TIMER_0_8253		;msb
	xchg	ah,al
	neg	ax			;convert from countdown
					; remaining to elapsed
					; count
	mov	cs:[EndTimedCount],ax
;
; Restart timer 0, which is still waiting for an initial count
; to be loaded.
;

ife PS2

	DELAY
	mov	al,00110100b		;mode 2, waiting to load a
					; 2-byte count
	out	MODE_8253,al
	DELAY
	sub	al,al
	out	TIMER_0_8253,al		;lsb
	DELAY
	mov	al,ah
	out	TIMER_0_8253,al		;msb
	DELAY

endif

	sti		;let the BIOS count continue
;
; Time a zero-length code fragment, to get a reference for how
; much overhead this routine has. Time it 16 times and average it,
; for accuracy, rounding the result.
;
	mov	cs:[ReferenceCount],0
	mov	cx,16
	cli				;interrupts off to allow a
					; precise reference count
RefLoop:
	call	ReferenceZTimerOn
	call	ReferenceZTimerOff
	loop	RefLoop
	sti
	add	cs:[ReferenceCount],8	;total + (0.5 * 16)
	mov	cl,4
	shr	cs:[ReferenceCount],cl	;(total) / 16 + 0.5
;
; Restore the context of the program being timed and return to it.
;
	pop	cx
	pop	ax
	MPOPF
	ret

_ZTimerOff endp

;
; Called by ZTimerOff to start the timer for overhead measurements.
;

ReferenceZTimerOn	proc	near
;
; Save the context of the program being timed.
;
	push	ax
	pushf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
;
	mov	al,00110100b	;mode 2
	out	MODE_8253,al
;
; Set the timer count to 0.
;
	DELAY
	sub	al,al
	out	TIMER_0_8253,al		;lsb
	DELAY
	out	TIMER_0_8253,al		;msb
;
; Restore the context of the program being timed and return to it.
;
	MPOPF
	pop	ax
	ret

ReferenceZTimerOn	endp

;
; Called by ZTimerOff to stop the timer and add the result to
; ReferenceCount for overhead measurements. Doesn't need to look
; at the BIOS count because timing a zero-length code fragment
; isn't going to take anywhere near 54 ms.
;

ReferenceZTimerOff proc	near
;
; Save the context of the program being timed.
;
	pushf
	push	ax
	push	cx

;
; Match the interrupt-window delay in ZTimerOff.
;
	sti
	rept	10
	jmp	$+2
	endm

	mov	al,00000000b
	out	MODE_8253,al		;latch timer 
;
; Read the count and save it.
;
	DELAY
	in	al,TIMER_0_8253		;lsb
	DELAY
	mov	ah,al
	in	al,TIMER_0_8253		;msb
	xchg	ah,al
	neg	ax			;convert from countdown
					; remaining to elapsed
					; count
	add	cs:[ReferenceCount],ax
;
; Restore the context and return.
;
	pop	cx
	pop	ax
	MPOPF
	ret

ReferenceZTimerOff endp

;********************************************************************
;* Routine called to report timing results.			    *
;********************************************************************

_ZTimerReport	proc	near

	pushf
	push	ax
	push	bx
	push	cx
	push	dx
	push	si
	push	di
	push	ds
;
	push	cs	;DOS functions require that DS point
	pop	ds	; to text to be displayed on the screen
	assume	ds:_TEXT
;
; See if midnight or more than an hour passed during timing. If so,
; notify the user.
;
	mov	ax,[StartBIOSCountHigh]
	cmp	ax,[EndBIOSCountHigh]
	jz	CalcBIOSTime		;hour count didn't change,
					; so everything's fine
	inc	ax
	cmp	ax,[EndBIOSCountHigh]
	jnz	TestTooLong		;midnight or two hour
					; boundaries passed, so the
					; results are no good
	mov	ax,[EndBIOSCountLow]
	cmp	ax,[StartBIOSCountLow]
	jb	CalcBIOSTime		;a single hour boundary
					; passed-that's OK, so long as
					; the total time wasn't more
					; than an hour

;
; Over an hour elapsed or midnight passed during timing, which
; renders the results invalid. Notify the user. This misses the
; case where a multiple of 24 hours has passed, but we'll rely
; on the perspicacity of the user to detect that case.
;
TestTooLong:
	mov	ah,9
	mov	dx,offset TurnOverStr
	int	21h
	jmp	short ZTimerReportDone
;
; Convert the BIOS time to microseconds.
;
CalcBIOSTime:
	mov	ax,[EndBIOSCountLow]
	sub	ax,[StartBIOSCountLow]
	mov	dx,54925		;number of microseconds each
					; BIOS count represents
	mul	dx
	mov	bx,ax			;set aside BIOS count in
	mov	cx,dx			; microseconds
;
; Convert timer count to microseconds.
;
	mov	ax,[EndTimedCount]
	mov	si,8381
	mul	si
	mov	si,10000
	div	si		;* .8381 = * 8381 / 10000
;
; Add timer and BIOS counts together to get an overall time in
; microseconds.
;
	add	bx,ax
	adc	cx,0
;
; Subtract the timer overhead and save the result.
;
	mov	ax,[ReferenceCount]
	mov	si,8381		;convert the reference count
	mul	si		; to microseconds
	mov	si,10000
	div	si		;* .8381 = * 8381 / 10000
	sub	bx,ax
	sbb	cx,0
	mov	[CurrentCountLow],bx
	mov	[CurrentCountHigh],cx
;
; Convert the result to an ASCII string by trial subtractions of
; powers of 10.
;
	mov	di,offset PowersOfTenEnd - offset PowersOfTen - 4
	mov	si,offset TimedCountStr
CTSNextDigit:
	mov	bl,'0'
CTSLoop:
	mov	ax,[CurrentCountLow]
	mov	dx,[CurrentCountHigh]
	sub	ax,PowersOfTen[di]
	sbb	dx,PowersOfTen[di+2]
	jc	CTSNextPowerDown
	inc	bl
	mov	[CurrentCountLow],ax
	mov	[CurrentCountHigh],dx
	jmp	CTSLoop
CTSNextPowerDown:
	mov	[si],bl
	inc	si
	sub	di,4
	jns	CTSNextDigit
;
;
; Print the results.
;
	mov	ah,9
	mov	dx,offset OutputStr
	int	21h
;
ZTimerReportDone:
	pop	ds
	pop	di
	pop	si
	pop	dx
	pop	cx
	pop	bx
	pop	ax
	MPOPF
	ret

_ZTimerReport	endp

_TEXT	ends
	end
