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