The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Twenty Four (Part 2)

Table of Content

Chapter Twenty Four (Part 4)

CHAPTER TWENTY FOUR:
THE PC GAME ADAPTER (Part 3)
24.6 - An SGDI Driver for the CH Products' Flight Stick Pro'
24.6 An SGDI Driver for the CH Products' Flight Stick Pro'

The CH Product's FlightStick Pro joystick is a good example of a specialized product for which the SGDI driver is a perfect solution. The FlightStick Pro provides three pots and five switches, the fifth switch being a special five-position cooley switch. Although the pots on the FlightStick Pro map to three of the analog inputs on the standard game adapter card (pots zero, one, and three), there are insufficient digital inputs to handle the eight inputs necessary for the FlightStick Pro's four buttons and cooley switch.

The FlightStick Pro (FSP) uses some electronic circuitry to map these eight switch positions to four input bits. To do so, they place one restriction on the use of the FSP switches - you can only press one of them at a time. If you hold down two or more switches at the same time, the FSP hardware selects one of the switches and reports that value; it ignores the other switches until you release the button. Since only one switch can be read at a time, the FSP hardware generates a four bit value that determines the current state of the switches. It returns these four bits as the switch values on the standard game adapter card. The following table lists the values for each of the switches:

FlightStick Pro Switch Return Values
Value (binary) Priority Switch Position
0000 Highest Up position on the cooley switch.
0100 7 Right position on the cooley switch.
1000 6 Down position on the cooley switch.
1100 5 Left position on the cooley switch.
1110 4 Trigger on the joystick.
1101 3 Leftmost button on the joystick.
1011 2 Rightmost button on the joystick.
0111 Lowest Middle button on the joystick.
1111 - No buttons currently down.

Note that the buttons look just like a single button press. The cooley switch positions contain a position value in bits six and seven; bits four and five always contain zero when the cooley switch is active.

The SGDI driver for the FlightStick Pro is very similar to the standard game adapter card SGDI driver. Since the FlightStick Pro only provides three pots, this code doesn't bother trying to read pot 2 (which is non-existent). Of course, the switches on the FlightStick Pro are quite a bit different than those on standard joysticks, so the FSP SGDI driver maps the FPS switches to eight of the SGDI logical switches. By reading switches zero through seven, you can test the following conditions on the FSP:

Flight Stick Pro SGDI Switch Mapping
This SGDI Switch number: Maps to this FSP Switch:
0 Trigger on joystick.
1 Left button on joystick.
2 Middle button on joystick.
3 Right button on joystick.
4 Cooley up position.
5 Cooley left position.
6 Cooley right position.
7 Cooley down position.

The FSP SGDI driver contains one other novel feature, it will allow the user to swap the functions of the left and right switches on the joystick. Many games often assign important functions to the trigger and left button since they are easiest to press (right handed players can easily press the left button with their thumb). By typing "LEFT" on the command line, the FSP SGDI driver will swap the functions of the left and right buttons so left handed players can easily activate this function with their thumb as well.

The following code provides the complete listing for the FSPSGDI driver. Note that you can use the same test program from the previous section to test this driver.

                .286
                page    58, 132
                name    FSPSGDI
                title   FSPSGDI (CH Products Standard Game Device Interface).

; FSPSGDI.EXE
;
;       Usage:
;               FSPSDGI {LEFT}
;
; This program loads a TSR which patches INT 15 so arbitrary game programs
; can read the CH Products FlightStick Pro joystick in a portable fashion.


wp              equ     <word ptr>
byp             equ     <byte ptr>



; We need to load cseg in memory before any other segments!

cseg            segment para public 'code'
cseg            ends


; Initialization code, which we do not need except upon initial load,
; goes in the following segment:

Initialize      segment para public 'INIT'
Initialize      ends

; UCR Standard Library routines which get dumped later on.

                .xlist
                include         stdlib.a
                includelib      stdlib.lib
                .list

sseg            segment para stack 'stack'
sseg            ends

zzzzzzseg       segment para public 'zzzzzzseg'
zzzzzzseg       ends



CSEG            segment para public 'CODE'
                assume  cs:cseg, ds:nothing

