;****************************************************************************
;*
;*						   Long Period Zen Timer
;*
;*							   From the book
;*						 "Zen of Assembly Language"
;*							Volume 1, Knowledge
;*
;*							 by Michael Abrash
;*
;*					  Modifications by Kendall Bennett
;*                  Copyright (C) 1996 SciTech Software
;*
;* Filename:	$Workfile:   lztimer.asm  $
;* Version:		$Revision:   1.0  $
;*
;* Language:	80386 Assembler
;* Environment:	IBM PC (MS DOS)
;*
;* Description:	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.
;*
;*				The routines in this package only works with interrupts
;*				enabled, and in fact will explicitly turn interrupts on
;*				in order to ensure we get accurate results from the timer.
;*
;*	Externally 'C' callable routines:
;*
;*	LZTimerOn:		Saves the BIOS time of day count and starts the
;*					long period Zen Timer.
;*
;*	LZTimerLap:		Latches the current count, and keeps the timer running
;*
;*	LZTimerOff:		Stops the long-period Zen Timer and saves the timer
;*					count and the BIOS time of day count.
;*
;*	LZTimerCount:	Returns an unsigned long representing the timed count
;*					in microseconds. If more than an hour passed during
;*					the timing interval, LZTimerCount will return the
;*					value 0xFFFFFFFF (an invalid count).
;*
;*	Note:	If either more than an hour passes between calls to LZTimerOn
;*			and LZTimerOff, an error is reported. For timing code that takes
;*			more than a few minutes to execute, use the low resolution
;*			Ultra Long Period Zen Timer code, which should be accurate
;*			enough for most purposes.
;*
;*	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, key, 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 being 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, except
;*	interrupts which are always turned on
;*
;* $Date:   05 Feb 1996 14:35:54  $ $Author:   KendallB  $
;*
;****************************************************************************

		IDEAL

INCLUDE "model.mac"				; Memory model macros

header	lztimer					; Set up memory model

ifndef  __WINDOWS16__
ifndef  __WINDOWS32__

;****************************************************************************
;
; Equates used by long period Zen Timer
;
;****************************************************************************

; Base address of 8253 timer chip

BASE_8253		=		40h

; The address of the timer 0 count registers in the 8253

TIMER_0_8253	=		BASE_8253 + 0

; The address of the mode register in the 8253

MODE_8253		=		BASE_8253 + 3

; The address of the BIOS timer count variable in the BIOS data area.

TIMER_COUNT		=		6Ch

; 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.

macro	DELAY
		jmp		$+2
		jmp		$+2
		jmp		$+2
endm

begdataseg	lztimer

		$EXTRN  __ZTimerBIOS,USHORT

StartBIOSCount      dd  ?       ; Starting BIOS count dword
EndBIOSCount		dd	?		; Ending BIOS count dword
EndTimedCount		dw	?		; Timer 0 count at the end of timing period

enddataseg	lztimer

begcodeseg	lztimer				; Start of code segment

;----------------------------------------------------------------------------
; void LZTimerOn(void);
;----------------------------------------------------------------------------
; Starts the Long period Zen timer counting.
;----------------------------------------------------------------------------
procstart		_LZTimerOn

; Set the 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

; Store the timing start BIOS count

		push	es
		mov		ax,[__ZTimerBIOS]
		mov		es,ax
		cli							; No interrupts while we grab the count
		mov		eax,[es:TIMER_COUNT]
		sti
		mov		[StartBIOSCount],eax
		pop		es

; 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

		ret

procend		_LZTimerOn

;----------------------------------------------------------------------------
; void LZTimerOff(void);
;----------------------------------------------------------------------------
; Stops the long period Zen timer and saves count.
;----------------------------------------------------------------------------
procstart		_LZTimerOff

; Latch the timer count.

		mov		al,00000000b		; latch timer 0
		out		MODE_8253,al
		cli							; Stop the BIOS count

; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won't change).

		push	es
		mov     ax,[__ZTimerBIOS]
		mov		es,ax
		mov		eax,[es:TIMER_COUNT]
		mov		[EndBIOSCount],eax
		pop		es

; Read out the count we latched earlier.

		in		al,TIMER_0_8253		; least significant byte
		DELAY
		mov		ah,al
		in		al,TIMER_0_8253		; most significant byte
		xchg	ah,al
		neg		ax					; Convert from countdown remaining
									;  to elapsed count
		mov		[EndTimedCount],ax
		sti							; Let the BIOS count continue

		ret

procend		_LZTimerOff

;----------------------------------------------------------------------------
; unsigned long LZTimerLap(void)
;----------------------------------------------------------------------------
; Latches the current count and converts it to a microsecond timing value,
; but leaves the timer still running. We dont check for and overflow,
; where the time has gone over an hour in this routine, since we want it
; to execute as fast as possible.
;----------------------------------------------------------------------------
procstart		_LZTimerLap

		use_ebx						; Save EBX for 32 bit code

