Search code examples
assemblytimex86x86-16dos

The DOS.GetSystemTime function 2Ch is not accurate


The DOS.GetSystemTime function 2Ch returns the current time as hours (CH), minutes (CL), seconds (DH), and hundredths of a second (DL). As it turns out these 'hundredths of a second' are more like 'twentieths of a second'.

I have included a program that interrogates this DOS function continuously, displaying only the unique timestamps. The results are disappointing. How can I obtain true 0.01 sec readings?

  ORG  256

Begin:
  mov  bh, -1
Main:
  mov  ah, 01h          ; BIOS.CheckKeystroke
  int  16h              ; -> AX ZF
  jz   Work
  mov  ah, 00h          ; BIOS.GetKeystroke
  int  16h              ; -> AX
Pause:
  mov  ah, 00h          ; BIOS.GetKeystroke
  int  16h              ; -> AX
  cmp  al, 27           ; ESC
  jne  Work
  ret                   ; TerminateProgram
Work:
  call DOSTime          ; -> CX DX
  cmp  bh, dl
  je   Main             ; Hundredths didn't change
  mov  bh, dl

  push dx               ; (1)
  mov  bl, ':'
  mov  al, ch           ; Hours
  call PrintTrio        ; -> (AX DX)
  mov  al, cl           ; Minutes
  call PrintTrio        ; -> (AX DX)
  pop  cx               ; (1)
  mov  bl, '.'
  mov  al, ch           ; Seconds
  call PrintTrio        ; -> (AX DX)
  mov  bl, 13
  mov  al, cl           ; Hundredths
  call PrintTrio        ; -> (AX DX)

  mov  dl, 10
  mov  ah, 02h          ; DOS.PrintCharacter
  int  21h
  jmp  Main
; ----------------------
; IN (al,bl) OUT () MOD (ax,dx)
PrintTrio:
  aam
  add  ax, '00'
  push ax               ; (1)
  mov  dl, ah
  mov  ah, 02h          ; DOS.PrintCharacter
  int  21h
  pop  dx               ; (1)
  mov  ah, 02h          ; DOS.PrintCharacter
  int  21h
  mov  dl, bl
  mov  ah, 02h          ; DOS.PrintCharacter
  int  21h
  ret
; ----------------------
; IN () OUT (cx,dx)
DOSTime:
  push ax
  mov  ah, 2Ch          ; DOS.GetSystemTime
  int  21h              ; -> CX DX
  pop  ax
  ret
; ----------------------

A typical output from the above program would be:

17:15:25.84
17:15:25.89
17:15:25.95
17:15:26.00
17:15:26.06
17:15:26.11
17:15:26.17
17:15:26.22
17:15:26.28
17:15:26.33
17:15:26.39
17:15:26.44
17:15:26.50
17:15:26.55
17:15:26.60
17:15:26.66
17:15:26.71
17:15:26.77
17:15:26.82
17:15:26.88
17:15:26.93
17:15:26.99
17:15:27.04
17:15:27.10

This is a question from another programmer that wanted to use the DOS function 2Ch for delaying and then found this inaccurary to pose problems: Delay program using int 21h with ah = 2Ch.


