Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- {$G-,R-,S-,Q-}
- {{$DEFINE QUANTIZETIMER}
- {If defined, results from the timer method are adjusted by discarding the
- least-significant bits. The faster a machine you have, the less this is
- necessary; if you have 12MHz or higher, don't use it at all.}
- {
- Unit to read joystick values from a db15 analog joystick.
- 20060225, trixter@oldskool.org. Some information by Lou Duchez; assembler
- implementations inspired by James P. McAdams and Bret Johnson
- 20090309: altered to use constants that make sense from TInterrupts;
- adding quantization for timer-based method
- 20110304: Cleaned up code and made sure channel 0 was initialized to
- mode 2 which provides consistent results.
- 20130501: Added digital directions and recalibration
- Background:
- The basic approach to reading a joystick is to monitor port 201h. The eight
- bits at that port correspond to:
- 01h - Joystick A, "X" position
- 02h - Joystick A, "Y" position
- 04h - Joystick B, "X" position
- 08h - Joystick B, "Y" position
- 10h - Joystick A, button 1
- 20h - Joystick A, button 2
- 40h - Joystick B, button 1
- 80h - Joystick B, button 2
- The buttons are easy: a bit of "0" means "pressed" and "1" means "not
- pressed". But how do you get a variable X and Y axis from a single bit?!
- Here's what you do:
- 1. Write a value -- any value -- to port 201h. The four lowest bits will then
- all assume a value of "1".
- 2. Start a counter, and see how many iterations it takes for your desired bit
- to go to zero. The number of iterations = the joystick position, with lower
- values corresponding to "left" or "up" and higher values corresponding to
- "right" or "down".
- Now, what method to use to get the values? You can do one of three things:
- 1. For maximum accuracy, LOOP with CX. The CX register, coupled with LOOPNZ,
- is the fastest method of doing this and results in the highest resolution.
- Unfortunately, it requires interrupts to be turned off during measuring.
- 2. On a machine with a BIOS date after November 8th, 1982, call the
- Int 15h,84h BIOS routine. On a real PC/XT, this employs method #1 for
- all 4 axis and buttons (other implementations may vary).
- 3. For compatibility across varying CPU speeds and lightest system footprint,
- use the 8259 timer. The returned values are constant across all PCs.
- If you're writing a game, the system timer method is best. If you're using
- the joystick as an all-points-addressable device (such as a drawing
- program), the CX/LOOPNZ method is best (assuming the speed of the machine is
- not variable).
- Misc. notes:
- - There is a bug in the implementation of some joysticks/adapters in that,
- after waiting for one axis to go to 0, you need to wait for the other one to
- go to 0 as well. If you don't, and you fire the one-shots to read the stick a
- second time before the other axis bit has settled, your results are
- unpredictable (ie. wrong, stuck, always clear, etc.).
- - You may be tempted to write a single routine that monitors all four bits and
- spits out all four values, does the waiting, etc. -- but since that takes
- time, on a slow machine all that processing can actually affect
- the results! I've personally measured this on a 4.77MHz 8088; extended
- processing of the bits takes so long that the numbers returned by the
- timer-based routine start to become very coarse. This unit intentionally
- treats each axis in a seperate block of code to ensure the least amount of
- processing per axis, resulting in the most granular results possible.
- - Jordan Knight informed me that you can't write random data to the one-shots,
- you have to write all 1's or very early adapters won't work.
- - If you want to use the BIOS, your machine must be made after November
- 8th, 1982, because that's when IBM started putting the code in there.
- Here is how:
- INT 15,84 - Joy-Stick Support
- AH = 84h
- DX = 0 to read the current switch settings
- = 1 to read the joystick position (resistive inputs)
- on return (DX=0, read switch setting):
- CF = 0 if successful
- = 1 if error
- AH = 80h error code if CF set on PC
- = 86h error code if CF set on XT before 11/8/82
- AL = switch settings in bits 7-4 (if read switch function)
- on return (DX=1, read joystick position):
- AX = A(X)
- BX = A(Y)
- CX = B(X)
- DX = B(Y)
- }
- unit joystick;
- interface
- type
- polltype=(p_timer,p_loop,usebios);
- joyCoordType=record
- x,y:word;
- end;
- const
- JoyPortAddr:word=$201;
- {bits for joystick_pos and buttons}
- jax=$01; {joystick a, x axis}
- jay=$02; {joystick a, y axis}
- jbx=$04; {joystick b, x axis}
- jby=$08; {joystick b, y axis}
- ja1=$10; {joystick a, button 1}
- ja2=$20; {joystick a, button 2}
- jb1=$40; {joystick b, button 1}
- jb2=$80; {joystick b, button 2}
- {unit variables}
- GotA:boolean=false;
- GotB:boolean=false;
- centerA:joyCoordType=(x:0;y:0);
- centerB:joyCoordType=(x:0;y:0);
- {digital directions:}
- jau=$01; {up}
- jad=$02; {down}
- jal=$04; {left}
- jar=$08; {right}
- jbu=$10;
- jbd=$20;
- jbl=$40;
- jbr=$80;
- pollmethod:polltype=p_timer; {This *must* be p_timer for this units's
- start-up code to work on all machines.}
- function joystick_position(which_bit:byte):word;
- function joystick_button(which_bit:byte):boolean;
- function joystick_buttons:byte;
- function joystick_direction:byte;
- procedure joystick_recenter;
- implementation
- uses
- TInterrupts;
- const
- J_maxtimer=9999; {Maximum number of 0.8381 usec ticks}
- j_maxpolls=$4ff; {Maximum number of times we're willing to loop polling the
- joystick before we give up (cpu too fast, joystick not working, etc.)
- This number was derived from the IBM PC/XT BIOS disassembly; it limits the
- range from 0-1279. The maximum number obtained from a 1.8 GHz Athlon was
- less than 1100 so it seems this number is okay. In fact, since BIOS code
- runs faster than code in main memory, it is especially safe for us to use
- this number.}
- function joystick_button;assembler;
- {readport high-level: result:=(port[$201] and mask)=0}
- asm
- mov bl,pollmethod
- cmp bl,usebios {compare; BIOS method requested?}
- jne @readport {no? then jmp to port method, otherwise fall through}
- @askbios:
- mov ax,8400h {AH=function call; set AL=0 for later}
- xor dx,dx {0 = read switch settings}
- int 15h
- jc @buttonexit {carry set? if so, something went wrong}
- jmp @returnbutton {if not, process our bits}
- @readport:
- mov dx,JoyPortADDR {PorT ADDR of JOYSTICKS}
- mov ah,which_bit {MASK For DESIRED 1-SHOT}
- in al,dx {get joystick port bits}
- @returnbutton:
- not al {invert so that 1=pressed, 0=not pressed}
- and al,which_bit {mask off our bit... al is our return}
- @buttonexit:
- end;
- function joystick_buttons;assembler;
- {readport high-level: result:=(port[$201] xor $FF) and $F0}
- asm
- mov dx,JoyPortADDR {port addr of joysticks}
- in al,dx {get joystick port bits}
- not al {invert so that 1=pressed, 0=not pressed}
- and al,$f0 {mask off axis bits, leaving button bits}
- end;
- function joystick_position;assembler;
- {
- Returns:
- ax=number of timer ticks or software loops as joystick position value.
- }
- asm
- jmp @start
- @masks:
- {masks that match either stick A or B based on which_bit}
- {0000 0001 0010 0011 0100 0101 0110 0111 1000}
- db 0, 3, 3, 0, 12, 0, 0, 0, 12
- @start:
- mov dx,JoyPortADDR {used in both methods' inner loops}
- mov bl,pollmethod
- cmp bl,p_loop {compare; loop method requested?}
- je @loop_method
- cmp bl,usebios {BIOS method requested?}
- je @bios_method
- {otherwise, fall through to timer method}
- @timer_method:
- mov bl,which_bit {mask for desired bit}
- {Channel 0, Latch Counter, Rate Generator, Binary}
- mov bh,iMC_Chan0+iMC_LatchCounter+iMC_OpMode2+iMC_BinaryMode
- mov cx,j_maxtimer {maximum compare value for inner loop below}
- mov al,bh {Begin building timer count}
- mov di,$FFFF {value to init the one-shots with}
- pushf {Save interrupt state}
- cli {Disable interrupts so our operation is atomic}
- out 43h,al {Tell timer about it}
- in al,40h {Get LSB of timer counter}
- xchg al,ah {Save it in ah (xchg accum,reg is 3c 1b}
- in al,40h {Get MSB of timer counter}
- popf {Restore interrupt state}
- xchg al,ah {Put things in the right order; AX:=starting timer}
- xchg di,ax {load AX with 1's, while storing AX into DI for further comparison}
- out dx,al {write all 1's to start the one-shots}
- @read:
- mov al,bh {Use same Mode/Command as before (latch counter, etc.)}
- pushf {Save interrupt state}
- cli {Disable interrupts so our operation is atomic}
- out 43h,AL {Tell timer about it}
- in al,40h {Get LSB of timer counter}
- xchg al,ah {Save it in ah for a second}
- in al,40h {Get MSB of timer counter}
- popf {Restore interrupt state}
- xchg al,ah {AX:=new timer value}
- mov si,di {copy original value to scratch}
- sub si,ax {subtract new value from old value}
- cmp si,cx {compare si to maximum time allowed}
- ja @nostick {if above, then we've waited too long -- blow doors}
- in al,dx {if we're still under the limit, read all eight bits}
- test al,bl {check axis bit we care about}
- jnz @read {loop while the bit tested isn't zero yet}
- {$IFDEF QUANTIZETIMER}{see top of file for explanation}
- shr si,1
- shr si,1
- shr si,1
- {$ENDIF}
- jmp @joy_exit {si holds number of timer ticks gone by}
- @loop_method:
- mov cx,j_maxpolls {number of ticks to count down}
- mov si,cx {used later}
- mov ah,which_bit {mask for desired bit}
- mov al,$FF {set up al for all 1's to write to one-shots}
- pushf {save interrupt flag state, in case we are being called from an interrupt-driven procedure ourselves}
- cli {turn off interrupts so our timing loop isn't affected}
- out dx,al {write all 1's to start the one-shots}
- @readit:
- in al,dx {read all eight bits}
- test al,ah {check desired bit}
- loopnz @readit {loop while the bit tested isn't zero}
- popf {turn interrupts back on}
- jcxz @nostick {if cx is 0 then we timed out and should abort}
- sub si,cx {si:=j_maxpolls - ticks counted}
- jmp @joy_exit
- @bios_method:
- mov ax,8400h {joystick function}
- mov dx,1 {read sticks}
- xor si,si {assume the worst}
- int 15h {do it}
- jc @returnticks {error? si already 0, jmp to done}
- {process int15 -- this isn't efficient if you ONLY want to use BIOS method}
- mov si,ax {assume we wanted jax}
- test which_bit,jax {were we right?}
- jnz @returnticks {jump if desired AND jax <> 0}
- mov si,bx {assume jay}
- test which_bit,jay
- jnz @returnticks
- mov si,cx {assume jbx}
- test which_bit,jbx
- jnz @returnticks
- mov si,dx {oh well, must have been jby}
- jmp @returnticks {don't need to wait for bits to settle if using BIOS routine}
- @nostick:
- xor si,si {no stick? return 0}
- @joy_exit:
- (*mov ah,00001111b {mask all four axis bits --
- SLOW if other stick not present! So we only pick mask we need:}*)
- lea bx,@masks {point BX to our mask table}
- mov al,which_bit
- segcs xlat {load proper mask (A or B) for our stick}
- mov ah,al {(ie. mask for only joystick we care about)}
- mov cx,j_maxpolls
- @clearstick: {This took me days to find. You have to wait}
- in al,dx {for all stick AXIS bits to go to 0 before you can}
- test al,ah {reliably read stick again! See Misc. notes above}
- loopnz @clearstick
- @returnticks:
- mov ax,si {return # of ticks as the value}
- end;
- function joystick_direction:byte;
- {
- Idea is to present user with a byte that contains bits showing if the stick
- is pointing up, down, left, or right -- like an atari joystick. This routine
- assumes a 50% deadzone to cut down on the user involvement.
- }
- var
- b:byte;
- w:word;
- newreada,newreadb:joyCoordType;
- begin
- b:=0;
- if gota then begin
- newreada.x:=joystick_position(jax);
- w:=centera.x shr 1;
- if newreada.x < centera.x - w then b:=b OR jal;
- if newreada.x > centera.x + w then b:=b OR jar;
- newreada.y:=joystick_position(jay);
- w:=centera.y shr 1;
- if newreada.y < centera.y - w then b:=b OR jau;
- if newreada.y > centera.y + w then b:=b OR jad;
- end;
- if gotb then begin
- newreadb.x:=joystick_position(jbx);
- w:=centerb.x shr 1;
- if newreadb.x < centerb.x - w then b:=b OR jbl;
- if newreadb.x > centerb.x + w then b:=b OR jbr;
- newreadb.y:=joystick_position(jby);
- w:=centerb.y shr 1;
- if newreadb.y < centerb.y - w then b:=b OR jbu;
- if newreadb.y > centerb.y + w then b:=b OR jbd;
- end;
- joystick_direction:=b;
- end;
- Procedure joystick_recenter;
- {recalibrates digital joystick directions}
- const
- numavgs=5; {number of samples to gather before calcing center results}
- var
- avgw:word; {used for averaging initial center stick results}
- avgb:byte;
- begin
- {Set initial center position (and hope user has calibrated stick)
- for use with the "direction" function.}
- if gota then begin
- avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jax); centera.x:=avgw div numavgs;
- avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jay); centera.y:=avgw div numavgs;
- end;
- if gotb then begin
- avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jbx); centerb.x:=avgw div numavgs;
- avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jby); centerb.y:=avgw div numavgs;
- end;
- end;
- begin
- {init channel 0, 3=access mode lobyte/hibyte, mode 2, 16-bit binary}
- {We do this so we can get a sensible countdown value from mode 2 instead
- of the 2xspeedup var from mode 3. I have no idea why old BIOSes init
- mode 3; everything 486 and later inits mode 2. Go figure. This should
- not damage anything in DOS or TSRs, in case you were wondering.}
- InitChannel(0,3,2,$0000);
- {Set "GotA and "GotB" booleans by checking for "0" from the X axis
- of both sticks. This is "safe" because it uses the most compatible
- method, timer-based polling.}
- GotA:=(joystick_position(jax)<>0);
- GotB:=(joystick_position(jbx)<>0);
- joystick_recenter;
- end.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement