![]() |
Linux, PC und HardwareProf. Jürgen Plate |
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.
Portadresse | Funktion | ||
---|---|---|---|
278 | 378 | 3BC | Datenport |
279 | 379 | 3BD | Statusport |
27A | 37A | 3BE | Steuerport |
Üblich sind die Adressen 278-27A und 378-37A, den Druckerport mit den Adressen 3BC-3BE fand man früher oft auf Grafikkarten.
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 |
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);
Bit | Funktion | Pegel |
---|---|---|
0 | Strobe | 0 |
1 | Auto-Feed | 1 |
2 | Reset | 0 |
3 | Select printer | 1 |
4 | IRQ7 enable | 1 |
5 | Direction | 1 |
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);
Bit | Funktion | Pegel |
---|---|---|
3 | Fehler | 0 |
4 | Select out | 1 |
5 | Paper out | 1 |
6 | Acknowledge | 0 |
7 | Busy | 0 |
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.
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.
Bit | Betriebs-Modus | |
---|---|---|
7-5 | 000 | Standard Mode |
001 | Byte Mode | |
010 | Parallel Port FIFO Mode | |
011 | ECP FIFO Mode | |
100 | EPP Mode | |
101 | Reserved | |
110 | FIFO Test Mode | |
111 | Configuration Mode | |
4 | ECP Interrupt Bit | |
3 | DMA Enable Bit | |
2 | ECP Service Bit | |
1 | FIFO Full | |
0 | FIFO 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
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!).
#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.cDas 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.
#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
#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
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:
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); } }