RTC-based millisecond timer

DOS specific questions.
Post Reply
MichaelW
Posts: 3500
Joined: May 16, 2006 22:34
Location: USA

RTC-based millisecond timer

Post by MichaelW »

dostimer.bas:

Code: Select all

''=============================================================================
'' This source implements a 1/1024 second-resolution timer for DOS,
'' along with a sleep procedure that provides a similar resolution.
''
'' To avoid problems with the Interrupt 8/IRQ0 handler that fbgfx
'' appears to install under some conditions, and various problems
'' with non-standard PIT timer 0 configurations, this timer is
'' based on the RTC timer and Interrupt 70h/IRQ8.
''=============================================================================

''-------------------------------------------------------------
'' Use a 64-bit counter to avoid having to deal with overflow.
''-------------------------------------------------------------

dim shared as longint g_dos_timer_count

dim shared as integer g_prev_handler_offset
dim shared as short   g_prev_handler_selector

''=============================================================================

function DosTimer() as double

    return g_dos_timer_count / 1024

end function

''=============================================================================

sub DosSleep( byval ms as integer )

    dim as double t

    t = DosTimer + ms / 1024
    do
    loop until DosTimer >= t

end sub

''=============================================================================

sub timer_constructor constructor

    asm

        ''-------------------------------------------------------
        '' This jump is necessary to prevent inline execution of
        '' our interrupt handler and code-segment data.
        ''-------------------------------------------------------

        jmp 1f

        ''------------------------------------------------------------------
        '' This is our protected-mode interrupt handler. Since, in general,
        '' we cannot depend on any segment register other than CS in the
        '' handler, the safe approach for any data that the handler needs
        '' to access directly is to store it in the code segment. Since the
        '' the handler also needs to access data in the data segment, it
        '' must have access to the data-segment selector.
        ''------------------------------------------------------------------

        .balign 4
      INT70H_HANDLER:

        cli

        ''-------------------------------------------------------------
        '' A hardware interrupt handler should preserve all registers.
        ''-------------------------------------------------------------

        push ds
        push eax

        ''----------------------------------------------------------
        '' Set up access to the data segment through the DS segment
        '' register. Most instructions that access data use DS as
        '' the default. The CS override causes the instruction to
        '' use CS instead of DS.
        ''----------------------------------------------------------

        mov ax, cs:DS_SEL
        mov ds, ax

        ''----------------------------
        '' Increment the timer count.
        ''----------------------------

        add DWORD PTR G_DOS_TIMER_COUNT, 1
        adc DWORD PTR G_DOS_TIMER_COUNT+4, 0

        ''----------------------------------------------------
        '' Do not pass the interrupt to the previous handler,
        '' just do a dummy read of status register C.
        ''----------------------------------------------------

        mov al, 0xc       '' address of status register C
        or  al, 0x80      '' set bit 7 to disable NMI
        out 0x70, al      '' write to address port
        in  al, 0x71      '' do dummy read
        mov al, 0xd       '' set the address port to status
        out 0x70, al      ''  register D and enable NMI
        in  al, 0x71      '' do dummy read

        ''-----------------------------------------------------------
        '' For a hardware interrupt the last handler in the handler
        '' chain must issue an EOI to the interrupt controller to
        '' prepare it for the next interrupt, and since in this case
        '' IRQ 8 is from the slave interrupt controller we must
        '' issue an EOI to both interrupt controllers starting with
        '' the slave.
        ''-----------------------------------------------------------

        mov al, 0x20      '' non-specific EOI
        out 0xa0, al      '' PIC 2 (slave)
        mov al, 0x20      '' non-specific EOI
        out 0x20, al      '' PIC 1 (master)

        ''----------------------------
        '' Return from the interrupt.
        ''----------------------------

        pop eax
        pop ds
        sti
        iret

        ''--------------------------------
        '' This is our code-segment data.
        ''--------------------------------

        .balign 4
        DS_SEL: .short  0

      1:

        ''-------------------------------------------------------------------
        '' Since code segments are limited to execute-only or execute/read,
        '' to write to the code segment we need to use an alias descriptor,
        '' created with the DPMI Create Alias Descriptor function. The alias
        '' descriptor is identical to the original CS descriptor, except for
        '' the 4-bit Type field of the access byte. Where for the original
        '' CS descriptor the Type value 1011b specifies an execute/read code
        '' segment, for the alias descriptor the Type value 0010b specifies
        '' a read/write data segment. The function returns a selector in AX
        '' that we temporarily load into the ES segment register to store
        '' the data-segment selector to our code-segment data.
        ''-------------------------------------------------------------------

        push es
        mov ax, 0xa
        mov bx, cs
        int 0x31
        mov es, ax
        mov ax, ds
        mov es:DS_SEL, ax
        pop es

        ''------------------------------------------------------
        '' Set the rate selection divider control in RTC status
        '' register A for 1024 interrupts per second.
        ''------------------------------------------------------

        cli
        mov al, 0xa       '' address of status register A
        or  al, 0x80      '' set bit 7 to disable NMI
        out 0x70, al      '' write to address port
        in  al, 0x71      '' get current value
        and al, 0xF0      '' clear lower nibble
        or  al, 6         '' set to 6
        out 0x71, al      '' write it back

        ''------------------------------------------
        '' Enable the periodic interrupt by setting
        '' bit 6 of status register B.
        ''------------------------------------------

        mov al, 0xb       '' address of status register B
        or  al, 0x80      '' set bit 7 to disable NMI
        out 0x70, al      '' write to address port
        in  al, 0x71      '' get current value
        or  al, 0x40      '' set bit 6
        out 0x71, al      '' write it back
        mov al, 0xd       '' set the address port to status
        out 0x70, al      ''  register D and enable NMI
        in  al, 0x71      '' do dummy read

        ''----------------------------------------------------
        '' Unmask (allow) IRQ 8 (necessary for some systems).
        ''----------------------------------------------------

        in  al, 0xa1      '' get current mask for PIC 2
        and al, NOT 1     '' clear mask for IRQ 8
        out 0xa1, al      '' write it back

        ''--------------------------------------------------
        '' Get and save the selector and offset address for
        '' the previous Interrupt 70h (IRO8) handler.
        ''--------------------------------------------------

        mov ax, 0x204
        mov bl, 0x70
        int 0x31
        mov G_PREV_HANDLER_OFFSET, edx
        mov G_PREV_HANDLER_SELECTOR, cx

        ''----------------------------------------------------------------
        '' Lock the interrupt handler and code-segment data, so they will
        '' remain fixed in memory even under a DPMI implementation that
        '' supports virtual memory.
        ''----------------------------------------------------------------

        mov ax, 0x600
        mov ecx, OFFSET 1b
        sub ecx, OFFSET INT70H_HANDLER
        inc ecx
        mov di, cx
        shr ecx, 16
        mov si, cx
        mov ecx, OFFSET INT70H_HANDLER
        mov ebx, ecx
        shr ebx, 16
        int 0x31

        ''------------------------------------------------
        '' Point the Interrupt 70h vector to our handler.
        ''------------------------------------------------

        mov ax, 0x205
        mov bl, 0x70
        mov cx, cs
        mov edx, OFFSET INT70H_HANDLER
        int 0x31

    end asm

