Linux, PC und Hardware


Prof. Jürgen Plate

Die parallele PC-Schnittstelle

Allgemeines

Sie wird verwendet, um Peripheriegeräte über mehrere Leitungen gleichzeitig anzusprechen. Sie erlauben es, Daten schnell byte- oder wortweise einzulesen oder auszugeben. Ein Standardgerät, das im allgemeinen auf diese Weise angeschlossen ist, ist der Drucker. Die "klassische" Druckerschnittstelle arbeitet mit acht Datenleitungen, einer Leitung, die anzeigt, dass die Daten gültig sind und einer Leitung, die meldet, ob der Drucker beschäftigt ist. Der normale Ablauf beim Drucken eines Zeichens stellt sich folgendermaßen dar:

Das auszugebende Zeichen wird an den Datenport gelegt. Danach wird die BUSY-Leitung (Bit 7) getestet, bis sie auf Low-Pegel geht oder nach 16 Sekunden ein Timeout erkannt wird. Anschließend wird die STROBE-Leitung (Bit 0) gepulst und so das Zeichen vom Drucker übernommen.

Die Daten BUSY und STROBE sind invertiert; man muß also auf Bit 7 = 1 beim STATUSPORT testen und Bit 0 des Steuerports kurz auf 1 setzen, um den Strobe-Impuls zu erzeugen. Ein Acknowledge-Signal vom Drucker löst den IRQ7 aus, falls dieser freigegeben ist.

Weitere Leitungen sind für zusätzliche Statusmeldungen zuständig. In den letzten Jahren wurde die Funktionalität der Druckerschnittstelle beträchtlich erweitert, so dass bei modernen Rechnern eine sehr schnelle bidirektionale Datenübertragung zu Peripheriegeräten möglich ist.

Daneben gibt es einfache konfigurierbare Parallel-Schnittstellen-Karten für den PC, die nicht für einen einzigen Verwendungszweck vorgesehen sind wie die Druckerschnittstelle. Früher wurde dafür überwiegend der Baustein 8255 verwendet, inzwischen kann man auch Geräte erwerben, die eine Parallelschnittstelle auf USB umsetzen.

Die "klassische" Druckerschnittstelle wird, da sie ursprünglich nur für einen speziellen Zweck vorgesehen war, über festverdrahtete, nicht weiter konfigurierbare Ausgabe- und Eingabe-Register betrieben. Einen schematischen Überblick der Schaltung und der Zuordnung der Register-Bits zur 25-poligen Buchse am PC zeigt folgendes Bild:

Über die Basisadresse der Druckerschnittstelle werden die acht Datenleitungen zum Drucker angesprochen, ihr Zustand kann über diese Adresse festgelegt und auch gelesen werden. Das Ausgaberegister mit Basisadresse + 2 bedient die zum Drucker gehenden Steuerleitungen. Bit 4 dieses Registers legt fest, ob die vom Drucker kommende Acknowledge-Leitung auf die IRQ-7-Leitung des PC geschaltet wird und damit einen Interrupt auslösen kann. Auch der Zustand dieser Leitungen kann über die gleiche Adresse wieder eingelesen werden. Die vom Drucker kommenden Statusleitungen sind mit einem Eingaberegister verbunden, das unter Basisadresse + 1 gelesen werden kann. Die Druckerschnittstelle des PC bietet somit zwölf Ausgabe- und fünf Eingabeleitungen. Eine Eingabeleitung kann zudem als Interruptleitung benutzt werden. Die Signale sind TTL-kompatibel. Zumindest die Datenleitungen und die Strobeleitung lassen relativ niederohmigen Betrieb zu (> 150 Ohm). Verwendet man paarweise verdrillte Kabel oder Flachbandkabel zum Anschluss der Peripherie, so ist durch die Anordnung der Masseleitungen ein relativ störsicherer Betrieb bis zu etwa 100 kHz möglich. So ist die Druckerschnittstelle auch gut für Steuerungszwecke geeignet.