Int15Vect       dword   0

PSP             word    ?

; Port addresses for a typical joystick card:

JoyPort         equ     201h
JoyTrigger      equ     201h


CurrentReading word     0

Pot             struc
PotMask         byte    0               ;Pot mask for hardware.
DidCal          byte    0               ;Is this pot calibrated?
min             word    5000            ;Minimum pot value
max             word    0               ;Max pot value
center          word    0               ;Pot value in the middle
Pot             ends

Pot0            Pot     <1>
Pot1            Pot     <2>
Pot3            Pot     <8>


; SwapButtons-  0 if we should use normal flightstick pro buttons,
;               1 if we should swap the left and right buttons.

SwapButtons     byte    0

; SwBits- the four bit input value from the Flightstick Pro selects one
;        of the following bit patterns for a given switch position.

SwBits          byte    10h             ;Sw4
                byte    0               ;NA
                byte    0               ;NA
                byte    0               ;NA
                byte    40h             ;Sw6
                byte    0               ;NA
                byte    0               ;NA
                byte    4               ;Sw 2

                byte    80h             ;Sw 7
                byte    0               ;NA
                byte    0               ;NA
                byte    8               ;Sw 3
                byte    20h             ;Sw 5
                byte    2               ;Sw 1
                byte    1               ;Sw 0
                byte    0               ;NA

SwBitsL         byte    10h             ;Sw4
                byte    0               ;NA
                byte    0               ;NA
                byte    0               ;NA
                byte    40h             ;Sw6
                byte    0               ;NA
                byte    0               ;NA
                byte    4               ;Sw 2

                byte    80h             ;Sw 7
                byte    0               ;NA
                byte    0               ;NA
                byte    2               ;Sw 3
                byte    20h             ;Sw 5
                byte    8               ;Sw 1
                byte    1               ;Sw 0
                byte    0               ;NA



; The IDstring address gets passed back to the caller on a testpresence
; call. The four bytes before the IDstring must contain the serial number
; and current driver number.

SerialNumber    byte    0,0,0
IDNumber        byte    0
IDString        byte    "CH Products:Flightstick Pro",0
                byte    "Written by Randall Hyde",0


;============================================================================
;
; ReadPots-     AH contains a bit mask to determine which pots we should read.
;               Bit 0 is one if we should read pot 0, bit 1 is one if we should
;               read pot 1, bit 3 is one if we should read pot 3. All other bits
;               will be zero.
;
;       This code returns the pot values in SI, BX, BP, and DI for Pot 0, 1,
;       2, & 3.
;

ReadPots        proc    near
                sub     bp, bp
                mov     si, bp
                mov     di, bp
                mov     bx, bp

; Wait for pots to finish any past junk:

                mov     dx, JoyPort
                out     dx, al                  ;Trigger pots
                mov     cx, 400h
Wait4Pots:      in      al, dx
                and     al, 0Fh
                loopnz  Wait4Pots

; Okay, read the pots:

                mov     dx, JoyTrigger
                out     dx, al                  ;Trigger pots
                mov     dx, JoyPort
                mov     cx, 8000h               ;Don't let this go on forever.
PotReadLoop:    in      al, dx
                and     al, ah
                jz      PotReadDone
                shr     al, 1
                adc     si, 0
                shr     al, 1
                adc     bp, 0
                shr     al, 2
                adc     di, 0
                loop    PotReadLoop
PotReadDone:
                ret
ReadPots        endp

;----------------------------------------------------------------------------
;
; Normalize-    BX contains a pointer to a pot structure, AX contains
;               a pot value. Normalize that value according to the
;               calibrated pot.
;
; Note: DS must point at cseg before calling this routine.


                assume  ds:cseg
Normalize       proc    near
                push    cx

; Sanity check to make sure the calibration process went okay.

                cmp     [bx].Pot.DidCal, 0
                je      BadNorm
                mov     dx, [bx].Pot.Center
                cmp     dx, [bx].Pot.Min
                jbe     BadNorm
                cmp     dx, [bx].Pot.Max
                jae     BadNorm