end sub

sub timer_destructor destructor

    asm

        ''------------------------------------------
        '' Restore RTC status register B to normal.
        ''------------------------------------------

        cli
        mov al, 0xb       '' address of status register B
        or  al, 0x80      '' set bit 7 to disable NMI
        out 0x70, al      '' write to address port
        in  al, 0x71      '' get current value
        and al, NOT 0x40  '' clear bit 6
        out 0x71, al      '' write it back
        mov al, 0xd       '' set the address port to status
        out 0x70, al      ''  register D and enable NMI
        in  al, 0x71      '' do dummy read
        sti

        ''-----------------------------------------------------------
        '' Restore the Interrupt 70h vector to the previous handler.
        ''-----------------------------------------------------------

        mov ax, 0x205
        mov bl, 0x70
        mov cx, G_PREV_HANDLER_SELECTOR
        mov edx, G_PREV_HANDLER_OFFSET
        int 0x31

        ''-----------------------------------------------------
        '' Unlock the interrupt handler and code-segment data.
        ''-----------------------------------------------------

        mov ax, 0x601
        mov ecx, OFFSET 1b
        sub ecx, OFFSET INT70H_HANDLER
        inc ecx
        mov di, cx
        shr ecx, 16
        mov si, cx
        mov ecx, OFFSET INT70H_HANDLER
        mov ebx, ecx
        shr ebx, 16
        int 0x31

    end asm