Solution

  • When DOS gets a request for the system time, it will read the timer tick maintained by BIOS and quickly convert its value into the corresponding hours, minutes, and seconds. There is a remainder to this calculation and DOS returns it as the 'hundredths of seconds' value. This can never be as accurate as 0.01 sec given that the timer tick only changes every 0.054925 seconds.

    The 32-bit timer that BIOS maintains at 0040:006C increments each time channel 0 of the Programmable Interval Timer (PIT) generates an interrupt 8 (IRQ0). This interrupt is the result of an internal counter that just decremented 65536 times. One such decrement takes 0.838095 µsec because the PIT operates at 14.31818 MHz / 12 = 1.193182 MHz.
    We can read this internal counter and use it as the least significant 16 bits of a new 'super counter', if we combine it with the 32-bit counter we find at 0040:006C.
    Because our modest goal is to obtain just 0.01 sec precision, the calculation only uses the high 8 bits of the internal counter and the low 24 bits of the BIOS timer variable.

    The code assumes that PIT channel 0 is operating in mode 2 (rate generator) or mode 3 (square wave generator) with a frequency divisor of 65536. Because the code uses the Read-Back command, it is also assumed that the PIT is not an 8253 (obsolete), but at least an 8254.

    The number of ticks in one day is taken to be 1573041, which is 1 more than you might expect. See Why did some BIOSes have the timer tick wrap-around at 1800B1h instead of at 1800B0h?.

    BIOS Year TotalTicks PIT #0 Mode
    Award Modular BIOS v4.51 PG 1996 1573040 8254 3
    Phoenix BIOS 4.00 Release 6.00 1999 1573041 8254 3
    Compaq System ROM 686P9 v1.11 2002 1573040 8254 3
    Phoenix Technologies LTD v1.23 2007 1573041 8254 2
    AMI BIOS 2010 1573040 8254 2
    DOSBox 0.74 2010 (*) 8254 3

    (*) Differing from normal BIOSes, DOSBox does not make the BIOS timer wraparound at midnight. The MyTime procedure makes provisions for this.

    1800B0FFh .. 8639999
    
            1 .. 8639999 / 1800B0FFh
    
            N .. N * (8639999 / 1800B0FFh)
    

    Avoiding the cumbersome division.
    From (8639999 / 1800B0FFh) = (x / 100000000h) we get:

    x = 8639999 / 1800B0FFh * 100000000h
    x = 92149630
    

    The new 'super counter' whose max value is 1800B0FFh, gets converted into hundredths of a seconds by first multiplying it with 92149630 (057E177Eh), and then dividing by 4294967296 (100000000h) which is a simple matter of discarding the low half of the 64-bit product.

    This is 8086 assembly code to be assembled with FASM:

    ; IN () OUT (cx,dx)
    MyTime:                 ; Assumes 8254 PIT
      push ax bx si di bp
      push ds               ; (1)
      pushf                 ; (2)
      xor  ax, ax
      mov  ds, ax
      mov  si, 046Ch        ; Address of the BIOS.TimerTick
      cli
      mov  cx, [si+1]       ; CX:BH = [0,1573040]
      mov  bh, [si]
      sti
      nop
      nop
    .ReDo:
      cli
      mov  al, 11_00_001_0b ; Read-back latched count word and status byte
      out  43h, al          ;  for PIT channel 0
      jmp  $+2
      in   al, 40h          ; Get status byte in AH
      mov  ah, al
      in   al, 40h          ; Get count word in DX
      mov  dl, al
      in   al, 40h
      sti
      mov  dh, al
      test dx, dx           ; Don't accept 0
      jz   .ReDo
      not  dx               ; Make upcounting
      mov  al, ah
      and  al, 00111111b
      cmp  al, 00110100b    ; Is it lowbyte/highbyte mode 2 for PIT channel 0 ?
      je   .Mode2
    .Mode3:
      inc  dx               ; (-Count + ~OutputPin * 65536) / 2
      shl  ah, 1
      cmc
      rcr  dx, 1
    .Mode2:
      cli
      cmp  bh, [si]
      je   .GotIt
      test dx, dx
      js   .GotIt
      mov  cx, [si+1]       ; CX:BH = [0,1573040]
      mov  bh, [si]
    .GotIt:
      popf                  ; (2)
      pop  ds               ; (1)
      mov  bl, dh           ; -> CX:BX
    
      mov  ax, 057Eh        ; 92149630 = 057E177Eh
      mul  cx
      mov  si, ax
      mov  di, dx
    
      mov  ax, 057Eh
      mul  bx
      mov  bp, ax
      add  si, dx
      adc  di, 0
    
      mov  ax, 177Eh
      mul  cx
      add  bp, ax
      adc  si, dx
      adc  di, 0
    
      mov  ax, 177Eh
      mul  bx
      add  bp, dx
      adc  si, 0
      adc  di, 0
    
      mov  ax, si
      mov  dx, di
      mov  bx, 6000
      div  bx               ; -> AX is BigMinutes, DX is BigHundredths
    
      mov  bl, 60
      div  bl               ; -> AL is Hours, AH is Minutes
    .DOSBox:                ; DOSBox does not reset the BIOS.TimerTick
      sub  al, 24
      jnb  .DOSBox
      add  al, 24
      mov  ch, al           ; Hours [0,23]
      mov  cl, ah           ; Minutes [0,59]
    
      mov  ax, dx
      mov  bl, 100
      div  bl               ; -> AL is Seconds, AH is Hundredths
      mov  dh, al           ; Seconds [0,59]
      mov  dl, ah           ; Hundredths [0,99]
    
      pop  bp di si bx ax
      ret
    ; ----------------------