; Clip the value if it is out of range.

                cmp     ax, [bx].Pot.Min
                ja      MinOkay
                mov     ax, [bx].Pot.Min
MinOkay:

                cmp     ax, [bx].Pot.Max
                jb      MaxOkay
                mov     ax, [bx].Pot.Max
MaxOkay:

; Scale this guy around the center:

                cmp     ax, [bx].Pot.Center
                jb      Lower128

; Scale in the range 128..255 here:

                sub     ax, [bx].Pot.Center
                mov     dl, ah                  ;Multiply by 128
                mov     ah, al
                mov     dh, 0
                mov     al, dh
                shr     dl, 1
                rcr     ax, 1
                mov     cx, [bx].Pot.Max
                sub     cx, [bx].Pot.Center
                jz      BadNorm                 ;Prevent division by zero.
                div     cx                      ;Compute normalized value.
                add     ax, 128                 ;Scale to range 128..255.
                cmp     ah, 0
                je      NormDone
                mov     ax, 0ffh                ;Result must fit in 8 bits!
                jmp     NormDone

; Scale in the range 0..127 here:

Lower128:       sub     ax, [bx].Pot.Min
                mov     dl, ah                  ;Multiply by 128
                mov     ah, al
                mov     dh, 0
                mov     al, dh
                shr     dl, 1
                rcr     ax, 1
                mov     cx, [bx].Pot.Center
                sub     cx, [bx].Pot.Min
                jz      BadNorm
                div     cx                      ;Compute normalized value.
                cmp     ah, 0
                je      NormDone
                mov     ax, 0ffh                ;Result must fit in 8 bits!
                jmp     NormDone

BadNorm:        sub     ax, ax
NormDone:       pop     cx
                ret
Normalize       endp
                assume  ds:nothing

;============================================================================
; INT 15h handler functions.
;============================================================================
;
; Although these are defined as near procs, they are not really procedures.
; The MyInt15 code jumps to each of these with BX, a far return address, and
; the flags sitting on the stack. Each of these routines must handle the
; stack appropriately.
;
;----------------------------------------------------------------------------
; BIOS- Handles the two BIOS calls, DL=0 to read the switches, DL=1 to
;       read the pots. For the BIOS routines, we'll ignore the cooley
;       switch (the hat) and simply read the other four switches.

BIOS            proc    near
                cmp     dl, 1                   ;See if switch or pot routine.
                jb      Read4Sw
                je      ReadBIOSPots
                pop     bx
                jmp     cs:Int15Vect            ;Let someone else handle it!

Read4Sw:                push    dx
                mov     dx, JoyPort
                in      al, dx
                shr     al, 4
                mov     bl, al
                mov     bh, 0
                cmp     cs:SwapButtons, 0
                je      DoLeft2
                mov     al, cs:SwBitsL[bx]
                jmp     SBDone

DoLeft2:        mov     al, cs:SwBits[bx]
SBDone:         rol     al, 4           ;Put Sw0..3 in upper bits and make
                not     al              ; 0=switch down, just like game card.
                pop     dx
                pop     bx
                iret

ReadBIOSPots:   pop     bx              ;Return a value in BX!
                push    si
                push    di
                push    bp
                mov     ah, 0bh
                call    ReadPots
                mov     ax, si
                mov     bx, bp
                mov     dx, di
                sub     cx, cx
                pop     bp
                pop     di
                pop     si
                iret
BIOS            endp


;----------------------------------------------------------------------------
;
; ReadPot-      On entry, DL contains a pot number to read.
;               Read and normalize that pot and return the result in AL.

                assume  ds:cseg
ReadPot         proc    near
;;;;;;;;;;      push    bx              ;Already on stack.
                push    ds
                push    cx
                push    dx
                push    si
                push    di
                push    bp

                mov     bx, cseg
                mov     ds, bx

                cmp     dl, 0
                jne     Try1
                mov     ah, Pot0.PotMask
                call    ReadPots
                lea     bx, Pot0
                mov     ax, si
                call    Normalize
                jmp     GotPot

