;
; *** Listing 3-1 ***
;
; The precision Zen timer (PZTIMER.ASM)
;
; Uses the 8253 timer to time the performance of code that takes
; less than about 54 milliseconds to execute, with a resolution
; of better than 10 microseconds.
;
; By Michael Abrash 
;
; Externally callable routines:
;
;  ZTimerOn: Starts the Zen timer, with interrupts disabled.
;
;  ZTimerOff: Stops the Zen timer, saves the timer count,
;	times the overhead code, and restores interrupts to the
;	state they were in when ZTimerOn was called.
;
;  ZTimerReport: Prints the net time that passed between starting
;	and stopping the timer.
;
; Note: If longer than about 54 ms passes between ZTimerOn and
;	ZTimerOff calls, the timer turns over and the count is
;	inaccurate. When this happens, an error message is displayed
;	instead of a count. The long-period Zen timer should be used
;	in such cases.
;
; Note: Interrupts *MUST* be left off between calls to ZTimerOn
;	and ZTimerOff for accurate timing and for detection of
;	timer overflow.
;
; Note: These routines can introduce slight inaccuracies into the
;	system clock count for each code section timed even if
;	timer 0 doesn't overflow. If timer 0 does overflow, the
;	system clock can become slow by virtually any amount of
;	time, since the system clock can't advance while the
;	precison timer is timing. Consequently, it's a good idea
;	to reboot at the end of each timing session. (The
;	battery-backed clock, if any, is not affected by the Zen
;	timer.)
;
; All registers, and all flags except the interrupt flag, are
; preserved by all routines. Interrupts are enabled and then disabled
; by ZTimerOn, and are restored by ZTimerOff to the state they were
; in when ZTimerOn was called.
;

Code	segment word public 'CODE'
	assume	cs:Code, ds:nothing
	public	ZTimerOn, ZTimerOff, ZTimerReport

;
; 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 Operation Command Word 3 in the 8259 Programmable
; Interrupt Controller (PIC) (write only, and writable only when
; bit 4 of the byte written to this address is 0 and bit 3 is 1).
;
OCW3			equ	20h
;
; The address of the Interrupt Request register in the 8259 PIC
; (read only, and readable only when bit 1 of OCW3 = 1 and bit 0
; of OCW3 = 0).
;
IRR			equ	20h
;
; 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

OriginalFlags		db	?	;storage for upper byte of
					; FLAGS register when
					; ZTimerOn called
TimedCount		dw	?	;timer 0 count when the timer
					; is stopped
ReferenceCount		dw	?	;number of counts required to
					; execute timer overhead code
OverflowFlag		db	?	;used to indicate whether the
					; timer overflowed during the
					; timing interval
;
; String printed to report results.
;
OutputStr	label	byte
		db	0dh, 0ah, 'Timed count: ', 5 dup (?)
ASCIICountEnd	label	byte
		db	' microseconds', 0dh, 0ah
		db	'$'
;
; String printed to report timer overflow.
;
OverflowStr	label	byte
	db	0dh, 0ah
	db	'****************************************************'
	db	0dh, 0ah
	db	'* The timer overflowed, so the interval timed was  *'
	db	0dh, 0ah
	db	'* too long for the precision timer to measure.     *'
	db	0dh, 0ah
	db	'* Please perform the timing test again with the    *'
	db	0dh, 0ah
	db	'* long-period timer.                               *'
	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
	pop	ax			;get flags so we can keep
					; interrupts off when leaving
					; this routine
	mov	cs:[OriginalFlags],ah	;remember the state of the
					; Interrupt flag
	and	ah,0fdh 		;set pushed interrupt flag
					; to 0
	push	ax
;
; Turn on interrupts, so the timer interrupt can occur if it's
; pending.
;
	sti
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also
; leaves the 8253 waiting for the initial timer 0 count to
; be loaded.
;
	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
;
; Wait before clearing interrupts to allow the interrupt generated
; when switching from mode 3 to mode 2 to be recognized. The delay
; must be at least 210 ns long 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.
;
	rept 10
	jmp	$+2
	endm
;
; Disable interrupts to get an accurate count.
;
	cli