; Latch the timer count.

        mov     al,00000000b        ; latch timer 0
		out		MODE_8253,al
        cli                         ; Stop the BIOS count

; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won't change).

		push	es
		mov		ax,[__ZTimerBIOS]
		mov		es,ax
		mov		eax,[es:TIMER_COUNT]
		mov		[EndBIOSCount],eax
		pop		es

; Read out the count we latched earlier.

		in		al,TIMER_0_8253		; least significant byte
		DELAY
		mov		ah,al
		in		al,TIMER_0_8253		; most significant byte
		xchg	ah,al
		neg		ax					; Convert from countdown remaining
									;  to elapsed count
		mov		[EndTimedCount],ax
		sti							; Let the BIOS count continue

; See if a midnight boundary has passed and adjust the finishing BIOS
; count by the number of ticks in 24 hours. We wont be able to detect
; more than 24 hours, but at least we can time across a midnight
; boundary

		mov		eax,[EndBIOSCount]		; Is end < start?
		cmp		eax,[StartBIOSCount]
		jae		@@CalcBIOSTime			; No, calculate the time taken

; Adjust the finishing time by adding the number of ticks in 24 hours
; (1573040).

		add		[EndBIOSCount],1800B0h

; Convert the BIOS time to microseconds

@@CalcBIOSTime:
		mov		ax,[WORD EndBIOSCount]
		sub		ax,[WORD StartBIOSCount]
		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

		push	_si
		mov		ax,[EndTimedCount]
		mov		si,8381
		mul		si
		mov		si,10000
		div		si					; * 0.8381 = * 8381 / 10000
		pop		_si

; Add the timer and BIOS counts together to get an overall time in
; microseconds.

		add		ax,bx
		adc		cx,0
if flatmodel
		shl		ecx,16
		mov		cx,ax
		mov		eax,ecx				; EAX := timer count
else
		mov		dx,cx
endif
		unuse_ebx					; Restore EBX for 32 bit code
		ret

procend		_LZTimerLap

;----------------------------------------------------------------------------
; unsigned long LZTimerCount(void);
;----------------------------------------------------------------------------
; Returns an unsigned long representing the net time in microseconds.
;
; If an hour has passed while timing, we return 0xFFFFFFFF as the count
; (which is not a possible count in itself).
;----------------------------------------------------------------------------
procstart		_LZTimerCount

		use_ebx						; Save EBX for 32 bit code

; See if a midnight boundary has passed and adjust the finishing BIOS
; count by the number of ticks in 24 hours. We wont be able to detect
; more than 24 hours, but at least we can time across a midnight
; boundary

		mov		eax,[EndBIOSCount]		; Is end < start?
		cmp		eax,[StartBIOSCount]
		jae		@@CheckForHour			; No, check for hour passing

; Adjust the finishing time by adding the number of ticks in 24 hours
; (1573040).

		add		[EndBIOSCount],1800B0h

; See if more than an hour passed during timing. If so, notify the user.

@@CheckForHour:
		mov		ax,[WORD StartBIOSCount+2]
		cmp		ax,[WORD EndBIOSCount+2]
		jz		@@CalcBIOSTime		; Hour count didn't change, so
									;  everything is fine

		inc		ax
		cmp		ax,[WORD EndBIOSCount+2]
		jnz		@@TestTooLong		; Two hour boundaries passed, so the
									;  results are no good
		mov 	ax,[WORD EndBIOSCount]
		cmp		ax,[WORD StartBIOSCount]
		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 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:
if	flatmodel
		mov		eax,0FFFFFFFFh
else
		mov		ax,0FFFFh
		mov		dx,0FFFFh
endif
		jmp		short @@Done

; Convert the BIOS time to microseconds

@@CalcBIOSTime:
		mov		ax,[WORD EndBIOSCount]
		sub		ax,[WORD StartBIOSCount]
		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

		push	_si
		mov		ax,[EndTimedCount]
		mov		si,8381
		mul		si
		mov		si,10000
		div		si					; * 0.8381 = * 8381 / 10000
		pop		_si

; Add the timer and BIOS counts together to get an overall time in
; microseconds.

		add		ax,bx
		adc		cx,0
if flatmodel
		shl		ecx,16
		mov		cx,ax
		mov		eax,ecx				; EAX := timer count
else
		mov		dx,cx
endif

@@Done:
		unuse_ebx					; Restore EBX for 32 bit code
		ret

procend		_LZTimerCount

procstart   _LZ_disable
		cli
		ret
procend     _LZ_disable

procstart   _LZ_enable
		sti
		ret
procend     _LZ_enable

endcodeseg	lztimer

endif
endif

		END							; End of module
