Der PC-Game-Port

Prof. Jürgen Plate

Der PC-Game-Port

Der Game-Port wird über die Adressen 200 bis 207 angesprochen. Der Spiele-Adapter besitzt acht Eingangsleitungen: Vier für digitale Signale und vier für Wiederstandsmessung. Die vier Digitaleingänge sind intern über einen Widerstand von 1 KOhm an + 5V gelegt. Das Signal liegt also im Ruhezustand auf logisch 1. Diese Eingänge (S1 bis S4) können über Schalter oder Taster an Masse gelegt werden (Pins 4 und 12 des Steckers). Die Bits 4 bis 7 des Game-Ports sind den Schaltern zugeordnet. Ist ein Schalter geschlossen, liegt das entsprechende Bit auf 0.

Die Daten vom Game-Port werden auf Portadresse 201 (Hex) gelesen. Die Bits 0 bis 3 entsprechen den Widerständen R1 bis R4. Die Wiederstandseingänge wandeln einen Widerstand in einen digitalen Impuls um, dessen Länge dem angeschlossenen Widerstand entspricht: Zeit = 24,2 + 0,011*Widerstand [µs] Für den Beginn der Wandlung muß ein beliebiger Wert auf Port 201 ausgegeben werden. Die Zeitbasis wird damit zurückgesetzt. Die Bits 0 bis 3 gehen nun alle auf 1. Nach Ablauf der durch den Widerstand (Potentiometerwerte von 100 KOhm bis 250 KOhm kommen vor, 100 KOhm sind die Regel) bestimmten Zeit, wechselt das entsprechende Bit wieder auf 0. Die Zeitmessung geschieht durch kontinuierliches Lesen des Ports 201 und das inkrementieren eines Zählers. Sobald das interessierende Bit wieder auf 0 geht, wird der Zählerstand gespeichert. Man muß auch die Zählschleife zusätzlich bei erreichen eines maximalen Zählerstandes abbrechen - für den Fall, daß der Wiederstand nicht angeschlossen oder defekt ist. Um den Zählerstand nicht zu verfälschen, müßten eigentlich während der Messung die Interrupts abgeschaltet werden - bei einem Multitasking-Betriebssystem aber nicht zu empfehlen.

Man kann den Game-Port aber nicht nur für Spiele, sondern auch für einfache Meßaufgaben verwenden (z. B. mit licht- oder temperaturabhängigen Widerständen). Seine interne Schaltung besteht lediglich aus einem Vierfach-Monoflop 558, das vier mal den altbekannten Timer 555 enthält:

Der Ablauf der Widertandsmessung ist recht einfach:

  1. Im Ruhezustand ist der zeitbestimmende Kondensator (22 nF) geladen und das Monoflop liefert eine logische 1 am Ausgang.
  2. Wird auf Adresse 201H geschrieben, werden alle 4 Monoflops getriggert und der Kondensator entladen. Die Ausgänge der Monoflops gehen auf 0.
  3. Nun lädt sich der Kondensator über das Potentiometer des Joysticks wieder auf.
  4. Sobald die Spannung am Kondensator die Schaltschwelle des Monoflops erreicht hat, kippt dieses und der Ausgang geht wieder auf 1.

Je höher der Widerstand des Potentiometers ist, desto länger dauert es, bis das Monoflop wieder auf 1 wechselt. Nachdem die Ladekurve eines Kondensators nicht linear ist und auch Bauteiletoleranzen, Temperatur, usw. mit hineinspielen, ist die Messmethode recht ungenau. Auch variieren die Werte von Joystick zu Joystick, so daß immer eine Kalibirierung des Systems nötig ist.

Beachten Sie auch, daß am Stecker des Game-Ports sowohl die Masseleitung (Pins 4, 5, 12) als auch die Versorgungsspannung +5 V (Pins 1, 8, 9, 15) direkt anliegt. Ein Kurzschluß zwischen Masse und +5 V kann eventuell zu einem Defekt im Netzteil führen!

Etlich Game-Ports, insbesondere jene aus Soundkarten, haben auch noch den Midiport auf die 15polige Buchse geführt, auf Pin 12 das Signal "midi data output" und auf Pin 15 "midi data input". Damit ergibt sich folgendes Pinout:

pin     purpose
1       potentiometer common            (Joy A)
2       button 1                        (Joy A)
3       X coordinate potentiometer      (Joy A)
4       button common                   (Joy A)
5       button common                   (Joy B)
6       Y coordinate potentiometer      (Joy A)
7       button 2                        (Joy A)
8       unused
9       potentiometer common            (Joy B)
10      button 1                        (Joy B)
11      X coordinate potentiometer      (Joy B)
12      MIDI TXD (transmit)             (computer -> midi)
13      Y coordinate potentiometer      (Joy B)
14      button 2                        (Joy B)
15      MIDI RXD                        (midi -> computer)

Die Belegung des Datenwortes auf Adresse 201H entspricht der folgenden Tabelle:

         _______________________________________________________
        |   7  |   6  |   5  |   4  |   3  |   2  |   1  |   0  |
        |  S4  |  S3  |  S2  |  S1  |  R4  |  R3  |  R2  |  R1  |
        |______|______|______|______|______|______|______|______|