Die Leitungen des Steuerports sind bei vielen Schnittstellen als Open-Collector-Ausgänge ausgeführt. Wenn man die Ausgangsleitungen auf 1-Potential legt, könnte man diese Leitungen auch als Eingang verwenden (auf 0-Potential ziehen). Da diese Möglichkeit aber nicht allgemein wahrgenommen werden kann, ist i.a. davon abzuraten.

Port-Adressen

Der PC kann maximal drei Centronics-Schnittstellen betreiben. Die Portbelegung ist:

PortadresseFunktion
2783783BCDatenport
2793793BDStatusport
27A37A3BESteuerport

Üblich sind die Adressen 278-27A und 378-37A, den Druckerport mit den Adressen 3BC-3BE fand man früher oft auf Grafikkarten.

Belegung des Steckers am PC

Die folgende Tabelle beschreibt die Signale und die Beschaltung der Centronics-Buchse am Drucker.

Signal (Bild) Pin (SUB-D 25) Pin (Centronics) SPP Signal Richtung
Out
Register Hardware
invertiert
C0 1 1 Strobe Out Control Ja
D0 2 2 Data 0 Out Data  
D1 3 3 Data 1 Out Data  
D2 4 4 Data 2 Out Data  
D3 5 5 Data 3 Out Data  
D4 6 6 Data 4 Out Data  
D5 7 7 Data 5 Out Data  
D6 8 8 Data 6 Out Data  
D7 9 9 Data 7 Out Data  
S6 10 10 Ack In Status  
S7 11 11 Busy In Status Ja
S5 12 12 Paper-Out/Paper-End In Status  
S4 13 13 Select In Status  
C1 14 14 Auto-Linefeed Out Control Ja
S3 15 32 Error In Status  
C2 16 31 Reset Out Control  
C3 17 36 Select-Printer/Select-In Out Control Ja
  18 - 25 19-30 Ground Gnd    

Daten, Steuer- und Statusport

Datenport

Der Datenport ist ein 8-Bit-Ausgangsport, der ursprünglich durch ein Latch 74374 realisiert wurde. Die ausgegebenen Daten können über einen Treiber 74244 zurückgelesen werden. Heutzutage finden Sie auf Ihrem PC-Board natürlich keine TTL-Schaltkreise mehr, sondern die Schnittstellen sind in ein ASIC integriert. Die Funktion ist jedoch dieselbe wie bei diskretem Aufbau. Heutige Schnittstellen erlauben meist auch die Umschaltung zwischen Ausgang und Eingang (Näheres siehe unten).

Programmierbeispiel:

#define DATA 0x378
#define STATUS 0x379
#define CONTROL 0x37A
...
unsigned char byte;
...
/* Ausgabe des Bitmusters 0110 1001 */
byte = 0x69; 
outb(byte, DATA);

/* Setzen Bit 5, alle anderen Bits belassen */
outb(inb(DATA) | 0x10, DATA);

/* Ruecksetzen Bit 5, alle anderen Bits belassen */
outb(inb(DATA) & (~0x10), DATA);

Steuerport

BitFunktionPegel
0Strobe0
1Auto-Feed1
2Reset0
3Select printer1
4IRQ7 enable1
5Direction1

Das "Direction"-Bit wird nur von bidirektionalen Schnittstellen unterstützt (PS/2- und Byte-Mode). Bei Standard-Schnittstellen hat es keine Funktion. Wenn dieses Bit auf 1 gesetzt wird, wird der Ausgang des Datenports hochohmig und es können Daten eingelesen werden. In diesem Stadium werden Daten, die auf den Datenport geschrieben werden, zwar im Latch gespeichert, aber nicht auf die Ausgabepins geschaltet. Die folgende Abbildung zeigt das Schema des Datenports mit den entsprechenden Steuerleitungen:

Bei älteren Schnittstellen ist der \OE-Pin des Latch 74LS374 fest auf 0-Potential gelegt und daher der Port immer aktiv.

Bei der Programmierung ist zu berücksichtigen, daß einige Leitungen des Steuerports per Hardware invertiert werden. Um einen Pegel am Ausgang zu erhalten, der dem ausgegebenen Wert entspricht, ist einen Exklusiv-Oder-Verknüpfung des Wertes mit 0x0B nötig (Exor invertiert dann die entsprechenden Bits). Zum Beispiel:

byte = 0x08; /* 0000 1000 */
outb(byte^0x0B, CONTROL); 

Statusport

BitFunktionPegel
3Fehler0
4Select out1
5Paper out1
6Acknowledge0
7Busy0

Bei der Programmierung ist zu beachten, daß das Busy-Bit hardwaremäßig invertiert ist. Um einem korrigierten Eingabewert zu erzielen, ist wieder eine Exor-Verknüpfung mit 0x80 (binär 1000000) notwendig. Da beim Statusport die Bits 3 bis 7 belegt sind, kann man auch gleich noch das Ergebnis nach unten auf die Bits 0 bis 4 verschieben:

byte = ((inb(STATUS) ^ 0x80) >> 3); 
Es sind zwar nur fünf Eingangsleitungen, aber auch wenn der Datenport nicht bidirektional ist, kann man mit minimalem Hardwareaufwand einen 8-Bit-Eingang realisieren. Ganz ohne Hardware geht es, wenn man nacheinander jeweils die höherwertigen 4 Bits an die Statusbits 4-7 anlegt (und Bit 3 auf 1 setzt), danach die 4 niederwertigen Bits anlegt und Bit 3 auf 0 setzt. Da eine Gruppe von 4 Bits auch als "Nibble" bezeichet wird, kann man dies auch als "Nibble-Mode" bezeichen. Der Nachteil dieser Lösung ist, daß die EIngabeschaltung den Nibble-Wechsel vollziehen muß.

Mit etwas Hardware kann man einen statischen 8-Bit-Eingang realisieren. Es wird ein Multiplexerbaustein 74HC157 an den Statusport geschaltet. Über eine Ausgabeleitung des Steuerports kann nun das obere oder untere Nibble ausgewählt werden.