Try1:           cmp     dl, 1
                jne     Try3
                mov     ah, Pot1.PotMask
                call    ReadPots
                lea     bx, Pot1
                mov     ax, bp
                call    Normalize
                jmp     GotPot

Try3:           cmp     dl, 3
                jne     BadPot
                mov     ah, Pot3.PotMask
                call    ReadPots
                lea     bx, Pot3
                mov     ax, di
                call    Normalize
                jmp     GotPot

BadPot:         sub     ax, ax          ;Question: Should we pass this on
                                        ; or just return zero?
GotPot:         pop     bp
                pop     di
                pop     si
                pop     dx
                pop     cx
                pop     ds
                pop     bx
                iret
ReadPot         endp
                assume  ds:nothing


;----------------------------------------------------------------------------
;
; ReadRaw-      On entry, DL contains a pot number to read.
;               Read that pot and return the unnormalized result in AL.

                assume  ds:cseg
ReadRaw         proc    near
;;;;;;;;;;      push    bx              ;Already on stack.
                push    ds
                push    cx
                push    dx
                push    si
                push    di
                push    bp

                mov     bx, cseg
                mov     ds, bx

                cmp     dl, 0
                jne     Try1
                mov     ah, Pot0.PotMask
                call    ReadPots
                mov     ax, si
                jmp     GotPot

Try1:           cmp     dl, 1
                jne     Try3
                mov     ah, Pot1.PotMask
                call    ReadPots
                mov     ax, bp
                jmp     GotPot

Try3:           cmp     dl, 3
                jne     BadPot
                mov     ah, Pot3.PotMask
                call    ReadPots
                mov     ax, di
                jmp     GotPot

BadPot:         sub     ax, ax          ;Just return zero.
GotPot:         pop     bp
                pop     di
                pop     si
                pop     dx
                pop     cx
                pop     ds
                pop     bx
                iret
ReadRaw         endp
                assume  ds:nothing


;----------------------------------------------------------------------------
; Read4Pots-    Reads pots zero, one, two, and three returning their
;               values in AL, AH, DL, and DH. Since the flightstick
;               Pro doesn't have a pot 2 installed, return zero for
;               that guy.

Read4Pots       proc    near
;;;;;;;;;;;     push    bx              ;Already on stack
                push    ds
                push    cx
                push    si
                push    di
                push    bp

                mov     dx, cseg
                mov     ds, dx

                mov     ah, 0bh         ;Read pots 0, 1, and 3.
                call    ReadPots

                mov     ax, si
                lea     bx, Pot0
                call    Normalize
                mov     cl, al

                mov     ax, bp
                lea     bx, Pot1
                call    Normalize
                mov     ch, al

                mov     ax, di
                lea     bx, Pot3
                call    Normalize
                mov     dh, al          ;Pot 3 value.
                mov     ax, cx          ;Pots 0 and 1.
                mov     dl, 0           ;Pot 2 is non-existant.

                pop     bp
                pop     di
                pop     si
                pop     cx
                pop     ds
                pop     bx
                iret
Read4Pots       endp




;----------------------------------------------------------------------------
; CalPot-       Calibrate the pot specified by DL. On entry, AL contains
;               the minimum pot value (it better be less than 256!), BX
;               contains the maximum pot value, and CX contains the centered
;               pot value.

                assume  ds:cseg
CalPot          proc    near
                pop     bx              ;Retrieve maximum value
                push    ds
                push    si
                mov     si, cseg
                mov     ds, si

; Sanity check on parameters, sort them in ascending order:

                mov     ah, 0
                cmp     bx, cx
                ja      GoodMax
                xchg    bx, cx
GoodMax:        cmp     ax, cx
                jb      GoodMin
                xchg    ax, cx
GoodMin:        cmp     cx, bx
                jb      GoodCenter
                xchg    cx, bx
GoodCenter:


; Okay, figure out who were supposed to calibrate:

                lea     si, Pot0
                cmp     dl, 1
                jb      DoCal
                lea     si, Pot1
                je      DoCal
                cmp     dl, 3
                jne     CalDone
                lea     si, Pot3

DoCal:          mov     [si].Pot.min, ax
                mov     [si].Pot.max, bx
                mov     [si].Pot.center, cx
                mov     [si].Pot.DidCal, 1