end sub

''=============================================================================
And some test code:

Code: Select all

#include "dostimer.bas"

'screen 12
'screenres 640,480,32
'width 80,30

dim as double t1,t2,accum

sleep 3000

for i as integer = 1 to 1000
    t1 = DosTimer
    do
        t2 = DosTimer
    loop until t2 > t1
    accum += t2 - t1
next
print using "DosTimer resolution: ##.### ms";accum

for i as integer = 1 to 1000
    t1 = Timer
    do
        t2 = Timer
    loop until t2 > t1
    accum += t2 - t1
next
print using "Timer resolution: ##.### ms";accum

sleep

Code: Select all

#include "dostimer.bas"

dim as double t1,t2

''----------------------------------------------------------
'' This code is intended to time against an external clock.
''----------------------------------------------------------

sleep
t1 = DosTimer
sleep
t2 = DosTimer
print t2-t1
sleep
t1 = Timer
sleep
t2 = Timer
print t2-t1
sleep

Code: Select all

#include "dostimer.bas"

dim as double t1,t2

'screen 12

sleep 3000

t1 = DosTimer
for i as integer = 1 to 1000
    sleep 1
next
t2 = DosTimer
print using "##.### ms";t2-t1

t1 = DosTimer
for i as integer = 1 to 1000
    sleep 10
next
t2 = DosTimer
print using "##.### ms";t2-t1

t1 = DosTimer
for i as integer = 1 to 1000
    sleep 20
next
t2 = DosTimer
print using "##.### ms";t2-t1

t1 = DosTimer
for i as integer = 1 to 1000
    DosSleep 1
next
t2 = DosTimer
print using "##.### ms";t2-t1

t1 = DosTimer
for i as integer = 1 to 1000
    DosSleep 10
next
t2 = DosTimer
print using "##.### ms";t2-t1

t1 = DosTimer
for i as integer = 1 to 1000
    DosSleep 20
next
t2 = DosTimer
print using "##.### ms";t2-t1

sleep
I used this code, with and without dostimer.bas included, to estimate the CPU usage of the RTC interrupt and handler. On my DOS test system with a 300MHz P2, I get ~97800 loops without the interrupt and ~83000 with, so the CPU usage is ~~15%.

Code: Select all

#include "dostimer.bas"

dim as longint i
dim as double t

t = timer
do
    i += 1
loop until timer - t > 10

print i

sleep
The CPU usage should be lower with a faster processor, and should vary directly with the interrupts per second. Here is a list of the most likely rate selection divider control values and the interrupts per second that they set:

Code: Select all

3   8192
4   4096
5   2048
6   1024
7   512
8   256
9   128
10  64
11  32
12  16
All of my initial tests were done with CWSDPMI. I have now tested with HDPMI. No problems detected, but one surprising detail is that in my CPU usage test the usage dropped to ~~2%, and the number of loops without the interrupt increased to ~126500.

I still have not added the STI ahead of the IRET, as recommended here, because with the handler as it currently is, not chaining to the previous handler, I cannot see how the IRET could fail to restore EFLAGS to what to what it was before the interrupt, overwriting the effects of the STI.

After more thought, and considering the two higher-priority hardware interrupts (Interrupt 8/IRQ0 and Interrupt 9/IRQ1), I decided the conservative approach would be to specifically clear the interrupt flag on entry to the handler and set it immediately before returning from the interrupt.
Post Reply