Liegt der A/B-Eingang des Multiplexers auf 0, werden die A-Eingänge des Mux selektiert, im anderen Fall die B-Eingänge. Um ein Byte zu lesen, wird erst A/B auf 0 gelegt (da der Strobe-Ausgang hardwaremäßig invertiert ist, muß Bit 0 des Statusports auf 1 gesetzt werden:

outb(inb(CONTROL) | 0x01, CONTROL); /* Select Low Nibble (A)*/
Nun kann das niederwertige Nibble eingelesen werden:
byte = inb(STATUS) & 0xF0; /* Read Low Nibble */
Nachdem die Daten in den Bits 4-7 des Statusports gelandet sind, müssen sie noch um vier Stellen nach rechts geschoben werden (auf die Bits 0-3).
byte = byte >> 4; /* Shift Right 4 Bits */
Nun wird das höherwertige Nibble eingelesen. Dazu wird der Strobe-Ausgang auf 1 gesetzt und dann werden wieder 4 Bits eingelesen:
outb(inb(CONTROL) & 0xFE, CONTROL);  /* Select High Nibble (B)*/
byte = byte | (inb(STATUS) & 0xF0);  /* Read High Nibble */
byte = byte ^ 0x88;                      /* Adjust Value */
Die letzte Zeile toggelt die Bits 3 und 7, weil die BUSY-Leitung ein invertiertes Signal liefert. Danach steht das Eingansbyte korrekt in der Variablen.

Je nach Geschwindigkeit des Systems und abhängig von den Eingenschaften der Schnittstelle kann es vorkommen, daß zwischen den einzelnen Portzugriffen kurze Pausen eingefügt werden müssen.

Port-Typen

Durch verschiedene Verbesserungen wurde in den vergangenen Jahren die Leistungsfähigkeit der Druckerschnittstelle deutlich erhöht. Die von unterschiedlichen Herstellern eingeführten Veränderungen wurden 1994 vom IEEE vereinheitlicht und in einer neuen Norm für die Druckerschnittstelle, IEEE 1284, verabschiedet. Praktisch alle Hersteller halten sich inzwischen an diesen Standard. Die Norm enthält die Druckerschnittstelle alter Art (SPP, Standard Parallel Port) als Basisbetriebsart, ist also voll rückwärtskompatibel, daneben sind verschiedene bidirektionale Betriebsarten (u.a. EPP und ECP) definiert, die einen schnellen Datenaustausch (bis zu 2 MByte/sec) mit Peripheriegeräten wie Scannern oder externen Laufwerken ermöglichen. Neben den unterschiedlichen Betriebsarten sind im Standard IEEE 1284 auch die elektrischen Daten der Schnittstelle wie Steckerbelegung, Kabelart, maximale Kabellänge (10 m) usw. genauestens spezifiziert. Eine gute und ausführliche Beschreibung der Norm bietet die Webseite von Warp Nine Engineering (/http://www.fapo.com/ieee1284.htm), eines Herstellers von Peripherie-Bausteinen für die IEEE 1284. Hardware-Beschreibungen sind in den Datenblättern für die entsprechenden Schnittstellen-ICs enthalten (z. B. für den TL16PIR552, einem Baustein mit zwei seriellen und einem parallen Port oder den SN74LVC161284, einem IEEE-1284-kompatiblen Treiberbaustein).

SPP: Standard Parallel Port

Der SPP stellt den Parallelport dar, wie ihn IBM 1981 für den PC ausgewiesen hat und wie er oben schon behandelt wurde. Die Datenausgabe über die Datenleitungen D0 - D7 sind unidirektional, also nur in eine Richtung definiert. Mit dem SPP sind nur folgende Betriebsarten (nach IEEE1284) möglich:

EPP: Enhanced Parallel Port

Das EPP-Protokoll wurde von Intel, Xircom und Zenith Data Systems entwickelt und stellt eine Hochgeschwindigkeits-Parallelschnittstelle dar, welche sich aber noch mit SPP verträgt. Dieses Protokoll wurde von Intel in den 386SL (82360 I/O Chip) implementiert und schließlich zum IEEE1284-Standard erhoben. Bleibt anzumerken, daß es bei EPP leider zwei Versionen gibt: die ältere, EPP V1.7, und die neuere EPP-1284 (EPP 1.9).

Die zusätzlichen EPP-Register können durch BASE+03h, BASE+04h, BASE+05h adressiert werden. (BASE = 0x278, 0x378 oder 0x3BC). Mittels EPP können Datenraten von 500KB/s bis zu 2MB/s erreicht werden. Die Leitungen der Schnittstelle werden etwas anders spezifiziert als beim SPP.

Der Handshake erfolgt mit eigener Hardware. Es ist nur ein Portzugriff pro übertragenem Byte nötig. Das Busy-Signal hat die gleiche Funktion wie IOCHRDY am ISA-Bus und kann Waits direkt am Prozessor einleiten (Time-Out erforderlich). Über den Address-Strobe sind 256 verschiedene Geräte adressierbar. Weiterhin sind 32-Bit-Schreibzugriffe auf den Port möglich. Es gibt vier Arten von Datenübertragungszyklen:

ECP: Extended Capability Port

Das ECP-Protokoll wurde von Hewlett Packard und Microsoft als modifizierte Kommunikation zwischen Druckern und Scannern ausgewiesen. Ähnlich wie das EPP-Protokoll unterstützt ECP die Parallelübertragung mit hoher Geschwindigkeit zwischen Host-Adapter und Peripherie. Ferner sieht ECP einen eigenen DMA-Kanal vor, FIFO-Pufferung für Hin- und Rückkanal und Datenkompression nach dem Runlenght-Encoding (RLE) Verfahren. Die zusätzlich erforderlichen ECP-Register können durch aufaddieren von 0x400 auf die Basisadressen adressiert werden. Auch hier sind mehrere Geräte adressierbar. Spezifikationen: Es gibt zwei Arten von bidirektionalen Datenübertragungszyklen: Hier ist das Extended Control Register (ECR) ganz interessant, das unter der Adresse BASE + 0x402 zu finden ist.

BitBetriebs-Modus
7-5000Standard Mode
001Byte Mode
010Parallel Port FIFO Mode
011ECP FIFO Mode
100EPP Mode
101Reserved
110FIFO Test Mode
111Configuration Mode
4ECP Interrupt Bit
3DMA Enable Bit
2ECP Service Bit
1FIFO Full
0FIFO Empty

Da für die im folgenden gezeigten Beispiele für die Hardwareansteuerung die Modi EPP und ECP nicht verwendet werden, sei für diese auf die oben genannte Webseite von Warp Nine verwiesen.

Bei allen Experimenten am Parallelport ist Vorsicht geboten, weil bei unsachgemäßer Handhabung Kurzschlüsse für den Parallelport oder gar für das gesamte Mainboard entstehen können. Die Ausgänge eines Centronics-Ports halten einer Strombelastung von ca. 5-15 mA stand. Dies reicht mit einem geeigneten Vorwiderstand (z.B. 270 bis 680 Ohm), um eine LED zum Leuchten zu bringen. Möchte man Taster oder Schalter direkt abfragen, so müssen die Eingänge über Pull-Up-Widerstände in einen definierten High-Zustand gebracht werden und können dann gezielt auf Masse geschaltet werden.

Programmierung

Insbesondere Programmierer aus der einstigen DOS-Welt werden bei der ersten Berührung mit Linux zunächst die Funktionen inportb() und outportb() für die einfache Ansteuerung externer Hardware vermissen. Analoge Befehle stehen aber auch unter Linux zur Verfügung. Bei Anwendungen im User-Space können beliebig komplexe C-Bibliotheken hinzugelinkt werden und die Fehlersuche kann mit herkömlichen Debuggern erfolgen. Außerdem ist es problemlos möglich, ein blockierendes Programm im User-Space zur Terminierung zu zwingen. Fehlerhafter Implemtierungen können (auf Grund von Speicherschutzmechanismen) kaum Schaden am laufenden System anrichten und beeinflussen deshalb nur selten die Stabilität von Linux. Treiber haben dagegen den Vorteil, näher an die Echtzeitbedingung heranzukommen (kein Swappen, DMA, Interruptbearbeitung, usw.).

Programme mit direkter Hardware-Schreib-/Leseberechtigung dürfen aus Sicherheitsgründen nur von root ausführt werden, da sonst jeder beliebige Benutzer den Rechner bei falscher I/O-Programmierung lahmlegen könnte. Das dennoch potentiell auftretende Sicherheitsrisiko bei Eigenentwicklungen spielt bei Embedded Systems oder Heim-Linux-PCs eine geringere Rolle.

Direkte I/O-Addressierung durch Programme im User-Space eignet sich für einfache und zeitunkritische Mess- und Steueraufgaben, z.B. zur Programmierung von Relaissteckkarten, AD-Wandlern oder alphanumerischen LC-Displays (z.B. über den Parallelport).

Ein C-Programm muß verschiedene Funktionen aufrufen, um auf Ports verschiedener Größe zuzugreifen. Um die Portabilität zu erhöhen, täuscht der Kernel bei Computer-Architekturen, die in den Speicher abgebildete I/O-Register (memory mapped i/o) besitzen, die Port-I/O vor, indem er Port-Adressen auf Speicheradressen abbildet. Die architekturabhängige Header-Datei sys/io.h (glibc) oder unistd.h (libc5) definiert die unten aufgeführten Inline-Funktionen für die I/O-Portzugriffe.

Mit unsigned ohne weitere Typangabe wird eine architekturabhängige Definition verwendet, bei der es auf den genauen Typ nicht ankommt. Die Funktionen sind fast immer portabel, weil der Compiler die Werte automatisch während der Zuweisung mit dem Cast-Operator umwandelt. Die beiden Funktionen

lesen oder schreiben Byte-Ports (8 Bit). Das Argument port ist auf manchen Plattformen als unsigned long und auf anderen als unsigned short definiert. Der Rückgabewert von inb unterscheidet sich auch auf den einzelnen Hardware-Architekturen. Die Funktionen greifen auf 16-Bit-Ports (Wort) zu und stehen nicht in der M68k- und der S390-Version von Linux zur Verfügung.Die Funktionen greifen auf 32-Bit-Ports zu. longword ist je nach Plattform entweder als unsigned long oder als unsigned int definiert. Auch diese stehen nicht auf M68k- und S390-Systemen zur Verfügung.

Die oben genannten Funktionen sind hauptsächlich für die Verwendung in Gerätetreibern vorgesehen, können aber auch vom User-Space aus aufgerufen werden - zumindest auf PCs. Die GNU-C-Bibliothek definiert diese Funktionen in sys/io.h. Damit sie im User-Space-Code verwendet werden können, müssen aber folgende Bedingungen erfüllt sein:

    int ioperm (unsigned long from, unsigned long num, int turn_on);  
setzt die Portzugriffsrechtsbits für den Prozess für num Bytes beginnend mit der Portadresse from auf den Wert turn_on (0 oder 1). Der Gebrauch benötigt Superuser-Rechte. Es können nur die ersten 1024 I/O-Ports von ioperm freigegeben werden. Bei weiteren Ports muß die Funktion iopl verwendet werden. Rechte werden nicht von fork(), wohl aber von exec() vererbt. Bei Erfolg wird 0, im Fehlerfall wird -1 zurückgegeben und die Variable errno entsprechend gesetzt. Wenn die Systemaufrufe ioperm und iopl auf der Host-Plattform nicht zur Verfügung stehen, kann man vom User-Space aus trotzdem noch auf die I/O-Ports zugreifen, indem man die Gerätedatei /dev/port verwendet (sehr plattformabhängig!).

Ein erstes Beispiel


#include <sys/io.h>
int main(int argc, char* argv[]) 
  {
  int base = atoi(argv[1]);
  int value = atoi(argv[2]);
  ioperm(base,1,1); 
  outb(value,base);
  ioperm(base,1,0);
  return 0;
  }
Bei der Ausführung dieses Beispiels auf den Komandozeile müssen zwei Argumente übergeben werden (z.B. iop 888 85). Der erste Wert ist die Basis-Addresse eines freien Parallelports. Der Wert 888 (= 0x378) sollte hier für einen normalen PC brauchbar sein, solange nicht der Druckeranschluss per Jumper oder BIOS in einen anderen I/O-Addressraum verlegt wurde. Das zweite Argument wird als Bitmuster verwendet, welches an die Datenleitungen des Parallelports angelegt wird. Der verwendete I/O-Bereich wird durch den Aufruf von ioperm() freigeschaltet. Ab der Basisaddresse wird der Zugriff auf genau einen I/O-Port erlaubt, da der dritte Parameter von ioperm() 1 ist. In der folgenden Programmzeile wird das angegebenen Bitmuster (hier "01010101") auf den Datenleitungen des Druckeranschlusses ausgegeben. Schließlich wird der direkte Zugriff auf die Ports mit Hilfe einer 0 als letztem Parameter von ioperm() wieder verboten.

Das Programm wird mit folgender Befehlszeile übersetzt:

gcc -O2 -o iop iop.c
Das Programm kann nun z.B. mit ./iop 888 255 gestartet werden - unter der Voraussetzung dass man als root eingeloggt ist. Man kann beim Binary auch das Setuid-Bit für root gesetzt werden (chmod 4711 iop), wenn Sie gerne gefährlicher leben und mit Ihrer Hardware spielen möchten, ohne explizite Zugriffsrechte zu erwerben.

Beispiel: Programm für die Ausgabe auf Daten- und Steuerport

Das Programm läuft auf Intel-PCs. Die Kommandozeile benötigt drei Parameter:
#include <stdio.h>
#include <stdlib.h>
#include <sys/io.h>
#include <unistd.h> 

#define DEBUG 0

/* Base addresses of parallel ports */
#define LPT1 0x378
#define LPT2 0x278
#define LPT3 0x3BC

void usage(void)
{
  printf("Syntax : setport <dataport value> <control port value> <portnum>\n\n");
  printf("portnum: 1, 2 or 3 for LPT1, LPT2 or LPT3.\n");
  exit(1);
}

int main(int argc, char *argv[])
  {
  int data = 0;          /* data port value */
  int control = 0;       /* control port value */
  int BASEPORT = 0;      /* selected printer port */

  if (argc != 4) usage();       /* falsche Parameteranzahl */
  switch (argv[3][0])
    {
    case '1': BASEPORT = LPT1; break;
    case '2': BASEPORT = LPT2; break;
    case '3': BASEPORT = LPT3; break;
    default : usage();
    }
  data = atoi(argv[1]);
  if (data < 0 || data > 255)
    { perror("Error: data value out of range"); exit(1); } 
  control = atoi(argv[2]);
  if (control < 0 || control > 15)
    { perror("Error: control value out of range"); exit(1); } 
  /* Get access to the ports */ 
  if (ioperm(BASEPORT, 3, 1)) 
    { perror("Error: cannot access ioport"); exit(1); } 

#ifdef DEBUG
   printf("Debug: Writing to PORT %X.\n", BASEPORT);
   printf("Debug: Setting Data Port to %X.\n", data);
   printf("Debug: Setting Control Port to %X.\n", control);
#endif

  /* Set the data signals of the port */ 
  outb(data, BASEPORT); 
  outb(control ^ 0x0B, BASEPORT + 2);  /* mit Korrektur! */
  /* We don't need the ports anymore */ 
  if (ioperm(BASEPORT, 3, 0)) 
    { perror("Error: cannot access ioport"); exit(1); } 
  exit(0); 
  }
Quellcode setport.c

Beispiel: Programm für das Lesen vom Statusport

Das Programm läuft auf Intel-PCs. Die Kommandozeile benötigt nur einen Parameter: Der Rückgabewert ist im Fehlerfall 255. Bei erfolgreichem Portzugriff entspricht er den Werten des Statusports (nach rechts geschoben auf die Bits 0 - 4). Zum Testen kann man die Shell-Variable $? abfragen (./getport 1 ; echo $?).
#include <stdio.h>
#include <sys/io.h>
#include <unistd.h> 

#define DEBUG 0

/* Base addresses of parallel ports */
#define LPT1 0x378
#define LPT2 0x278
#define LPT3 0x3BC

void usage(void)
{
  printf("Syntax : getport <portnum>\n\n");
  printf("portnum: 1, 2 or 3 for LPT1, LPT2 or LPT3.\n");
  printf("returns: (inb(STATUSPORT) ^ 0x80) >> 3)\n"); 
  printf("         or 255 if an error occurs\n\n"); 
  exit(255);
}

int main(int argc, char *argv[])
  {
  int status = 0; 
  int BASEPORT = LPT1;
printf("argc %d\n",argc);
  if (argc != 2) usage();      /* falsche Parameteranzahl */
  switch (argv[1][0])
    {
    case '1': BASEPORT = LPT1; break;
    case '2': BASEPORT = LPT2; break;
    case '3': BASEPORT = LPT3; break;
    default: usage();
    }
  /* Get access to the ports */ 
  if (ioperm(BASEPORT, 3, 1)) 
    { perror("Error: cannot access ioport"); exit(255); } 
  /* Get status port */
  status = ((inb(BASEPORT + 1) ^ 0x80) >> 3); 

#ifdef DEBUG
   printf("Debug: Reading from PORT %X.\n", BASEPORT);
   printf("Debug: Port shows 0x%X.\n", status);
#endif

  /* We don't need the ports anymore */ 
  if (ioperm(BASEPORT, 3, 0)) 
    { perror("Error: cannot access ioport"); exit(255); } 
  exit(status); 
  }
Quellcode getport.c

Programmierung in Perl

Das Perl-Modul für die Druckerschnittstelle ist Device::ParallelPort. Das Modul stellt zwar eine recht leicht zu programmierende Schnittstelle zum Printerport zur Verfügung, es benötigt aber seinerseits den passenden Treiber, um die Hardware wirklich ansprechen zu können. Treiber gibt es dankenswerterweise für Linux (direkt oder per /dev/parport und Windows. Diese Treiber müssen zuerst geladen werden, damit das Perl-Programm tut, was es soll. Angeboten werden: Windows-Benutzer benötigen noch zusätzlich die Programmbibliothek inpout32.dll, die man von http://logix4u.net herunterladen kann und in die ein Kernelmode-Treiber eingebettet ist. Denken Sie auch daran, dass die Win32::API auf jeden Fall benötigt wird. Die Datei wird einfach nach dem Download in Verzeichnis C:\Windows\System oder in das Verzeichnis kopiert, in dem auch das Perl-Programm liegt. Danach kann mit dem Windows-Device des Perl-Moduls auf die Schnittstelle zugegriffen werden.

Das Perl-Modul ist nicht besonders schnell und unterliegt auch recht vielen Beschränkungen - so sollten Sie sich die Frage stellen, ob eine Unterteilung in ein C-Tool für Low-Level-Operationen und ein darauf aufsetzendes Perl-Programm nicht die bessere Idee sein könnte. Das Modul kennt jeweils Methoden für den bitweisen oder byteweisen Zugriff auf die Schnittstelle, wobei die drei aufeinanderfolgenden Adressen eines Ports in den Perl-Methoden wie ein 24-Bit-Wort behandelt werden. Es besitzt nur wenige Methoden:

Bei den set...-Funktionen darf der Wert kein Integer sein, sondern nur acht Bit lang sein. In Perl können Sie sich mit der chr()-Funktion behelfen, indem Sie beispielsweise set_byte(0, chr(WERT)) verwenden.

Leider sind bei allen Modulen die Portadressen fest auf die oben angegebenen Adressen codiert. Es gibt auch keine Möglichkeit, das per Methodenaufruf zu ändern, sondern man muss bis zum hardwarenahen C-Programm hinabsteigen. Wie gesagt, ist das Modul nicht besonders komfortabel ausgefallen. Bei einigen Treibern kann man zwischen den beiden Adressen 0x378 und 0x278 wählen, indem man beim Treibernamen einen Doppelpunkt und 0 oder 1 anhängt, z. B. "win32:0". Portadressen auf PCI-Karten sind nicht ansprechbar, ohne das Modul zu ändern.

Da die ganze Sache bei Windows etwas kritischer als bei Linux ist, nehme ich hier den Windows-Rechner zum Ausprobieren. Das erste Beispiel liest den Statusport der Schnittstelle aus. Da wie oben beschrieben ein Bit hardwaremäßig invertiert ist und die Bits auch die Positionen 7 bis 3 einnehmen, wird die Invertierung per Exor-Verknüpfung neutralisiert und die Bits auch gleich drei Stellen nach rechts verschoben. Die binäre Ausgabe spiegelt dann genau die Logikpegel am Eingang wider:

use strict;
use warnings;
use Device::ParallelPort;

my $port = Device::ParallelPort->new('win32:0');
while (1)
  {
  # Statusport einlesen und invertierte Leitung
  # korrigieren, 3 Stellen nach rechts schieben
  my $x = (ord($port->get_status()) ^ 0x80) >> 3;
  # als Binaerzahl ausgeben
  printf("%05b\n", $x);
  # 1/2 Sekunde warten (Trick!)
  select( undef, undef, undef, 0.5);
  }
Im Programm wird noch ein Trick angewendet. Um ein "sleep" mit Sekundenbruchteilen zu realisieren (Windows hat kein usleep() oder nanosleep()), wird die Funktion select mißbraucht. Alle Funktionsreferenzen des select-Aufrufs sind mit "undef" besetzt, sodass nur eine halbe Sekunde gewartet wird.

Das zweite Listing schaltet der Reihe nach alle Bits von Datenport (0 bis 7) und Steuerport (16 bis 19) erst ein und dann wieder aus. Auch hier wird die Verzögerung mit select verwendet. Bei Steuerport sieht es komisch aus, da einige Bits invertiert ausgegeben werden.

use strict;
use warnings;
use Device::ParallelPort;

my $port = Device::ParallelPort->new('win32:0');
while (1)
  {
  # Datenport als Lauflicht
  # Steuerport ebenso
  # sieht komisch aus --> einige Bits invertiert
  for my $i (0 .. 7, 16 .. 19)
    {
    $port->set_bit($i,1);         # LED einschalten
    # 1/2 Sekunde warten (Trick!)
    select( undef, undef, undef, 0.5);
    $port->set_bit($i,0);         # LED ausschalten
    # 1/2 Sekunde warten (Trick!)
    select( undef, undef, undef, 0.5); 
    }
  }

Copyright © FH München, FB 04, Prof. Jürgen Plate
Letzte Aktualisierung: 23. Feb 2012, 15:21:02