CalDone:        pop     si
                pop     ds
                iret
CalPot          endp
                assume  ds:nothing


;----------------------------------------------------------------------------
; TestCal-      Just checks to see if the pot specified by DL has already
;               been calibrated.

                assume  ds:cseg
TestCal         proc    near
;;;;;;;;        push    bx              ;Already on stack
                push    ds
                mov     bx, cseg
                mov     ds, bx

                sub     ax, ax          ;Assume no calibration
                lea     bx, Pot0
                cmp     dl, 1
                jb      GetCal
                lea     bx, Pot1
                je      GetCal
                cmp     dl, 3
                jne     BadCal
                lea     bx, Pot3

GetCal:         mov     al, [bx].Pot.DidCal
                mov     ah, 0
BadCal:         pop     ds
                pop     bx
                iret
TestCal         endp
                assume  ds:nothing


;----------------------------------------------------------------------------
;
; ReadSw-       Reads the switch whose switch number appears in DL.

SwTable         byte    11100000b, 11010000b, 01110000b, 10110000b
                byte    00000000b, 11000000b, 01000000b, 10000000b

SwTableL        byte    11100000b, 10110000b, 01110000b, 11010000b
                byte    00000000b, 11000000b, 01000000b, 10000000b

ReadSw          proc    near
;;;;;;;         push    bx              ;Already on stack
                mov     bl, dl          ;Save switch to read.
                mov     bh, 0
                mov     dx, JoyPort
                in      al, dx
                and     al, 0f0h
                cmp     cs:SwapButtons, 0
                je      DoLeft0
                cmp     al, cs:SwTableL[bx]
                jne     NotDown
                jmp     IsDown

DoLeft0:        cmp     al, cs:SwTable[bx]
                jne     NotDown

IsDown:         mov     ax, 1
                pop     bx
                iret

NotDown:        sub     ax, ax
                pop     bx
                iret
ReadSw          endp


;----------------------------------------------------------------------------
;
; Read16Sw-     Reads all eight switches and returns their values in AX.

Read16Sw        proc    near
;;;;;;;;        push    bx              ;Already on stack
                mov     ah, 0           ;Switches 8-15 are non-existant.
                mov     dx, JoyPort
                in      al, dx
                shr     al, 4
                mov     bl, al
                mov     bh, 0
                cmp     cs:SwapButtons, 0
                je      DoLeft1
                mov     al, cs:SwBitsL[bx]
                jmp     R8Done

DoLeft1:        mov     al, cs:SwBits[bx]
R8Done:         pop     bx
                iret
Read16Sw        endp


;****************************************************************************
;
; MyInt15-      Patch for the BIOS INT 15 routine to control reading the
;               joystick.

MyInt15         proc    far
                push    bx
                cmp     ah, 84h         ;Joystick code?
                je      DoJoystick
OtherInt15:     pop     bx
                jmp     cs:Int15Vect

DoJoystick:             mov     bh, 0
                mov     bl, dh
                cmp     bl, 80h
                jae     VendorCalls
                cmp     bx, JmpSize
                jae     OtherInt15
                shl     bx, 1
                jmp     wp cs:jmptable[bx]

jmptable        word    BIOS
                word    ReadPot, Read4Pots, CalPot, TestCal
                word    ReadRaw, OtherInt15, OtherInt15
                word    ReadSw, Read16Sw
JmpSize         =       ($-jmptable)/2


; Handle vendor specific calls here.

VendorCalls:    je      RemoveDriver
                cmp     bl, 81h
                je      TestPresence
                pop     bx
                jmp     cs:Int15Vect

; TestPresence- Returns zero in AX and a pointer to the ID string in ES:BX

TestPresence:   pop     bx              ;Get old value off stack.
                sub     ax, ax
                mov     bx, cseg
                mov     es, bx
                lea     bx, IDString
                iret

; RemoveDriver-If there are no other drivers loaded after this one in
;                memory, disconnect it and remove it from memory.

RemoveDriver:
                push    ds
                push    es
                push    ax
                push    dx

                mov     dx, cseg
                mov     ds, dx

; See if we're the last routine patched into INT 15h

                mov     ax, 3515h
                int     21h
                cmp     bx, offset MyInt15
                jne     CantRemove
                mov     bx, es
                cmp     bx, wp seg MyInt15
                jne     CantRemove

                mov     ax, PSP         ;Free the memory we're in
                mov     es, ax
                push    es
                mov     ax, es:[2ch]    ;First, free env block.
                mov     es, ax
                mov     ah, 49h
                int     21h
;
                pop     es              ;Now free program space.
                mov     ah, 49h
                int     21h

                lds     dx, Int15Vect   ;Restore previous int vect.
                mov     ax, 2515h
                int     21h

CantRemove:     pop     dx
                pop     ax
                pop     es
                pop     ds
                pop     bx
                iret
MyInt15         endp
cseg            ends



; The following segment is tossed when this code goes resident.

Initialize      segment para public 'INIT'
                assume  cs:Initialize, ds:cseg
Main            proc
                mov     ax, cseg        ;Get ptr to vars segment
                mov     es, ax
                mov     es:PSP, ds      ;Save PSP value away
                mov     ds, ax

                mov     ax, zzzzzzseg
                mov     es, ax
                mov     cx, 100h
                meminit2

                print
                byte    "Standard Game Device Interface driver",cr,lf
                byte    "CH Products Flightstick Pro",cr,lf
                byte    "Written by Randall Hyde",cr,lf
                byte    cr,lf
                byte    "'FSPSGDI LEFT' swaps the left and right buttons for "
                byte    "left handed players",cr,lf
                byte    "'FSPSGDI REMOVE' removes the driver from memory"
                byte    cr, lf, lf
                byte    0

                mov     ax, 1
                argv                    ;If no parameters, empty str.
                stricmpl
                byte    "LEFT",0
                jne     NoLEFT
                mov     SwapButtons, 1
                print
                byte    "Left and right buttons swapped",cr,lf,0
                jmp     SwappedLeft

NoLEFT:         stricmpl
                byte    "REMOVE",0
                jne     NoRmv
                mov     dh, 81h
                mov     ax, 84ffh
                int     15h             ;See if we're already loaded.
                test    ax, ax          ;Get a zero back?
                jz      Installed
                print
                byte    "SGDI driver is not present in memory, REMOVE "
                byte    "command ignored.",cr,lf,0
                mov     ax, 4c01h       ;Exit to DOS.
                int     21h

Installed:      mov     ax, 8400h
                mov     dh, 80h         ;Remove call
                int     15h
                mov     ax, 8400h
                mov     dh, 81h         ;TestPresence call
                int     15h
                cmp     ax, 0
                je      NotRemoved
                print
                byte    "Successfully removed SGDI driver from memory."
                byte    cr,lf,0
                mov     ax, 4c01h       ;Exit to DOS.
                int     21h

NotRemoved:     print
                byte    "SGDI driver is still present in memory.",cr,lf,0
                mov     ax, 4c01h       ;Exit to DOS.
                int     21h



NoRmv:


; Okay, Patch INT 15 and go TSR at this point.

SwappedLeft:    mov     ax, 3515h
                int     21h
                mov     wp Int15Vect, bx
                mov     wp Int15Vect+2, es

                mov     dx, cseg
                mov     ds, dx
                mov     dx, offset MyInt15
                mov     ax, 2515h
                int     21h

                mov     dx, cseg
                mov     ds, dx
                mov     dx, seg Initialize
                sub     dx, ds:psp
                add     dx, 2
                mov     ax, 3100h               ;Do TSR
                int     21h
Main            endp

Initialize      ends

sseg            segment para stack 'stack'
                word    128 dup (0)
endstk          word    ?
sseg            ends

zzzzzzseg       segment para public 'zzzzzzseg'
                byte    16 dup (0)
zzzzzzseg       ends
                end     Main

Chapter Twenty Four (Part 2)

Table of Content

Chapter Twenty Four (Part 4)

Chapter Twenty Four: The PC Game Adapter (Part 3)
29 SEP 1996