13
\$\begingroup\$

Whilst developing a game I needed a delay routine capable of doing delays ranging from 0.5 sec to just a few msec. The obvious choice was to use the BIOS delay function 86h on int 15h. In a true real address mode environment it works correctly but I saw that an emulator like DOSBox messes up things. Tons of illegal reads and writes. So I had to come up with another solution.

I can't use the TimeStampCounter because DOSBox delivers garbage for it. Solutions that reprogram the PIT or use the RTC interrupt have many pitfalls and will probably not even work under DOSBox.

My solution then is to find out how many iterations a delay routine can do in the interval between 2 ticks of the standard 18.2Hz timer. Those ticks are 55 msec apart. Because sometimes measurements can be erratic I only accept the results if 2 consecutive measurements vary by less than 1%%. Finally I divide the good measurement by 55 to obtain the number of iterations per msec aka SpeedFactor. Hereafter whenever I want to pause the program I multiply the desired delay expressed in msec by this SpeedFactor and then perform that number of iterations within the delay routine.

It is necessary to use the selfsame routine to both find out the SpeedFactor and to do the actual delays. That's why this routine has 2 exit conditions.

  • The elapsing of a 32 bit iteration count.
  • The elapsing of a 16 bit tick count.

During program setup the iteration counter is kind of disabled by assigning it an impossibly large value. Thus it's the tick counter criterion that will be used.
During normal program operation it is the tick counter that is kind of disabled by assigning it a very large value. Thus it's the iteration counter criterion that will be used.

; IN (dx:ax,bx) OUT (dx:ax,bx)
; Do DX:AX iterations or loop until Timer did BX Ticks
ShortWait:
        push    ds cx si di
        xchg    si, ax                  ;'mov si, ax'
        mov     di, dx
        xor     ax, ax
        cwd
        mov     ds, ax
.a:     mov     cx, [046Ch]             ;BIOS Timer
.b:     sub     si, 1
        sbb     di, 0
        jb      .c
        add     ax, 1
        adc     dx, 0
        cmp     cx, [046Ch]
        je      .b
        dec     bx
        jnz     .a
.c:     pop     di si cx ds
        ret

About the accompanying program I wrote.

