View difference between Paste ID: xv02GJF3 and eVGaVTRg
SHOW: | | - or go back to the newest paste.
1
{$G-,R-,S-,Q-}
2
{{$DEFINE QUANTIZETIMER}
3
{If defined, results from the timer method are adjusted by discarding the
4
least-significant bits.  The faster a machine you have, the less this is
5
necessary; if you have 12MHz or higher, don't use it at all.}
6
7
{
8
Unit to read joystick values from a db15 analog joystick.
9
20060225, trixter@oldskool.org.  Some information by Lou Duchez; assembler
10
implementations inspired by James P. McAdams and Bret Johnson
11
20090309: altered to use constants that make sense from TInterrupts;
12
adding quantization for timer-based method
13
20110304: Cleaned up code and made sure channel 0 was initialized to
14
mode 2 which provides consistent results.
15
20130501: Added digital directions and recalibration
16
17
Background:
18
19
The basic approach to reading a joystick is to monitor port 201h.  The eight
20
bits at that port correspond to:
21
22
01h - Joystick A, "X" position
23
02h - Joystick A, "Y" position
24
04h - Joystick B, "X" position
25
08h - Joystick B, "Y" position
26
10h - Joystick A, button 1
27
20h - Joystick A, button 2
28
40h - Joystick B, button 1
29
80h - Joystick B, button 2
30
31
The buttons are easy: a bit of "0" means "pressed" and "1" means "not
32
pressed".  But how do you get a variable X and Y axis from a single bit?!
33
Here's what you do:
34
35
1. Write a value -- any value -- to port 201h.  The four lowest bits will then
36
all assume a value of "1".
37
38
2. Start a counter, and see how many iterations it takes for your desired bit
39
to go to zero.  The number of iterations = the joystick position, with lower
40
values corresponding to "left" or "up" and higher values corresponding to
41
"right" or "down".
42
43
Now, what method to use to get the values?  You can do one of three things:
44
45
1. For maximum accuracy, LOOP with CX.  The CX register, coupled with LOOPNZ,
46
is the fastest method of doing this and results in the highest resolution.
47
Unfortunately, it requires interrupts to be turned off during measuring.
48
49
2. On a machine with a BIOS date after November 8th, 1982, call the
50
Int 15h,84h BIOS routine.  On a real PC/XT, this employs method #1 for
51
all 4 axis and buttons (other implementations may vary).
52
53
3. For compatibility across varying CPU speeds and lightest system footprint,
54
use the 8259 timer.  The returned values are constant across all PCs.
55
56
If you're writing a game, the system timer method is best.  If you're using
57
the joystick as an all-points-addressable device (such as a drawing
58
program), the CX/LOOPNZ method is best (assuming the speed of the machine is
59
not variable).
60
61
Misc. notes:
62
63
- There is a bug in the implementation of some joysticks/adapters in that,
64
after waiting for one axis to go to 0, you need to wait for the other one to
65
go to 0 as well.  If you don't, and you fire the one-shots to read the stick a
66
second time before the other axis bit has settled, your results are
67
unpredictable (ie. wrong, stuck, always clear, etc.).
68
69
- You may be tempted to write a single routine that monitors all four bits and
70
spits out all four values, does the waiting, etc. -- but since that takes
71
time, on a slow machine all that processing can actually affect
72
the results!  I've personally measured this on a 4.77MHz 8088; extended
73
processing of the bits takes so long that the numbers returned by the
74
timer-based routine start to become very coarse.  This unit intentionally
75
treats each axis in a seperate block of code to ensure the least amount of
76
processing per axis, resulting in the most granular results possible.
77
78
- Jordan Knight informed me that you can't write random data to the one-shots,
79
you have to write all 1's or very early adapters won't work.
80
81
- If you want to use the BIOS, your machine must be made after November
82
8th, 1982, because that's when IBM started putting the code in there.
83
Here is how:
84
85
                     INT 15,84 - Joy-Stick Support
86
87
        AH = 84h
88
        DX = 0 to read the current switch settings
89
           = 1 to read the joystick position (resistive inputs)
90
91
        on return (DX=0, read switch setting):
92
        CF = 0 if successful
93
           = 1 if error
94
        AH = 80h error code if CF set on PC
95
           = 86h error code if CF set on XT before 11/8/82
96
        AL = switch settings in bits 7-4 (if read switch function)
97
98
        on return (DX=1, read joystick position):
99
        AX = A(X)
100
        BX = A(Y)
101
        CX = B(X)
102
        DX = B(Y)
103
}
104
105
unit joystick;
106
107
interface
108
109
type
110
  polltype=(p_timer,p_loop,usebios);
111
  joyCoordType=record
112
    x,y:word;
113
  end;
114
115
const
116
  JoyPortAddr:word=$201;
117
  {bits for joystick_pos and buttons}
118
  jax=$01; {joystick a, x axis}
119
  jay=$02; {joystick a, y axis}
120
  jbx=$04; {joystick b, x axis}
121
  jby=$08; {joystick b, y axis}
122
  ja1=$10; {joystick a, button 1}
123
  ja2=$20; {joystick a, button 2}
124
  jb1=$40; {joystick b, button 1}
125
  jb2=$80; {joystick b, button 2}
126
  {unit variables}
127
  GotA:boolean=false;
128
  GotB:boolean=false;
129
  centerA:joyCoordType=(x:0;y:0);
130
  centerB:joyCoordType=(x:0;y:0);
131
  {digital directions:}
132
  jau=$01; {up}
133
  jad=$02; {down}
134
  jal=$04; {left}
135
  jar=$08; {right}
136
  jbu=$10;
137
  jbd=$20;
138
  jbl=$40;
139
  jbr=$80;
140
141
  pollmethod:polltype=p_timer; {This *must* be p_timer for this units's
142
  start-up code to work on all machines.}
143
144
function joystick_position(which_bit:byte):word;
145
function joystick_button(which_bit:byte):boolean;
146
function joystick_buttons:byte;
147
function joystick_direction:byte;
148
procedure joystick_recenter;
149
150
implementation
151
152
uses
153
  TInterrupts;
154
155
const
156
  J_maxtimer=9999; {Maximum number of 0.8381 usec ticks}
157
  j_maxpolls=$4ff; {Maximum number of times we're willing to loop polling the
158
  joystick before we give up (cpu too fast, joystick not working, etc.)
159
  This number was derived from the IBM PC/XT BIOS disassembly; it limits the
160
  range from 0-1279.  The maximum number obtained from a 1.8 GHz Athlon was
161
  less than 1100 so it seems this number is okay.  In fact, since BIOS code
162
  runs faster than code in main memory, it is especially safe for us to use
163
  this number.}
164
165
function joystick_button;assembler;
166
{readport high-level:  result:=(port[$201] and mask)=0}
167
asm
168
  mov    bl,pollmethod
169
  cmp    bl,usebios     {compare; BIOS method requested?}
170
  jne    @readport      {no?  then jmp to port method, otherwise fall through}
171
172
@askbios:
173
  mov     ax,8400h       {AH=function call; set AL=0 for later}
174
  xor     dx,dx          {0 = read switch settings}
175
  int     15h
176
  jc      @buttonexit    {carry set?  if so, something went wrong}
177
  jmp     @returnbutton  {if not, process our bits}
178
179
@readport:
180
  mov     dx,JoyPortADDR {PorT ADDR of JOYSTICKS}
181
  mov     ah,which_bit   {MASK For DESIRED 1-SHOT}
182
  in      al,dx          {get joystick port bits}
183
184
@returnbutton:
185
  not     al             {invert so that 1=pressed, 0=not pressed}
186
  and     al,which_bit   {mask off our bit... al is our return}
187
@buttonexit:
188
end;
189
190
function joystick_buttons;assembler;
191
{readport high-level:  result:=(port[$201] xor $FF) and $F0}
192
asm
193
  mov     dx,JoyPortADDR {port addr of joysticks}
194
  in      al,dx          {get joystick port bits}
195
  not     al             {invert so that 1=pressed, 0=not pressed}
196
  and     al,$f0         {mask off axis bits, leaving button bits}
197
end;
198
199
function joystick_position;assembler;
200
{
201
Returns:
202
  ax=number of timer ticks or software loops as joystick position value.
203
}
204
asm
205
  jmp    @start
206
@masks:
207
  {masks that match either stick A or B based on which_bit}
208
         {0000 0001 0010 0011 0100 0101 0110 0111 1000}
209
  db         0,   3,   3,   0,  12,   0,   0,   0,  12
210
@start:
211
  mov    dx,JoyPortADDR {used in both methods' inner loops}
212
  mov    bl,pollmethod
213
  cmp    bl,p_loop      {compare; loop method requested?}
214
  je     @loop_method
215
  cmp    bl,usebios     {BIOS method requested?}
216
  je     @bios_method
217
  {otherwise, fall through to timer method}
218
219
@timer_method:
220
  mov    bl,which_bit   {mask for desired bit}
221
                        {Channel 0, Latch Counter, Rate Generator, Binary}
222
  mov    bh,iMC_Chan0+iMC_LatchCounter+iMC_OpMode2+iMC_BinaryMode
223
  mov    cx,j_maxtimer  {maximum compare value for inner loop below}
224
  mov    al,bh          {Begin building timer count}
225
  mov    di,$FFFF       {value to init the one-shots with}
226
  pushf                 {Save interrupt state}
227
  cli                   {Disable interrupts so our operation is atomic}
228
  out    43h,al         {Tell timer about it}
229
  in     al,40h         {Get LSB of timer counter}
230
  xchg   al,ah          {Save it in ah (xchg accum,reg is 3c 1b}
231
  in     al,40h         {Get MSB of timer counter}
232
  popf                  {Restore interrupt state}
233
  xchg   al,ah          {Put things in the right order; AX:=starting timer}
234
  xchg   di,ax          {load AX with 1's, while storing AX into DI for further comparison}
235
  out    dx,al          {write all 1's to start the one-shots}
236
@read:
237
  mov    al,bh          {Use same Mode/Command as before (latch counter, etc.)}
238
  pushf                 {Save interrupt state}
239
  cli                   {Disable interrupts so our operation is atomic}
240
  out    43h,AL         {Tell timer about it}
241
  in     al,40h         {Get LSB of timer counter}
242
  xchg   al,ah          {Save it in ah for a second}
243
  in     al,40h         {Get MSB of timer counter}
244
  popf                  {Restore interrupt state}
245
  xchg   al,ah          {AX:=new timer value}
246
  mov    si,di          {copy original value to scratch}
247
  sub    si,ax          {subtract new value from old value}
248
  cmp    si,cx          {compare si to maximum time allowed}
249
  ja     @nostick       {if above, then we've waited too long -- blow doors}
250
  in     al,dx          {if we're still under the limit, read all eight bits}
251
  test   al,bl          {check axis bit we care about}
252
  jnz    @read          {loop while the bit tested isn't zero yet}
253
  {$IFDEF QUANTIZETIMER}{see top of file for explanation}
254
  shr    si,1
255
  shr    si,1
256
  shr    si,1
257
  {$ENDIF}
258
  jmp    @joy_exit      {si holds number of timer ticks gone by}
259
260
@loop_method:
261
  mov    cx,j_maxpolls  {number of ticks to count down}
262
  mov    si,cx          {used later}
263
  mov    ah,which_bit   {mask for desired bit}
264
  mov    al,$FF         {set up al for all 1's to write to one-shots}
265
  pushf                 {save interrupt flag state, in case we are being called from an interrupt-driven procedure ourselves}
266
  cli                   {turn off interrupts so our timing loop isn't affected}
267
  out    dx,al          {write all 1's to start the one-shots}
268
269
@readit:
270
  in     al,dx          {read all eight bits}
271
  test   al,ah          {check desired bit}
272
  loopnz @readit        {loop while the bit tested isn't zero}
273
  popf                  {turn interrupts back on}
274
  jcxz   @nostick       {if cx is 0 then we timed out and should abort}
275
  sub    si,cx          {si:=j_maxpolls - ticks counted}
276
  jmp    @joy_exit
277
278
@bios_method:
279
  mov    ax,8400h       {joystick function}
280
  mov    dx,1           {read sticks}
281
  xor    si,si          {assume the worst}
282
  int    15h            {do it}
283
  jc     @returnticks   {error?  si already 0, jmp to done}
284
  {process int15 -- this isn't efficient if you ONLY want to use BIOS method}
285
  mov    si,ax          {assume we wanted jax}
286
  test   which_bit,jax  {were we right?}
287
  jnz    @returnticks   {jump if desired AND jax <> 0}
288
  mov    si,bx          {assume jay}
289
  test   which_bit,jay
290
  jnz    @returnticks
291
  mov    si,cx          {assume jbx}
292
  test   which_bit,jbx
293
  jnz    @returnticks
294
  mov    si,dx          {oh well, must have been jby}
295
  jmp    @returnticks   {don't need to wait for bits to settle if using BIOS routine}
296
297
@nostick:
298
  xor    si,si          {no stick?  return 0}
299
300
@joy_exit:
301
  (*mov    ah,00001111b   {mask all four axis bits --
302
  SLOW if other stick not present!  So we only pick mask we need:}*)
303
  lea    bx,@masks      {point BX to our mask table}
304
  mov    al,which_bit
305
  segcs  xlat           {load proper mask (A or B) for our stick}
306
  mov    ah,al          {(ie. mask for only joystick we care about)}
307
  mov    cx,j_maxpolls
308
@clearstick:            {This took me days to find.  You have to wait}
309
  in     al,dx          {for all stick AXIS bits to go to 0 before you can}
310
  test   al,ah          {reliably read stick again!  See Misc. notes above}
311
  loopnz @clearstick
312
313
@returnticks:
314
  mov    ax,si          {return # of ticks as the value}
315
end;
316
317
function joystick_direction:byte;
318
{
319
Idea is to present user with a byte that contains bits showing if the stick
320
is pointing up, down, left, or right -- like an atari joystick.  This routine
321
assumes a 50% deadzone to cut down on the user involvement.
322
}
323
var
324
  b:byte;
325
  w:word;
326
  newreada,newreadb:joyCoordType;
327
begin
328
  b:=0;
329
  if gota then begin
330
    newreada.x:=joystick_position(jax);
331
    w:=centera.x shr 1;
332
    if newreada.x < centera.x - w then b:=b OR jal;
333
    if newreada.x > centera.x + w then b:=b OR jar;
334
    newreada.y:=joystick_position(jay);
335
    w:=centera.y shr 1;
336
    if newreada.y < centera.y - w then b:=b OR jau;
337
    if newreada.y > centera.y + w then b:=b OR jad;
338
  end;
339
  if gotb then begin
340
    newreadb.x:=joystick_position(jbx);
341
    w:=centerb.x shr 1;
342
    if newreadb.x < centerb.x - w then b:=b OR jbl;
343
    if newreadb.x > centerb.x + w then b:=b OR jbr;
344
    newreadb.y:=joystick_position(jby);
345
    w:=centerb.y shr 1;
346
    if newreadb.y < centerb.y - w then b:=b OR jbu;
347
    if newreadb.y > centerb.y + w then b:=b OR jbd;
348
  end;
349
  joystick_direction:=b;
350
end;
351
352
Procedure joystick_recenter;
353
{recalibrates digital joystick directions}
354
355
const
356
  numavgs=5; {number of samples to gather before calcing center results}
357
358
var
359
  avgw:word; {used for averaging initial center stick results}
360
  avgb:byte;
361
362
begin
363
  {Set initial center position (and hope user has calibrated stick)
364
  for use with the "direction" function.}
365
  if gota then begin
366
    avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jax); centera.x:=avgw div numavgs;
367
    avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jay); centera.y:=avgw div numavgs;
368
  end;
369
  if gotb then begin
370
    avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jbx); centerb.x:=avgw div numavgs;
371
    avgw:=0; for avgb:=0 to numavgs-1 do avgw:=avgw+joystick_position(jby); centerb.y:=avgw div numavgs;
372
  end;
373
end;
374
375
begin
376
  {init channel 0, 3=access mode lobyte/hibyte, mode 2, 16-bit binary}
377
  {We do this so we can get a sensible countdown value from mode 2 instead
378
  of the 2xspeedup var from mode 3.  I have no idea why old BIOSes init
379
  mode 3; everything 486 and later inits mode 2.  Go figure.  This should
380
  not damage anything in DOS or TSRs, in case you were wondering.}
381
  InitChannel(0,3,2,$0000);
382
383
  {Set "GotA and "GotB" booleans by checking for "0" from the X axis
384
  of both sticks.  This is "safe" because it uses the most compatible
385
  method, timer-based polling.}
386
  GotA:=(joystick_position(jax)<>0);
387
  GotB:=(joystick_position(jbx)<>0);
388
  joystick_recenter;
389
end.