Previous Table of Contents Next


While permitting the timer interrupt to occur allows long intervals to be timed, that same interrupt makes the long-period Zen timer less accurate than the precision Zen timer, since the time the BIOS spends handling timer interrupts during the timing interval is included in the time measured by the long-period timer. Likewise, any other interrupts that occur during the timing interval, most notably keyboard and mouse interrupts, will increase the measured time.

The long-period Zen timer has some of the same effects on the system time as does the precision Zen timer, so it’s a good idea to reboot the system after a session with the long-period Zen timer. The long-period Zen timer does not, however, have the same potential for introducing major inaccuracy into the system clock time during a single timing run since it leaves interrupts enabled and therefore allows the system clock to update normally.

Stopping the Clock

There’s a potential problem with the long-period Zen timer. The problem is this: In order to measure times longer than 54 ms, we must maintain not one but two timing components, the timer 0 count and the BIOS time-of-day count. The time-of-day count measures the passage of 54.9 ms intervals, while the timer 0 count measures time within those 54.9 ms intervals. We need to read the two time components simultaneously in order to get a clean reading. Otherwise, we may read the timer count just before it turns over and generates an interrupt, then read the BIOS time-of-day count just after the interrupt has occurred and caused the time-of-day count to turn over, with a resulting 54 ms measurement inaccuracy. (The opposite sequence—reading the time-of-day count and then the timer count—can result in a 54 ms inaccuracy in the other direction.)

The only way to avoid this problem is to stop timer 0, read both the timer and time-of-day counts while the timer is stopped, and then restart the timer. Alas, the gate input to timer 0 isn’t program-controllable in the PC, so there’s no documented way to stop the timer. (The latched read feature we used in Listing 3.1 doesn’t stop the timer; it latches a count, but the timer keeps running.) What should we do?

As it turns out, an undocumented feature of the 8253 makes it possible to stop the timer dead in its tracks. Setting the timer to a new mode and waiting for an initial count to be loaded causes the timer to stop until the count is loaded. Surprisingly, the timer count remains readable and correct while the timer is waiting for the initial load.

In my experience, this approach works beautifully with fully 8253-compatible chips. However, there’s no guarantee that it will always work, since it programs the 8253 in an undocumented way. What’s more, IBM chose not to implement compatibility with this particular 8253 feature in the custom chips used in PS/2 computers. On PS/2 computers, we have no choice but to latch the timer 0 count and then stop the BIOS count (by disabling interrupts) as quickly as possible. We’ll just have to accept the fact that on PS/2 computers we may occasionally get a reading that’s off by 54 ms, and leave it at that.

I’ve set up Listing 3.5 so that it can assemble to either use or not use the undocumented timer-stopping feature, as you please. The PS2 equate selects between the two modes of operation. If PS2 is 1 (as it is in Listing 3.5), then the latch-and-read method is used; if PS2 is 0, then the undocumented timer-stop approach is used. The latch-and-read method will work on all PC-compatible computers, but may occasionally produce results that are incorrect by 54 ms. The timer-stop approach avoids synchronization problems, but doesn’t work on all computers.

LISTING 3.5 LZTIMER.ASM

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

Code segment word public ‘CODE’
     assume cs:  Code, 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                  equ1
;
; Base address of the 8253 timer chip.
;
BASE_8253            equ40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253         equBASE_8253 + 0
;
; The address of the mode register in the 8253.
;
MODE_8253            equBASE_8253 + 3
;
; The address of the BIOS timer count variable in the BIOS
; data segment.
;
TIMER_COUNT           equ46ch
;
; 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:   pushcs      ;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 labelbyte
          db     0dh, 0ah, ‘Timed count: ‘
TimedCountStr    db10 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.
;
PowersOfTenlabelword
     dd   1
     dd   10
     dd   100
     dd   1000
     dd   10000
     dd   100000
     dd   1000000
     dd   10000000
     dd   100000000
     dd   1000000000
PowersOfTenEnd   labelword
;
; 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.
;
TurnOverStrlabelbyte
     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
     pus  hf
;
; 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
     subal,al
     outTIMER_0_8253,al       ;lsb
     DELAY
     outTIMER_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
     subax, ax
     movds, ax
     movax, ds:[TIMER_COUNT+2]
     movcs: [StartBIOSCountHigh],ax
     movax, ds:[TIMER_COUNT]
     movcs: [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
     subal, 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
     popax
     ret

ZTimerOnendp

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

ZTimerOff procnear

;
; Save the context of the program being timed.
;
     pushf
     pushax
     pushcx
;
; 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.
;
     popcx
     popax
     MPOPF
     ret

ZTimerOff endp

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

ReferenceZTimerOnprocnear
;
; Save the context of the program being timed.
;
     pushax
     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
     popax
     ret

ReferenceZTimerOnendp

;
; 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 procnear
;
; Save the context of the program being timed.
;
     pushf
     pushax
     pushcx

;
; Match the interrupt-window delay in ZTimerOff.
;
     sti
     rept10
     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.
;
     popcx
     popax
     MPOPF
     ret

ReferenceZTimerOff endp

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

ZTimerReportprocnear

     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     :Code
;
; 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

Code   ends
       end


Previous Table of Contents Next

Graphics Programming Black Book © 2001 Michael Abrash