I/O-Ports benutzen

Über I/O-Ports kommunizieren Treiber mit vielen Geräten — zumindest zeitweise. In diesem Abschnitt behandeln wir die diversen Funktionen, mit denen man I/O-Ports verwenden kann, und behandeln auch einige Portabilitätsprobleme.

Denken Sie zunächst daran, daß I/O-Ports alloziert werden müssen, bevor sie von Ihrem Treiber verwendet werden können. Wie in “the Section called I/O-Ports und I/O-Speicher in Kapitel 2” in Kapitel 2 besprochen, gibt es folgende Funktionen zum Allozieren und Freigeben von Ports:


#include <linux/ioport.h>
int check_region(unsigned long start, unsigned long len);
struct resource *request_region(unsigned long start,
       unsigned long len, char *name);
void release_region(unsigned long start, unsigned long len);

Nachdem ein Treiber einen Bereich von I/O-Ports angefordert hat, den er für seine Aufgaben benötigt, muß er diese Ports lesen oder beschreiben. Dazu unterscheiden die meisten Architekturen zwischen 8-Bit-, 16-Bit- und 32-Bit-Ports. Normalerweise können Sie diese nicht miteinander vermischen, wie das bei normalem Systemspeicher möglich ist.[1]

Ein C-Programm muß daher verschiedene Funktionen aufrufen, um auf Ports verschiedener Größe zuzugreifen. Wie im vorigen Abschnitt bereits erwähnt wurde, täuschen Computer-Architekturen, die nur in den Speicher abgebildete I/O-Register unterstützen, die Port-I/O vor, indem sie Port-Adressen auf Speicheradressen abbilden; der Kernel versteckt die Details vor dem Treiber, um die Portabilität zu verbessern. Die Linux-Kernel-Header-Dateien (genauer gesagt, die architekturabhängige Header-Datei <asm/io.h>) definieren die unten aufgeführten Inline-Funktionen, um auf I/O-Ports zuzugreifen:

Note: Von nun an meint unsigned ohne weitere Typangaben eine architekturabhängige Definition, auf deren genaue Art es nicht ankommt. Diese Funktionen sind fast immer portabel, weil der Compiler die Werte automatisch während der Zuweisung mit dem Cast-Operator umwandelt. Sie sind unsigned, um Compiler-Warnungen zu vermeiden. Bei diesen Cast-Operationen geht keine Information verloren, solange der Programmierer sinnvolle Werte verwendet, um Überläufe zu vermeiden. Wir werden für den Rest des Kapitels bei dieser Konvention der “unvollständigen Typisierung” bleiben.

unsigned inb(unsigned port);, void outb(unsigned char byte, unsigned port);

Byte-Ports (8 Bit breit) lesen oder schreiben. 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 Architekturen.

unsigned inw(unsigned port);, void outw(unsigned short word, unsigned port);

Diese Funktionen greifen auf 16-Bit-Ports (“Wort-breit”) zu; sie stehen nicht in der M68k- und der S390-Version von Linux zur Verfügung, die nur Byte-I/O unterstützen.

unsigned inl(unsigned port);, void outl(unsigned longword, unsigned port);

Diese Funktionen greifen auf 32-Bit-Ports zu. longword ist je nach Plattform entweder als unsigned long oder als unsigned int definiert. Wie auch die 16 Bit breite I/O ist auch diese nicht auf M68k- und S390-Systemen verfügbar.

Beachten Sie, daß keine 64-Bit-I/O-Operationen definiert sind. Selbst auf 64-Bit-Architekturen verwenden I/O-Ports nur 32 Bit breite Datenpfade.

Die oben genannten Funktionen sind hauptsächlich für die Verwendung in Gerätetreibern gedacht, können aber auch vom User-Space aus aufgerufen werden — zumindest auf PC-artigen Computern. Die GNU-C-Bibliothek definiert diese Funktionen in <sys/io.h>. Damit inb und Kollegen in User-Space-Code verwendet werden können, müssen aber die folgenden Bedingungen erfüllt sein:

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. Beachten Sie aber, daß die Bedeutung dieser Datei sehr plattformabhängig ist und die Datei vermutlich außerhalb der PC-Plattform nicht sinnvoll verwendet werden kann.

Die Beispielprogramme misc-progs/inp.c und misc-progs/outp.c sind ein minimales Werkzeug für das Lesen und Schreiben von Ports von der Kommandozeile im User-Space aus. Sie erwarten, unter verschiedenen Namen (inpb, inpw, inpl usw.) installiert zu werden, und arbeiten dann auf 8, 16 oder 32 Bit breiten Ports, je nachdem, unter welchem Namen sie aufgerufen worden sind. Wenn ioperm nicht zur Verfügung steht, verwenden sie /dev/port.