Doe vier höhstwertigen Bits geben die Tastenstellung wieder, die vier niederwertigen Bits den Ablauf der Monoflops:
7 6 5 4 3 2 1 0  Bitzuordnung
* . . . . . . .  Taste S4 (pin 14), 0=closed, 1=open (default)
. * . . . . . .  Taste S3 (pin 10), 0=closed, 1=open (default)
. . * . . . . .  Taste S2 (pin 7), 0=closed, 1=open (default)
. . . * . . . .  Taste S1 (pin 2), 0=closed, 1=open (default)
. . . . * . . .  Monoflop R4 (pin 13), 1=timing, 0=timed-out
. . . . . * . .  Monoflop R3 (pin 11), 1=timing, 0=timed-out
. . . . . . * .  Monoflop R2 (pin 6), 1=timing, 0=timed-out
. . . . . . . *  Monoflop R1 (pin 3), 1=timing, 0=timed-out
Recht einfach können die Tasteneingänge genutzt werden, den dazu ist nur der Datenwert von Adresse 201H zu lesen:
#include <stdio.h> 
#include <unistd.h> 
#include <sys/io.h> 

int main() 
  { 
  int i = 0; 
  /* Get access to the port */ 
  if (ioperm(0x201, 1, 1)) 
    { perror("ioperm"); exit(1); } 
  while (1) /* forever do */ 
    { 
    i = inb(0x201);
    /* shift to lower nibble */
    i = i >> 4; 
    printf("Button Status %X\n",i);
    /* Sleep for a while */ 
    sleep(1); 
    } 
  /* We don't need the ports anymore */ 
  if (ioperm(0x201, 1, 0)) 
    { perror("ioperm"); exit(1); } 
  exit(0); 
  }
Quellprogramm jport.c.

Das Programm muß mit Optimierungsstufe -O oder -O2 übersetzt werden, da inb() als Inline Macro definiert ist und sonst nicht substituiert wird. Siehe auch den Abschnitt Programmierung im Parallelport-Kapitel.

Um die XY-Position zu lesen, müsste man eigentlich alle Interrupts abschalten und dann zusammen mit dem Monoflop des Gameports einen Zähler starten, der erst wieder gestoppt wird, wenn das Monoflop in den stabilen Zustand zurückfällt. Bei einem Multitasking-Betriebssystem geht das natürlich nicht! Man kann aber als Ersatz für den Zähler den "time stamp counter" des Linux-Kernels auswerten. Dies ist ein sehr schnell inkrementierender 64-Bit-Zähler. Auf den "time stamp counter" (TSC) kann über ein Macro namens "rdtsc" zugegriffen werden. Die Definition des Makros befindet sich im Headerfile asm/msr.h. Damit kann der Joystick nach folgendem Schema ausgelesen werden:

#include <asm/io.h>
#include <asm/msr.h>

#define CPU_HZ 1462904000   /* CPU-Takt wie er in /proc/cpuinfo 
                               angezeigt wird */

void  trigger(void)
  {
  outb(0x0, JS_PORT);
  }

main()
  {
  unsigned int low1, high1, low2, high2;
  
  if (ioperm(0x200, 1, 1)) 
    { perror("ioperm"); exit(1); } 
  iopl(3);
  trigger();
  rdtsc(low1, high1);
  while(inb(0x200) & 1);
  rdtsc(low2, high2);

  printf("low1=%u, high1=%u, low2=%u, high2=%u\n", low1, high1, low2, high2);
  printf("low2 - low1 = %u\n", low2 - low1);
  printf("in ms = %f\n", (((double)(low2-low1))/CPU_HZ)*1000);
  }
Auch dieses Programm muß mit Optimierungsstufe -O oder -O2 übersetzt werden.

Für analoge Anwendungen existiert auch ein Treiber (hauptsächlich für Spiele), der als Kernel-Modul geladen werden kann. Die Header-Datei dazu heißt <linux/joystick.h>. Zum Ansprechen des Treibers gibt es einige ioctl-Aufrufe:

Das Lesen der Joystick-Daten erfolgt vom Gerät /dev/jsn, wobei n die Nummer des Joysticks ist. Die Major-Gerätenummer ist 15, die Minor-Nummer entspricht n. Ein Lesen von /dev/jsn gibt Daten vom Typ struct JS_DATA_TYPE zurück. Im Treiberpaket sind auch einige Testprogramme enthalten, die den Gebrauch demonstrieren, z.B.:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/joystick.h>

#define JOY_DEV "/dev/js0"

int main()
  {
  int joy_fd, num_of_axis=0, num_of_buttons=0, x;
  int *axis=NULL,
  char *button=NULL;
  struct js_event js;

  if( ( joy_fd = open( JOY_DEV , O_RDONLY)) == -1 )
    {
    printf( "Couldn't open joystick\n" );
    return -1;
    }

  printf("Joystick: %d axis %d buttons\n",num_of_axis,num_of_buttons );

  /* read number af axes and buttons */
  ioctl(joy_fd, JSIOCGAXES, &num_of_axis);
  ioctl(joy_fd, JSIOCGBUTTONS, &num_of_buttons);

  /* allocate memory */
  axis = (int *) calloc(num_of_axis, sizeof(int));
  button = (char *) calloc(num_of_buttons, sizeof(char));

  /* use non-blocking mode */
  fcntl(joy_fd, F_SETFL, O_NONBLOCK);  

  while(1)  /* infinite loop */
    {
    /* read the joystick state */
    read(joy_fd, &js, sizeof(struct js_event));

    /* see what to do with the event */
    switch (js.type & ~JS_EVENT_INIT)
      {
      case JS_EVENT_AXIS:
        axis [ js.number ] = js.value; break;
      case JS_EVENT_BUTTON:
        button [ js.number ] = js.value; break;
      }

    /* print the results */
    for( x=0; x < num_of_axis; ++x )
    printf("A%d: %6d ", x, axis[x]);

    for( x=0; x < num_of_buttons; ++x )
      printf("B%d: %d ", x, button[x]);

    printf(" \r");
    fflush(stdout);
    }

  close( joy_fd );
  return 0;
  }