The included demo uses many different delays ranging from 0 to 500 msec. The animation it shows is of little importance. I just want every frame to complete in 1 minute. The demo can run forever but will instantly stop if a key is pressed. I measured the time it took to show 10 frames. As expected the emulators deviate most. The real address mode (that didn't need my solution to begin with) produces a descent delay but DOSBox's results are pittyful.

                             Loops/msec Retries RelSpeed Runtime
----------------------------------------------------------------
Real mode
 Pentium-S   133 MHz  DOS6.2   22268     (0)      1.00x   10'00"
 Pentium-S   166 MHz  DOS6.2   27693     (0)      1.24x   10'00"
 Dual T2080 1730 MHz  DOS6.2  159306     (1)      7.15x   10'00"
 Atom N455  1666 MHz  DOS6.2  207823     (0)      9.33x   10'00"
DOS window (a)
 Pentium-S   133 MHz  WIN98    22097     (2)      0.99x   10'05"
 Pentium-S   166 MHz  WIN95    27499     (9)      1.23x   10'05"
 Dual T2080 1730 MHz  VISTA   342179     (9)     15.36x    9'59" (b)
 Atom N455  1666 MHz  WIN7    212042     (7)      9.52x   10'03" (b)
DOSBox on WIN7
 Max CPU cycles, frameskip 0    1802     (9)      0.08x    9'32"
 Max CPU cycles, frameskip 10   2109     (9)      0.09x   10'13"
----------------------------------------------------------------
(a) Windows apparantly always uses frameskip.
(b) A bug in Vista/Win7 messes up the first frame ???

By now I wanted to find out if delays smaller than 1 msec would be possible, thus stretching the principle of proportionality. I added a series of 10 second tests that repeat very tiny delays many times. Results are acceptable as long as delaying requires a sufficiently large number of iterations.

                               100 µs   10 µs    1 µs    0.1 µs
---------------------------------------------------------------
Real mode
 Pentium-S   133 MHz  DOS6.2     9.94"  10.08"  10.39"
 Pentium-S   166 MHz  DOS6.2     9.99"  10.03"  10.44"
 Dual T2080 1730 MHz  DOS6.2    10.03"   9.90"   9.99"  10.66"
 Atom N455  1666 MHz  DOS6.2     9.99"  10.03"  10.08"  10.57"
DOS window
 Pentium-S   133 MHz  WIN98      9.99"  10.03"  10.35"
 Pentium-S   166 MHz  WIN95      9.90"   9.99"  10.12"
 Dual T2080 1730 MHz  VISTA      9.76"   9.76"   9.81"  10.17"
 Atom N455  1666 MHz  WIN7       9.85"   9.85"   9.90"  10.26"
DOSBox on WIN7
 Max CPU cycles, frameskip 0     9.40"   9.85"
 Max CPU cycles, frameskip 10   10.08"  10.39"
---------------------------------------------------------------

Conclusion

I don't care about the exact duration of a single delaying instance.
I do care about how the user perceives the program speed be it a game or something else.
My low tech solution seems to be adequate although it could benefit from a better speed calculation especially under emulators. I'm not looking for any high tech solution that would probably just break under said emulators.

A word about my choices

To keep this programming project interesting I challenge myself to

  • Use only 8086 instructions
  • Work exclusively with the BIOS api
  • Use a freely available assembler, I chose FASM
  • Support at least one emulator, I chose DOSBox

Questions you might help me with

  1. What do think about my delay method? An apparent problem is that it's not synchronized with the real time meaning if I were to change my demo so it operates on e.g. a very elaborated graphical picture and didn't change the delay values I would end up with a running time longer than expected.

  2. I tried to obtain an animation with a speedprofile of starting slowly, then moving fast, and finally arriving gently. I don't think I succeeded. Perhaps the text screen is not suitable for such a visual effect or maybe it's the limited trajectory of only 17 discrete positions that's to blame. What do you think?

  3. What do you think about my programming in general? I'm not a regular FASM user so I still have to learn it's specifics. Especially the use of macro's is new to me.

  4. Do you like my commenting or do you think I didn't write enough comments?

  5. I always try to optimize for size. Can you clearly see where this was done?

The complete program

; ----------------------------------------------
                format  MZ
                stack   100h
                entry   TheCode:Main

; ----------------------------------------------
                segment TheCode
; ----------------------------------------------

macro SetCursorPosition
        {       mov     ah, 02h
                int     10h             }

macro GetCursorPosition
        {       mov     ah, 03h
                int     10h             }

macro TypeCharacter
        {       mov     ah, 0Eh
                int     10h             }

macro GetKeyboardKey
        {       mov     ah, 00h
                int     16h             }

macro TestKeyboardKey
        {       mov     ah, 01h
                int     16h             }
; ----------------------------------------------
; IN () OUT (dx:ax)
; Wait for the start of a new TimerTick period (54.9254 msec)
; Then measure a 4 tick period (219.7016 msec)
GetSpeedFactor: push    bx cx
                mov     bx, 1
                call    .ShortWait              ; -> DX:AX BX=0
                mov     bl, 40                  ;BH=0
                call    .ShortWait              ; -> DX:AX BX=0
                mov     cx, 10
                xchg    ax, cx
                mul     dx
                xchg    ax, cx
                mov     dx, 10
                mul     dx
                add     dx, cx
                mov     cx, 2197
                xchg    ax, bx                  ;BX=0
                xchg    dx, ax
                div     cx
                xchg    ax, bx
                div     cx
                mov     dx, bx
                pop     cx bx
                ret
; - - - - - - - - - - - - - - - - - - - - - - -
.ShortWait:     mov     ax, -1
                cwd
; ---   ---   ---   ---   ---   ---   ---   ---
; IN (dx:ax,bx) OUT (dx:ax,bx)
; Do DX:AX iterations or loop until Timer did BX Ticks
ShortWait:      push    ds cx si di
                xchg    si, ax                  ;'mov si, ax'
                mov     di, dx
                xor     ax, ax
                cwd
                mov     ds, ax
.a:             mov     cx, [046Ch]             ;BIOS Timer
.b:             sub     si, 1
                sbb     di, 0
                jb      .c
                add     ax, 1
                adc     dx, 0
                cmp     cx, [046Ch]
                je      .b
                dec     bx
                jnz     .a
.c:             pop     di si cx ds
                ret
; ----------------------------------------------
Main:           mov     ax, TheData
                mov     ds, ax
                mov     es, ax
                cld
; Measure the number of iterations (within the ShortWait routine) per msec
; Only accept if consecutive measurements vary by less than 1%%
; If measurements remain erratic than do accept the last one
                mov     bp, 10                  ;Max try
                call    GetSpeedFactor          ; -> DX:AX
.a:             xchg    si, ax                  ;'mov si, ax'
                mov     di, dx
                call    GetSpeedFactor          ; -> DX:AX
                push    ax dx                   ;(1)
.b:             sub     ax, si
                sbb     dx, di
                jnb     .c
                add     ax, si
                adc     dx, di
                xchg    si, ax
                xchg    di, dx
                jmp     .b
.c:             mov     cx, 1000
                xchg    ax, cx
                mul     dx
                xchg    ax, cx
                mov     dx, 1000
                mul     dx
                add     dx, cx
                sub     si, ax
                sbb     di, dx
                pop     dx ax                   ;(1)
                cmc
                dec     bp
                jnbe    .a
                mov     [SpeedFactor], ax
                mov     [SpeedFactor+2], dx
; Display the result
                mov     bx, 10                  ;CONST
                mov     si, Msg1
                call    .TypeString
                mov     ax, [SpeedFactor]
                mov     dx, [SpeedFactor+2]
                push    bx                      ;BX=10 Sentinel
.d:             xor     cx, cx
                xchg    ax, cx
                xchg    dx, ax
                div     bx                      ;BX=10
                xchg    ax, cx
                div     bx                      ;BX=10
                xchg    dx, cx
                push    cx
                mov     cx, ax
                or      cx, dx
                jnz     .d
                pop     ax
.e:             add     al, "0"
                TypeCharacter
                pop     ax
                cmp     ax, bx                  ;BX=10 Sentinel
                jb      .e
; Display the number of retries when measuring was done
                sub     ax, bp
                add     al, "0"-1
                mov     [Msg2_], al
                mov     si, Msg2
                call    .TypeString
; Compare this computer to a (20 year old) Pentium-S at 133 MHz
                mov     cx, 100                 ;Percentage
                mov     ax, [SpeedFactor+2]
                mul     cx
                xchg    cx, ax
                mul     [SpeedFactor]
                add     dx, cx
                mov     cx, 22268               ;Pentium
                div     cx
                mov     di, 6
                sub     sp, di
                mov     bp, sp
.f:             lea     si, [di-1]
.g:             xor     dx, dx
                div     bx                      ;BX=10
                add     dl, "0"
.h:             dec     di
                mov     [bp+di], dl
                jz      .i
                mov     dl, "."
                cmp     di, 4
                je      .h
                test    ax, ax
                jnz     .f
                jmp     .g
; Display the result with 2 decimal places
.i:             cmp     si, 3
                jb      .j
                mov     si, 2
.j:             mov     al, [bp+si]
                TypeCharacter
                inc     si
                cmp     si, 6
                jb      .j
                add     sp, si
                mov     si, Msg3
                call    .TypeString
; Next comes a little demo that uses a variety of delays
; Get that stopwatch out
                call    .GetKey                 ; -> AX
; First setup the frame
.Demo:          mov     cx, 38
                mov     al, "Ú"
.k:             TypeCharacter
                mov     al, "Ä"
                loop    .k
                mov     al, "¿"
                call    .TypeString__
                mov     bl, 3                   ;BH=0
.l:             mov     cl, 18                  ;CH=0
                mov     al, "Ã"
.m:             TypeCharacter
                mov     al, "Ä"
                loop    .m
                mov     cl, 20                  ;CH=0
                mov     al, 254
.n:             TypeCharacter
                loop    .n
                mov     al, "´"
                call    .TypeString__
                dec     bx
                jnz     .l
                mov     cl, 38                  ;CH=0
                mov     al, "À"
.o:             TypeCharacter
                mov     al, "Ä"
                loop    .o
                mov     al, "Ù"
                call    .TypeString__
; Then move all the little bricks to the left
                GetCursorPosition               ; -> DX (AX CX)
                sub     dh, 4
                mov     dl, 1+16
                mov     cx, 20
.p:             push    cx dx                   ;(2)
                mov     si, SpeedProfile
                mov     di, 17
.q:             mov     bl,3                    ;BH=0
.r:             SetCursorPosition
                mov     al, 254
                TypeCharacter
                mov     al, "Ä"
                TypeCharacter
                inc     dh
                dec     bx
                jnz     .r
                sub     dx, 0301h
                push    bx dx                   ;(3)
                mov     ax, [SpeedFactor+2]
                mul     word [si]
                xchg    bx, ax                  ;'mov bx, ax'
                lodsw
                mul     [SpeedFactor]
                add     dx, bx
                mov     bx, -1
                call    ShortWait               ; -> DX:AX BX
                pop     dx bx                   ;(3)
                dec     di
                jnz     .q
                pop     dx cx                   ;(2)
                inc     dx                      ;'inc dl'
                TestKeyboardKey                 ; -> AX ZF
                jnz     .EndOfDemo
                loop    .p
; Finally increase and display the FrameCounter
; One can only hope that 38 decimal digits are enough
                mov     si, CRLF
.s:             dec     si
                mov     al, "0"
                xchg    [si], al
                cmp     al, " "
                je      .t
                cmp     al, "9"
                je      .s
                mov     [si], al
.t:             inc     byte [si]
                mov     si, Msg4
                call    .TypeString
; Now is a good time to read your stopwatch
; The demo repeats itself
                jmp     .Demo
.EndOfDemo:     GetKeyboardKey                  ; -> AX
; Next comes a series of tests with truly tiny delays
                mov     si, Msg5
                call    .TypeString             ; -> SI
                xor     cx, cx                  ;CONST
; Calculate DelayCount for 0.1, 0.01, 0.001, 0.0001, and 0.00001 msec
.Test:          mov     di, 10
                mov     ax, [SpeedFactor+2]
                xor     dx, dx
                div     di
                push    ax
                mov     ax, [SpeedFactor]
                div     di
                cmp     dx, 5
                pop     dx
                mov     [SpeedFactor], ax
                mov     [SpeedFactor+2], dx
                cmc
                adc     ax, cx                  ;CX=0
                adc     dx, cx                  ;CX=0
                push    dx ax                   ;(4)
                sub     ax, di                  ;DI=10
                sbb     dx, cx                  ;CX=0
                jb      .EndOfTests
; Set RepeatCount to 100K, 1M, 10M, 100M, and 1G
                mov     ax, [RepeatCount+2]
                mul     di
                xchg    di, ax
                mul     [RepeatCount]
                add     dx, di
                mov     [RepeatCount], ax
                mov     [RepeatCount+2], dx
                sub     ax, 1
                sbb     dx, cx                  ;CX=0
                push    dx ax                   ;(5)
; Get ready for START message
                call    .TypeString             ; -> SI
                call    .GetKey                 ; -> AX
                push    ds                      ;(6)
                mov     ds, cx                  ;CX=0
                mov     cl, 55                  ;CH=0 3 sec
.u:             mov     ax, [046Ch]             ;BIOS Timer
.v:             cmp     ax, [046Ch]
                je      .v
                loop    .u
                pop     ds                      ;(6)
; The 10 second test
                push    si                      ;(7)
                mov     si, MsgStart
                call    .TypeString
                pop     si                      ;(7)
                pop     ax dx di bp             ;(5)(4)
                push    bx                      ;(8)
                nop                             ;'cli'
.w:             mov     bx, -1
                call    ShortWait               ; -> (BX)
                sub     di, 1
                sbb     bp, cx                  ;CX=0
                jnb     .w
                nop                             ;'sti'
                pop     bx                      ;(8)
                push    si                      ;(9)
                mov     si, MsgStop
                call    .TypeString
                pop     si                      ;(9)
                cmp     si, Msg6
                jb      .Test
; Final message before exiting
.EndOfTests:    mov     si, Msg6
                call    .TypeString
                GetKeyboardKey                  ; -> AX
.ExitToDOS:     mov     ax, 4C00h
                int     21h                     ;DOS 'Terminate'
; - - - - - - - - - - - - - - - - - - - - - - -
.GetKey:        GetKeyboardKey                  ; -> AX
                cmp     al, 27
                je      .ExitToDOS
                ret
; - - - - - - - - - - - - - - - - - - - - - - -
.TypeString__:  mov     si, CRLF
.TypeString_:   TypeCharacter
.TypeString:    lodsb
                cmp     al, 0
                jne     .TypeString_
                ret

; ----------------------------------------------
                segment TheData
; ----------------------------------------------

RepeatCount     dw      10000,0
SpeedFactor     dw      0,0
SpeedProfile    dw      430,230,150,99,99,99,99,99,99,99,99,99,99,300,400,500,0
Msg1            db      'The delay routine does ',0
Msg2            db      ' iterations per msec.',13,10
                db      'Number of retries to obtain this result : '
 Msg2_          db      ' ',13,10,'This computer operates at ',0
Msg3            db      ' x the speed of a Pentium-S at 133 MHz.',13,10,10
                db      'The following demo completes each frame in 1 minute.'
                db      13,10,'Pressing a key at any time will stop the demo.'
                db      13,10,'Now press any key to start it...',13,10,10,0
Msg4            db      13,10,10,38 dup (' ')
 CRLF           db      13,10,0
Msg5            db      13,10,10,10,'Next comes a series of 10 second tests'
                db      ' using ever smaller delays.',13,10,'Use a stopwatch'
                db      ' on seeing messages START STOP.',13,10,10,0
                db      'Press key for 0.1 msec test...     ',0
                db      'Press key for 0.01 msec test...    ',0
                db      'Press key for 0.001 msec test...   ',0
                db      'Press key for 0.0001 msec test...  ',0
                db      'Press key for 0.00001 msec test... ',0
 Msg6           db      10,'Thank you...',13,10,10,0
MsgStart        db      'START',0
MsgStop         db      ' STOP',13,10,0
\$\endgroup\$
4
\$\begingroup\$

Congratulations with this excellent software! I recently recommended it to other people.

I'll start by pointing out that you have a small but crucially important typo in your GetSpeedFactor routine. You typed mov bl, 40 whereas mov bl, 4 would have been correct in accordance with the comments you wrote.


I ran the corrected program on my Celeron (test) computer and have added my results to your tables:

                             Loops/msec Retries RelSpeed Runtime
----------------------------------------------------------------
Real mode
 Pentium-S   133 MHz  DOS6.2   22268     (0)      1.00x   10'00"
 Pentium-S   166 MHz  DOS6.2   27693     (0)      1.24x   10'00"
 Dual T2080 1730 MHz  DOS6.2  159306     (1)      7.15x   10'00"
 Atom N455  1666 MHz  DOS6.2  207823     (0)      9.33x   10'00"
 Celeron    1400 MHz  DOS6.2  199319     (0)      6.95x   10'00"
DOS window (a)
 Pentium-S   133 MHz  WIN98    22097     (2)      0.99x   10'05"
 Pentium-S   166 MHz  WIN95    27499     (9)      1.23x   10'05"
 Dual T2080 1730 MHz  VISTA   342179     (9)     15.36x    9'59" (b)
 Atom N455  1666 MHz  WIN7    212042     (7)      9.52x   10'03" (b)
 Celeron    1400 MHz  XP      197417     (8)      8.86x    9'59" (b)
DOSBox on WIN7
 Max CPU cycles, frameskip 0    1802     (9)      0.08x    9'32"
 Max CPU cycles, frameskip 10   2109     (9)      0.09x   10'13"
DOSBox on XP
 Max CPU cycles, frameskip 0     428     (0)      0.01x   10'00"
 Max CPU cycles, frameskip 10    428     (0)      0.01x   10'00"
----------------------------------------------------------------
(a) Windows apparantly always uses frameskip.
(b) A bug in XP/Vista/Win7 messes up the first frame ???

As you can see results are near perfect and so I can't share your opinion on DOSBox being pittyful in this regard. To correct the messed-up first frame read my answer to your 2nd question.

                               100 µs   10 µs    1 µs    0.1 µs
---------------------------------------------------------------
Real mode
 Pentium-S   133 MHz  DOS6.2     9.94"  10.08"  10.39"
 Pentium-S   166 MHz  DOS6.2     9.99"  10.03"  10.44"
 Dual T2080 1730 MHz  DOS6.2    10.03"   9.90"   9.99"  10.66"
 Atom N455  1666 MHz  DOS6.2     9.99"  10.03"  10.08"  10.57"
 Celeron    1400 MHz  DOS6.2     9.90"   9.81"   9.99"  10.48"
DOS window
 Pentium-S   133 MHz  WIN98      9.99"  10.03"  10.35"
 Pentium-S   166 MHz  WIN95      9.90"   9.99"  10.12"
 Dual T2080 1730 MHz  VISTA      9.76"   9.76"   9.81"  10.17"
 Atom N455  1666 MHz  WIN7       9.85"   9.85"   9.90"  10.26"
 Celeron    1400 MHz  XP         9.94"   9.72"   9.90"  10.44"
DOSBox on WIN7
 Max CPU cycles, frameskip 0     9.40"   9.85"
 Max CPU cycles, frameskip 10   10.08"  10.39"
DOSBox on XP
 Max CPU cycles, frameskip 0    10.21"
 Max CPU cycles, frameskip 10   10.00"
---------------------------------------------------------------

1.What do think about my delay method? An apparent problem is that it's not synchronized with the real time meaning if I were to change my demo so it operates on e.g. a very elaborated graphical picture and didn't change the delay values I would end up with a running time longer than expected.

True, but then again you would be twiddling with these delay values anyway as part of normal program development. Also be aware that the luxury to have these delay values adjusted automatically might come with a severe cost and no longer qualify your solution as low tech.


2.I tried to obtain an animation with a speedprofile of starting slowly, then moving fast, and finally arriving gently. I don't think I succeeded. Perhaps the text screen is not suitable for such a visual effect or maybe it's the limited trajectory of only 17 discrete positions that's to blame. What do you think?

A short 17-step trajectory is too limited to really appreciate multiple (3) speeds but also the moving of whole characters at a time will inevitably cause a jerky sensation.
The solution lies in carefully reshaping some characters to simulate the fine movement found on the graphics screen.

This is what you did:

........ .oooooo.
oooooooo .oooooo.
........ .oooooo.   C4h,FEh

Step 1
.oooooo. ........
.oooooo. oooooooo
.oooooo. ........   FEh,C4h +Delay

This is what you could do:

........ .oooooo.
oooooooo .oooooo.
........ .oooooo.   C4h,FEh

Step 1
........ oooooo..
ooooooo. oooooo.o
........ oooooo..   C8h,C9h +Delay/8

Step 2
.......o ooooo...
oooooo.o ooooo.oo
.......o ooooo...   CAh,CBh +Delay/8

Step 3
......oo oooo....
ooooo.oo oooo.ooo
......oo oooo....   CCh,CDh +Delay/8

Step 4
.....ooo ooo.....
oooo.ooo ooo.oooo
.....ooo ooo.....   CEh,CFh +Delay/8

Step 5
....oooo oo......
ooo.oooo oo.ooooo
....oooo oo......   D0h,D1h +Delay/8

Step 6
...ooooo o.......
oo.ooooo o.oooooo
...ooooo o.......   D2h,D3h +Delay/8

Step 7
..oooooo ........
o.oooooo .ooooooo
..oooooo ........   D4h,D5h +Delay/8

Step 8
.oooooo. ........
.oooooo. oooooooo
.oooooo. ........   D6h,D7h +Delay/8

You can use the BIOS function UserAlphaLoad with AX=1110h on INT 10h to create these 16 intermediate shapes. Since the last 2 shapes already exist you could just create 14 shapes. Consult the BIOS manual to setup the parameters. Instead of using Delay/8 you could define unique delay values for all of these fine steps, but I don't think it would be worth the effort. As always: Trying is knowing.
Please note:

  • The text video modes on VGA very often use a 9x16 character box. Without the proper precaution a carefully planned smooth animation will suffer the effect of an empty 9th pixel column. The trick is to only redefine ASCII's in the range from C0h to DFh because for these characters the 9th pixel is automatically set to the value of the 8th pixel. DOSBox luckily does not use the 9x16 character box, and the Windows DOS window simply doesn't allow redefining characters.
  • The problem with the messed-up first frame in the Windows DOS window disappears with using some font-related BIOS function early in the program! My guess would be that the NTVDM starts up with a non-256-character font but when in your code the commands arrive to display all of those extended ASCII's the NTVDM quickly changes its font disrupting what had already been rendered.

3.What do you think about my programming in general? I'm not a regular FASM user so I still have to learn it's specifics. Especially the use of macro's is new to me.

Your clear programming style gives away that you must be an experienced programmer. I see no point in trying to change what's already established. But I do think, and the other answerer also said this, that you should declare more constants. Perusing your code I can up with these:

BiosTimer equ [046Ch]

BoxUpperLeft=218
BoxHorizontal=196
BoxUpperRight=191
BoxLeftConnection=195
BoxRightConnection=180
BoxLowerLeft=192
BoxLowerRight=217
Rod=BoxHorizontal
Brick=254
FirstReshapedChar=0C8h
Trajectory=17
Bricks=20

FASM offers both symbolic constants (defined with equ) and numerical constants (defined with =). The symbolic constants are replaced with their values during the preprocessor phase and basically replace a text by another text. The numerical constants are replaced with their values during the assembly phase and basically replace a text by the result of a numerical expression. FASM allows to re-assign values to the numerical constants, thus turning them into assembly-time variables.


4.Do you like my commenting or do you think I didn't write enough comments?

Some people will say that there can never be enough comments. I disagree. I like to see myself as a programmer, not as a novelist. Your program is only slightly under-commented. I would have loved to see an extra comment each time you have a calculation going (and there are quite a few).
Why not replace

; Calculate DelayCount for 0.1, 0.01, 0.001, 0.0001, and 0.00001 msec
.Test:          mov     di, 10
                mov     ax, [SpeedFactor+2]

with

; Calculate DelayCount for 0.1, 0.01, 0.001, 0.0001, and 0.00001 msec
.Test:          mov     di, 10                  ;SpeedFactor=(SpeedFactor/10)
                mov     ax, [SpeedFactor+2]

5.I always try to optimize for size. Can you clearly see where this was done?

Yes, these are well indicated.

\$\endgroup\$
  • \$\begingroup\$ Thanks for this awesome answer. Glad to see that you actually ran the program. I hope to post an updated version of my program very soon. \$\endgroup\$ – Sep Roland Dec 13 '15 at 16:41
7
\$\begingroup\$

New solution

At the beginning of your post, you mention having troubles with creating a timer and using the BIOS interrupts 86h and 15h:

In a true real address mode environment it works correctly but I saw that an emulator like DOSBox messes up things. Tons of illegal reads and writes.

I personally have never used these interrupts, nor have I ever heard of them.

In the past, when I have had to make a subroutine for a delay, I have always thought to use the interrupt 1Ah 00h.

This is really simple interrupt: when used, it will return the tick count in CX:DX.

Obtains the current tick-count. The tick count is stored at 0040:006E in the BIOS Data Area. It is incremented about once every 55 ms by INT 08H.


Stats

The tick-counter is updated quite frequently: about every 55 milliseconds, the counter is increased.

Approximately 18.2 ticks is equivalent to a single second. This can be scaled up and down for entire minutes or for half seconds.


Flow

Thanks to the simplicity of this interrupt, it is very easy to create a timing method. All that needs to be done is to loop and then to do some comparing of values to make sure that a certain amount of ticks have passed by.

The general flow of a subroutine for "sleeping" with this interrupt would go like this:

  1. Store the tick count
  2. Add to the stored tick count the amount of ticks to wait
  3. Get the tick count
  4. If the new tick count is equal to the one stored, end.
  5. Go to 3.

See? Very simple, and very straightforward.


Code

Here is what that subroutine would look like:

; input: bx = ticks to wait
sleep:
    mov ah, 0
    int 1Ah
    add bx, dx     ; current tick count + ticks to wait

.stall:
    int 1Ah

    cmp dx, bx     ; if we have advanced BX ticks
    jne .stall

    ret

Note: I am not familiar with the syntax of the FASM. This code was written with my knowledge of NASM.

Of course, if needed, you may have to preserve some registers on the stack.


Reasoning

I do not like your method for timing. The main reason? Portability. There are many, many different types of processors, and (most) all are going to have different clocks.

Different clocks means different speeds, which means unexpected results from this code. If you this were running on a very fast processor, this code would be run too fast, and if this were running on a very slow processor, this code would be run too slow. Everything is blown out of proportion.


However, using this timing method that I introduced, the timing will be the same across many processors, assuming they have the same implementation of interrupt 1Ah.

This provides you with a very reliable source of timing, so you know that two seconds on one machine will be the same two seconds on another.


Formatting

Your formatting is little inconsistent in a few places.

Let's start with the comments.


Comments

Generally, in assembly, comments are all on the same column. This greatly enhances readability.

Why? My thoughts on it are this: as someone is reading your code, they are going to be used to seeing all of the assembler instructions on the same column. Therefore, it is better on the eyes to also have all the comments on the same line.

Here is a place that could you use some fixing:

call    .ShortWait      ; -> DX:AX BX=0
mov bl, 40          ;BH=0
call    .ShortWait      ; -> DX:AX BX=0

here, you would move the comment ;BH=0 to the right so it lines up with it's upper and lower comments.


Labels

When it comes to formatting labels in assembly, there are generally two types of people:

label: instruction
       instruction

and:

label:
    instruction
    instruction

Looking at your code, you like to go with the first version. While this is okay, you are allowing this method to tamper with your formatting.

Throughout your code, you do a good job keeping all your instructions on the same column. The only problem lines are the instructions right after the label:

GetSpeedFactor: push    bx cx
    mov bx, 1

With help from the comment above, I wasn't even able to see that first instruction the first few times I read over your code.

If you are going to go with this version of formatting labels, I recommend that you shift the rest of your instructions to the right to meet the same column as the first instruction.

I, personally, use the second version for labels, because it helps keep the formatting on the entire code very consistent.


Instructions

For the most part, you are consistent when formatting your instructions that take two "arguments":

add dx, cx
mov cx, 2197

I like this: one space after the instruction and one space after the first "argument".

However, you aren't consistent with this:

xchg    ax, bx          ;BX=0
xchg    dx, ax

Magic numbers

You have a lot of magic numbers/values in your code:

cmp cx, [046Ch]

and

mov bp, 10

and

mov di, 6

While it's good that you have comments on some of these, these comments add a lot of noise to your code.

I don't know how you would do it in the FASM, but I recommend that you create constants at the top of your code, and then use these constants rather than comments.

In the NASM, you would do:

%define CONSTANT VALUE

So, for example, you could do:

%define BIOS_TIMER [046Ch]

Labels

Your global label naming is very good: for the most part, it is easy to understand the purpose of what a subroutine does.

For example, this is clear:

GetSpeedFactor: ...

However, when it gets to sub-labels in your code, things get very, very confusing.

Take these sub-labels for example:

.a:
...
.b:
...
.c:

These sub-labels give absolutely no clue as to what the subroutine is doing in this section of the code. Please use more descriptive names so that when someone is reading this code, it is easy to follow the flow.

\$\endgroup\$
  • \$\begingroup\$ I did not refer to interrupt 86h. I wrote function 86h on int 15h. That's a very real function! The interrupt 1Ah 00h that you mention is not available in a DOS window like NTVDM. The method you propose does not offer the single millisecond resolution that I need. \$\endgroup\$ – Sep Roland Aug 15 '15 at 16:06
  • \$\begingroup\$ @user3144770 I do not know why that 1Ah is not available on that DOS emulator, because it is available on mine. Yes, my method does not necessarily allow millisecond resolution, but neither does your method (necessarily). (1) As I explained in my reasoning section, you are only going to get that precision on very specific code for a very specific processor. (2) If you need a time amount that small, use nop. (3) Do you really need a millisecond? I don't think a human can tell the difference between one millisecond and 55 milliseconds. \$\endgroup\$ – SirPython Aug 15 '15 at 16:12
  • 1
    \$\begingroup\$ All formatting issues stem from how CodeReview processes my code. I pasted a clean layout with all mnemonics, operands, and comments in their proper column \$\endgroup\$ – Sep Roland Aug 15 '15 at 16:16
  • 1
    \$\begingroup\$ @user3144770 Ah yes, I figured that it was something to do with that. No worries: I'm not holding anything against you :-) \$\endgroup\$ – SirPython Aug 15 '15 at 16:16
  • \$\begingroup\$ I ran my program on 4 different machines (133 MHz - 1.73 GHz) and results differ barely. \$\endgroup\$ – Sep Roland Aug 15 '15 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.