;
; 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 and return.
;
	MPOPF				;keeps interrupts off
	pop	ax
	ret

ZTimerOn	endp

;********************************************************************
;* Routine called to stop timing and get count.			    *
;********************************************************************

ZTimerOff proc	near

;
; Save the context of the program being timed.
;
	push	ax
	push	cx
	pushf
;
; Latch the count.
;
	mov	al,00000000b		;latch timer 0
	out	MODE_8253,al
;
; See if the timer has overflowed by checking the 8259 for a pending
; timer interrupt.
;
	mov	al,00001010b		;OCW3, set up to read
	out	OCW3,al			; Interrupt Request register
	DELAY
	in	al,IRR			;read Interrupt Request
					; register
	and	al,1			;set AL to 1 if IRQ0 (the
					; timer interrupt) is pending
	mov	cs:[OverflowFlag],al	;store the timer overflow
					; status
;
; Allow interrupts to happen again.
;
	sti
;
; 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	cs:[TimedCount],ax
; 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 original interrupt state.
;
	pop	ax			;retrieve flags when called
	mov	ch,cs:[OriginalFlags]	;get back the original upper
					; byte of the FLAGS register
	and	ch,not 0fdh		;only care about original
					; interrupt flag...
	and	ah,0fdh			;...keep all other flags in
					; their current condition
	or	ah,ch			;make flags word with original
					; interrupt flag
	push	ax			;prepare flags to be popped
;
; Restore the context of the program being timed and return to it.
;
	MPOPF				;restore the flags with the
					; original interrupt state
	pop	cx
	pop	ax
	ret

ZTimerOff endp

;
; Called by ZTimerOff to start timer for overhead measurements.
;

ReferenceZTimerOn	proc	near
;
; Save the context of the program being timed.
;
	push	ax
	pushf		;interrupts are already off
;
; 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	;set up to load
	out	MODE_8253,al	; initial timer count
	DELAY
;
; Set the timer count to 0.
;
	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

ReferenceZTimerOn	endp

;
; Called by ZTimerOff to stop timer and add result to ReferenceCount
; for overhead measurements.
;

ReferenceZTimerOff proc	near
;
; Save the context of the program being timed.
;
	push	ax
	push	cx
	pushf
;
; Latch the count and read it.
;
	mov	al,00000000b		;latch timer 0
	out	MODE_8253,al
	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 amount
					; counted down
	add	cs:[ReferenceCount],ax
;
; Restore the context of the program being timed and return to it.
;
	MPOPF
	pop	cx
	pop	ax
	ret

ReferenceZTimerOff endp

;********************************************************************
;* Routine called to report timing results.			    *
;********************************************************************

ZTimerReport	proc	near

	pushf
	push	ax
	push	bx
	push	cx
	push	dx
	push	si
	push	ds
;
	push	cs	;DOS functions require that DS point
	pop	ds	; to text to be displayed on the screen
	assume	ds:Code
;
; Check for timer 0 overflow.
;
	cmp	[OverflowFlag],0
	jz	PrintGoodCount
	mov	dx,offset OverflowStr
	mov	ah,9
	int	21h
	jmp	short EndZTimerReport
;
; Convert net count to decimal ASCII in microseconds.
;
PrintGoodCount:
	mov	ax,[TimedCount]
	sub	ax,[ReferenceCount]
	mov	si,offset ASCIICountEnd - 1
;
; Convert count to microseconds by multiplying by .8381.
;
	mov	dx,8381
	mul	dx
	mov	bx,10000
	div	bx		;* .8381 = * 8381 / 10000
;
; Convert time in microseconds to 5 decimal ASCII digits.
;
	mov	bx,10
	mov	cx,5
CTSLoop:
	sub	dx,dx
	div	bx
	add	dl,'0'
	mov	[si],dl
	dec	si
	loop	CTSLoop
;
; Print the results.
;
	mov	ah,9
	mov	dx,offset OutputStr
	int	21h
;
EndZTimerReport:
	pop	ds
	pop	si
	pop	dx
	pop	cx
	pop	bx
	pop	ax
	MPOPF
	ret

ZTimerReport	endp

Code	ends
	end
