;
; *** Listing 2-5 ***
;
; The long-period Zen timer. (LZTIMER.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 4/26/89
;
; 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.
;
                DOSSEG
                .model  small
                .code
        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

        end