Den Programmen kann das setuid root-Bit gesetzt werden, wenn Sie gerne gefährlich leben und mit Ihrer Hardware spielen möchten, ohne explizite Zugriffsrechte zu erwerben.

String-Operationen

Neben den einzelnen Eingabe- und Ausgabe-Operationen implementieren manche Prozessoren spezielle Anweisungen, um eine Folge von Bytes, Wörtern oder Langwörtern in einen oder aus einem I/O-Port der gleichen Größe zu übertragen. Diese Anweisungen heißen String-Anweisungen und erledigen diese Aufgabe schneller, als das eine in C programmierte Schleife könnte. Die folgenden Makros implementieren das Konzept der String-I/O, indem sie entweder eine einzelne Maschinenanweisung verwenden oder eine kurze Schleife ausführen, wenn der Ziel-Prozessor keine Anweisung für String-I/O hat. Die Makros sind überhaupt nicht definiert, wenn für M68k- und S390-Plattformen kompiliert werden soll. Dies sollte kein Portabilitätsproblem sein, da Treiber für diese Plattformen normalerweise ohnehin nicht auf anderen Plattformen verwendet werden; die Peripherie-Busse sind zu unterschiedlich.

Die Prototypen der String-Funktionen sehen folgendermaßen aus:

void insb(unsigned port, void *addr, unsigned long count);, void outsb(unsigned port, void *addr, unsigned long count);

Lesen oder Schreiben von count Bytes ab der Speicheradresse addr. Daten werden aus dem einzelnen Port port gelesen oder auf ihn geschrieben.

void insw(unsigned port, void *addr, unsigned long count);, void outsw(unsigned port, void *addr, unsigned long count);

Lesen oder Schreiben von 16-Bit-Werten aus einem bzw. auf einen 16-Bit-Port.

void insl(unsigned port, void *addr, unsigned long count);, void outsl(unsigned port, void *addr, unsigned long count);

Lesen oder Schreiben von 32-Bit-Werten aus einem bzw. auf einen 32-Bit-Port.

Wartende I/O

Einige Plattformen — besonders i386 — können Probleme bekommen, wenn der Prozessor versucht, Daten zu schnell von oder zum Bus zu transportieren. Diese Probleme können auftreten, wenn der Prozessor im Verhältnis zum ISA-Bus zu schnell getaktet ist, und zeigen sich, wenn das Gerät zu langsam ist. Eine Lösung für dieses Problem besteht darin, eine kurze Pause nach einer I/O-Anweisung einzufügen, wenn eine weitere solche Anweisung folgt. Wenn Ihr Gerät Daten übersieht, oder Sie auch nur befürchten, daß das passieren könnte, dann können Sie die wartenden Funktionen anstelle der normalen verwenden. Die wartenden Funktionen sind die gleichen wie die oben aufgeführten, haben aber Namen, die auf _p enden; sie heißen inb_p, outb_p usw. Die Funktionen sind auf den meisten unterstützten Architekturen nicht definiert, werden aber oft zum gleichen Code wie nicht-wartende I/O expandiert, weil es keinen Grund dafür gibt, extra zu warten, wenn die Architektur keinen veralteten Peripherie-Bus verwendet.

Plattformabhängigkeiten

I/O-Anweisungen sind von Natur aus stark prozessorabhängig. Da Sie die Details nutzen, mit denen der Prozessor Daten transportiert, ist es sehr schwer, die Unterschiede zwischen den Plattformen zu verbergen. Als Folge davon ist ein großer Teil des Port-I/O-Codes plattformabhängig.

Ein Beispiel für solche Inkompatibilitäten, nämlich bei den Datentypen, können Sie sehen, wenn Sie sich die Liste der Funktionen ansehen, in der die Argumente je nach Architektur-Unterschieden zwischen den Plattformen unterschiedliche Typen haben. Beispielsweise ist ein Port auf der x86-Plattform (auf der der Prozessor einen I/O-Raum von 64 KByte unterstützt) ein unsigned short. Auf anderen Plattformen aber, auf denen Ports nur spezielle Plätze im gleichen Adreßraum sind, ist er ein unsigned long.

Andere Plattformabhängigkeiten entstehen aus den grundlegenden strukturellen Unterschieden in den Prozessoren und können daher nicht vermieden werden. Wir werden hier nicht detailliert auf die Unterschiede eingehen, da wir annehmen, daß Sie keinen Gerätetreiber für ein bestimmtes System schreiben werden, ohne die zugrundeliegende Hardware zu verstehen. Statt dessen folgt hier ein Überblick über die Fähigkeiten der in der Kernel-Version 2.4 unterstützten Architekturen:

IA-32 (x86)

Die Architektur unterstützt alle Funktionen, die in diesem Kapitel beschrieben werden. Port-Nummern haben den Typ unsigned short.

IA-64 (Itanium)

