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. |