Alle Funktionen werden unterstützt; Ports sind unsigned long (und werden in den Speicher abgebildet). String-Funktionen sind in C implementiert.

Alpha

Alle Funktionen werden unterstützt, Ports werden in den Speicher abgebildet. Die Implementation der Port-I/O unterscheidet sich in den verschiedenen Alpha-Plattformen je nach verwendetem Chipsatz. String-Funktionen sind in C implementiert und werden in arch/alpha/lib/io.c definiert. Ports sind unsigned long.

ARM

Ports werden in den Speicher abgebildet; alle Funktionen werden unterstützt. String-Funktionen sind in C implementiert. Ports haben den Typ unsigned int.

M68k

Ports werden in den Speicher abgebildet; nur Byte-Funktionen werden unterstützt. String-Funktionen werden nicht unterstützt; der Port-Typ ist unsigned char*.

MIPS, MIPS64

Die MIPS-Portierung unterstützt alle Funktionen. String-Operationen sind mit kleinen Assembler-Schleifen implementiert, weil der Prozessor keine String-I/O auf Maschinenebene kennt. Ports werden in den Speicher abgebildet und sind unsigned int auf 32-Bit-Prozessoren sowie unsigned long auf 64-Bit-Prozessoren.

PowerPC

Alle Funktionen werden unterstützt; Ports haben den Typ unsigned char*.

S390

Ähnlich M68k, die Header-Datei für diese Plattform unterstützt nur Byte-breite Port-I/O ohne String-Operationen. Ports sind char-Zeiger und werden in den Speicher abgebildet.

Super-H

Ports sind unsigned int (und werden in den Speicher abgebildet). Alle Funktionen werden unterstützt.

SPARC, SPARC64

Auch hier wird der I/O-Raum in den Speicher abgebildet. Die Versionen der Port-Funktionen arbeiten mit unsigned long-Ports.

Neugierige Leser können weitere Informationen in den Dateien io.h finden, die manchmal noch einige zusätzliche architekturspezifische Funktionen außer den in diesem Kapitel angegebenen definieren. Beachten Sie aber, daß diese Dateien recht schwer zu lesen sind.

Es ist interessant, daß kein Prozessor außer den Prozessoren der x86-Familie getrennte Adreßräume für Ports hat, auch wenn mehrere der unterstützten Familien mit ISA- und/oder PCI-Steckplätzen ausgeliefert werden (und beide Busse unterschiedliche I/O- und Speicher-Adreßräume definieren).

Außerdem fehlen manchen Prozessoren (vor allem frühen Alphas) Anweisungen, mit denen ein oder zwei Bytes gleichzeitig verschoben werden können.[3] Daher simulieren deren Peripherie-Chipsätze 8-Bit- und 16-Bit-I/O-Zugriffe durch Abbilden auf spezielle Adreßbereiche im Speicher-Adreßraum. Eine inb- und eine inw-Anweisung, die auf dem gleichen Port arbeiten, sind also als zwei 32-Bit-Leseoperationen implementiert, die auf verschiedenen Adressen arbeiten. Glücklicherweise bleibt all dies durch die in diesem Abschnitt beschriebenen Makros vor dem Autor eines Gerätetreibers verborgen, aber wir halten es doch für interessant genug, um es zu erwähnen. Wenn Sie sich weiter informieren wollen, dann schauen Sie in include/asm-alpha/core_lca.h.

Wie I/O-Operationen auf den einzelnen Plattformen ausgeführt werden, ist im Programmierhandbuch der jeweiligen Plattform gut beschrieben; diese Handbücher sind normalerweise als PDF-Dateien im Web erhältlich.

Fußnoten

[1]

Übrigens sind I/O-Ports manchmal tatsächlich wie Speicher angeordnet, und Sie können beispielsweise zwei 8-Bit-Schreiboperationen zu einer 16-Bit-Operation kombinieren. Das gilt zum Beispiel für PC-Grafikkarten, aber Sie können sich nicht generell darauf verlassen.

[2]

Technisch gesehen muß die Fähigkeit CAP_SYS_RAWIO vorhanden sein, das ist aber auf aktuellen Systemen das gleiche, wie Superuser zu sein.

[3]

I/O mit einzelnen Bytes ist nicht so wichtig, wie Sie vielleicht denken, denn diese Operation wird nur selten gebraucht. Um ein einzelnes Byte aus einem beliebigen Adreßraum zu lesen oder in ihn zu schreiben, müssen Sie einen Datenpfad implementieren, der die unteren Bits des Datenbus zum Setzen der Register mit Byte-Positionen im externen Datenbus verbindet. Diese Datenpfade erfordern zusätzliche Logik-Gatter, die bei jeder Datenübertragung im Weg sind. Das Weglassen von Byte-breiten Lade- und Speicheroperationen kann die gesamte Systemperformance verbessern.