Kapitel 15. Ein Überblick über die Peripherie-Busse

Inhalt
Die PCI-Schnittstelle
Ein Blick zurück: ISA
PC/104 und PC/104+
Andere PC-Busse
SBus
NuBus
Externe Bus-Systeme
Abwärtskompatibilität
Schnellreferenz

Während Kapitel 8 die unterste Ebene der Hardware-Ansteuerung beschrieb, gibt dieses Kapitel einen Überblick über die Bus-Architekturen höherer Ebenen. Ein Bus besteht sowohl aus einer elektrischen Schnittstelle als auch aus einer Programmierschnittstelle. In diesem Kapitel gehen wir auf die Programmierschnittstelle ein.

Dieses Kapitel behandelt eine Reihe von Bus-Architekturen. Der Schwerpunkt wird aber auf denjenigen Kernel-Funktionen liegen, die auf PCI-Peripherie-Geräte zugreifen, weil heutzutage der PCI-Bus der auf Desktop-Systemen und größeren Computern am häufigsten verwendete und auch der vom Kernel am besten unterstützte Bus ist. ISA ist für Elektronikbastler immer noch eine gängige Wahl und wird später näher beschrieben, auch wenn dieser Bus ziemlich einfach gestrickt ist und es über ihn nicht viel mehr als das zu sagen gibt, was wir bereits in Kapitel 8 und Kapitel 9 geschrieben haben.

Die PCI-Schnittstelle

Obwohl viele Computerbenutzer PCI (Peripheral Component Interconnect) für ein Verfahren halten, elektrische Drähte zu organisieren, ist es eigentlich ein vollständiger Satz von Spezifikationen, der bestimmt, wie die einzelnen Teile eines Computers interagieren sollten.

Die PCI-Spezifikation behandelt die meisten Fragen aus dem Bereich der Computer-Schnittstellen. Wir werden hier nicht auf alle eingehen, sondern uns hauptsächlich darauf konzentrieren, wie ein PCI-Treiber seine Hardware finden und darauf Zugriff bekommen kann. Die in “the Section called Automatische und manuelle Konfiguration in Kapitel 2” in Kapitel 2 und in “the Section called Automatische Erkennung der IRQ-Nummer in Kapitel 9” in Kapitel 9 behandelten Suchtechniken können auch bei PCI-Geräten verwendet werden, aber die Spezifikation bietet auch noch eine Alternative zum Suchen.

Die PCI-Architektur wurde als ein Ersatz für den ISA-Standard entworfen und hatte drei Entwurfsziele: eine bessere Performance bei der Übertragung von Daten zwischen dem Computer und Peripherie-Geräten, größtmögliche Plattformunabhängigkeit und ein einfaches Hinzufügen und Entfernen von Peripherie-Geräten.

Der PCI-Bus erreicht seine bessere Performance durch Verwendung einer höheren Taktrate als ISA; diese beträgt 25 oder 33 MHz (der tatsächliche Wert ist ein Teiler des Systemtakts); 66-MHz- und sogar 133-MHz-Implementationen sind ebenfalls bereits verfügbar. Außerdem ist der Datenbus 32 Bit breit, eine 64 Bit-Erweiterung ist in die Spezifikation mit aufgenommen worden (auch wenn nur 64-Bit-Plattformen das implementieren). Plattformunabhängigkeit ist oft ein Ziel bei der Entwicklung von Computer-Bussen und ein besonders wichtiges Merkmal von PCI, weil die PC-Welt immer von prozessorspezifischen Schnittstellenstandards dominiert worden ist. PCI wird derzeit umfassend auf IA-32, Alpha, PowerPC, SPARC64 und IA-64-Systemen sowie einigen anderen Plattformen verwendet.

Für den Programmierer eines Treibers ist jedoch die Unterstützung der Autoerkennung von Karten am wichtigsten. PCI-Geräte haben im Gegensatz zu den meisten ISA-Peripherie-Geräten keine Jumper und werden beim Booten automatisch konfiguriert. Der Treiber-Programmierer muß also in der Lage sein, auf die Konfigurationsinformationen des Gerätes zuzugreifen, um die Initialisierung vervollständigen zu können. Das geschieht, ohne daß nach dem Gerät gesucht werden muß.

PCI-Adressierung

Jedes PCI-Peripherie-Gerät wird durch eine Bus-Nummer, eine Gerätenummer und eine Funktionsnummer identifiziert. Die PCI-Spezifikation erlaubt bis zu 256 Busse in einem System. Jeder Bus kann bis zu 32 Geräte enthalten, und jedes Gerät kann eine Multifunktionskarte (wie eine Soundkarte mit eingebautem CD-ROM-Anschluß) mit maximal acht Funktionen sein. Jede Funktion kann also auf Hardware-Ebene durch eine 16-Bit-Adresse (Schlüssel) identifiziert werden. Gerätetreiber für Linux müssen sich aber nicht mit diesen Binäradressen beschäftigen, weil sie eine bestimmte Datenstruktur namens pci_dev verwenden, um auf den Geräten zu arbeiten. (Das ist natürlich genau die struct pci_dev, die wir schon in Kapitel 13 kennengelernt haben.

Die meisten neueren Workstations enthalten mindestens zwei PCI-Busse. Bei mehreren Bussen in einem System werden sogenannte Brücken (bridges) verwendet, spezielle PCI-Peripherie-Geräte, deren Aufgabe es ist, die Busse zu verbinden. Das gesamte Layout eines PCI-Systems ist als Baum organisiert, in dem jeder Bus bis herauf zum Bus 0 mit einem Bus der nächsthöheren Schicht verbunden ist. Das CardBus PC-Karten-System wird ebenfalls über Brücken an das PCI-System angebunden. Ein typisches PCI-System ist in Abbildung 15-1 zu sehen, wo die diversen Brücken hervorgehoben sind.

Abbildung 15-1. Layout eines typischen PCI-Systems

Die 16-Bit-Hardware-Adressen von PCI-Peripherie-Geräten sind zwar meistens im struct pci_dev-Objekt versteckt, aber trotzdem gelegentlich sichtbar, besonders wenn es um Listen von Geräten geht. Eine solche Situation ist die Ausgabe von lspci (einem Bestandteil des pciutils-Pakets, das in den meisten Distributionen enthalten ist) und das Informationslayout in /proc/pci und /proc/bus/pci.[1] Wenn die Hardware-Adresse angezeigt wird, kann dies entweder als 16-Bit Wert, als zwei Werte (eine 8-Bit-Zahl und eine 8-Bit-Geräte- und Funktionsnummer) geschehen; alle Werte werden normalerweise als hexadezimale Zahlen dargestellt.

Beispielsweise verwendet /proc/bus/pci/devices ein einziges 16-Bit-Feld (um das Parsen und Sortieren zu vereinfachen), während /proc/bus/busnummer die Adressen in drei Felder aufteilt. Hier können Sie sehen, wie diese Adressen erscheinen, wobei jeweils nur der Anfang der ausgegebenen Zeilen dargestellt ist:

rudo% lspci | cut -d: -f1-2
00:00.0 Host bridge
00:01.0 PCI bridge
00:07.0 ISA bridge
00:07.1 IDE interface
00:07.3 Bridge
00:07.4 USB Controller
00:09.0 SCSI storage controller
00:0b.0 Multimedia video controller
01:05.0 VGA compatible controller
rudo% cat /proc/bus/pci/devices | cut -d\        -f1,3
0000    0
0008    0
0038    0
0039    0
003b    0
003c    b
0048    a
0058    b
0128    a

Die beiden Gerätelisten sind in der gleichen Reihenfolge sortiert, weil lspci die /proc-Dateien als Informationsquelle verwendet. Wenn wir den VGA-Video-Controller als Beispiel nehmen, bedeutet 0x128 nach der Aufteilung in Bus (acht Bits), Gerät (fünf Bits) und Funktion (drei Bits) 01:05.0. Das zweite Feld in den beiden Listings zeigt die Geräteklasse beziehungsweise die Interrupt-Nummer.

Die Hardware-Schaltkreise auf jeder Peripherie-Karte beantworten Fragen nach den drei Adreßräumen: Speicheradressen, I/O-Ports und Konfigurationsregister. Die ersten beiden Adreßräume werden von allen Geräten auf einem PCI-Bus gemeinsam genutzt (d. h. wenn Sie auf eine Speicheradresse zugreifen, dann sehen alle Geräte den Bus-Zyklus zum selben Zeitpunkt). Der Konfigurationsraum benutzt dagegen eine geographische Adressierung. Konfigurationstransaktionen (also Bus-Adressen im Konfigurationsraum) betreffen nur jeweils einen Steckplatz. Daher gibt es beim Zugriff auf Konfigurationsinformationen überhaupt keine Kollisionen.

Was den Treiber angeht, wird auf Speicher- und I/O-Bereiche wie sonst auch mit inb, readb usw. zugegriffen. Konfigurationstransaktionen werden dagegen durch Aufruf bestimmter Kernel-Funktionen ausgeführt, um auf die Konfigurationsregister zuzugreifen. Im Hinblick auf Interrupts hat jeder PCI-Steckplatz vier Interrupt-Pins, und jede Gerätefunktion kann einen davon verwenden, ohne sich darum kümmern zu müssen, wie diese Pins dann mit der CPU verbunden sind. Das liegt in der Verantwortung der jeweiligen Plattform und wird außerhalb des PCI-Busses implementiert. Weil die PCI-Spezifikation erwartet, daß Interrupt-Leitungen gemeinsam genutzt werden können, kann selbst ein Prozessor mit einer beschränkten Anzahl von IRQ-Leitungen wie die x86-Plattform viele PCI-Karten enthalten (die alle vier Interrupt-Pins haben).

Der I/O-Adreßraum in einem PCI-Bus verwendet einen 32-Bit-Adreß-Bus (und kann damit 4 GByte I/O-Ports adressieren), während der Speicherraum entweder mit 32-Bit- oder 64-Bit-Adressen angesprochen werden kann. 64-Bit-Adressen werden aber nur auf einigen wenigen Plattformen unterstützt. Die Adressen müssen eindeutig zu einem Gerät zugeordnet sein, aber es kann passieren, daß die Software zwei Geräte fälschlicherweise auf die gleiche Adresse abbildet, so daß auf keines von beiden zugegriffen werden kann. Dies passiert aber nur, wenn ein Treiber absichtlich mit Registern herumspielt, die ihn nichts angehen. Glücklicherweise kann jeder Speicher- und I/O-Adreßbereich auf der Karte mit Hilfe von Konfigurationsvorgängen umgeblendet werden. Die Firmware initialisiert also die PCI-Hardware beim Booten des Systems und bildet jeden Bereich auf eine andere Adresse ab, um Kollisionen zu vermeiden.[2] Die Adressen, auf die diese Bereiche derzeit abgebildet werden, können aus dem Konfigurationsraum ausgelesen werden, so daß ein Linux-Treiber darauf ohne Suchen zugreifen kann. Nachdem die Konfigurationsregister einmal ausgelesen worden sind, kann der Treiber gefahrlos auf seine Hardware zugreifen.

Der PCI-Konfigurationsraum besteht aus 256 Bytes je Gerätefunktion, das Layout der Konfigurationsregister ist standardisiert. Vier Bytes des Konfigurationsraumes enthalten eine eindeutige Geräte-ID, so daß der Treiber sein Gerät durch einen Vergleich mit der spezifischen ID des Gerätes erkennen kann.[3] Um es zusammenzufassen: Jede Karte wird also geographisch adressiert, um an ihre Konfigurationsregister heranzukommen. Diese Information kann dann verwendet werden, um normale I/O durchzuführen, ohne weiter eine geographische Adressierung verwenden zu müssen.

Es sollte aus dieser Beschreibung klargeworden sein, daß die wichtigste Verbesserung des PCI-Standards gegenüber ISA der Konfigurationsadreßraum ist. Daher muß ein PCI-Treiber neben dem üblichen Treiber-Code auch auf den Konfigurationsraum zugreifen können, um sich selbst riskantes Ausprobieren zu ersparen.

Im Rest dieses Kapitels werden wir das Wort “Gerät” für eine Gerätefunktion verwenden, weil jede Funktion auf einer Multifunktionskarte sich wie eine unabhängige Einheit verhält. Wenn wir also von einem Gerät sprechen, meinen wir das Tupel aus Bus-Nummer, Gerätenummer und Funktionsnummer, das durch eine 16-Bit-Zahl oder zwei 8-Bit-Zahlen (die normalerweise bus und devfn heißen) repräsentiert wird.

Hochfahren des Systems

Schauen wir uns an, wie PCI funktioniert, und beginnen wir dabei mit dem Hochfahren des Systems, denn an dieser Stelle werden die Geräte konfiguriert.

Wenn Spannung an einem PCI-Gerät angelegt wird, bleibt die Hardware inaktiv. Mit anderen Worten: Das Gerät antwortet nur auf Konfigurationsvorgänge. Beim Einschalten bildet das Gerät keinen Speicher und keine I/O-Ports in den Adreßraum des Computers ab; alle anderen gerätespezifischen Merkmale wie das Melden von Interrupts sind ebenfalls abgeschaltet.

Glücklicherweise verfügt jede PCI-Hauptplatine über PCI-fähige Firmware, die je nach Plattform BIOS, NVRAM oder PROM heißt. Mit der Firmware kann durch das Lesen und Schreiben von Registern im PCI-Controller auf den Konfigurationsadreßraum des Gerätes zugegriffen werden.

Beim Hochfahren des Systems führt die Firmware (oder der Linux-Kernel, wenn er dafür konfiguriert ist) Konfigurationsvorgänge mit jedem PCI-Peripherie-Gerät durch, um für jeden Adreßbereich des Geräts einen sicheren Platz zu finden. Wenn der Gerätetreiber auf das Gerät zugreift, sind dessen Speicher und I/O-Bereiche bereits in den Adreßraum des Prozessors abgebildet worden. Der Treiber kann diese Default-Zuweisung ändern, wird das aber nie tun müssen.

Wie bereits erwähnt wurde, kann der Benutzer unter Linux die Liste der PCI-Geräte und die Konfigurationsregister des Gerätes in den Dateien /proc/bus/pci/devices und /proc/bus/pci/*/* einsehen. Erstere ist eine Textdatei mit hexadezimaler Geräteinformation, letzteres sind Binärdateien, die einen Schnappschuß der Konfigurationsregister mit einer Datei pro Gerät melden.

Konfigurationsregister und Initialisierung

Es wurde bereits gesagt, daß das Layout des Konfigurationsraumes geräteunabhängig ist. In diesem Abschnitt werden wir uns die Konfigurationsregister anschauen, mit denen die Peripherie-Geräte konfiguriert werden.

PCI-Geräte haben einen 256-Byte-Adreßraum. Die ersten 64 Bytes sind standardisiert, der Rest ist geräteabhängig. Abbildung 15-2 zeigt das Layout des geräteunabhängigen Konfigurationsraumes.

Abbildung 15-2. Die standardisierten PCI-Konfigurationsregister

Wie die Abbildung zeigt, sind einige der Konfigurationsregister obligatorisch und andere optional. Jedes PCI-Gerät muß in den obligatorischen Registern sinnvolle Werte enthalten, während der Inhalt der optionalen Register von den jeweiligen Fähigkeiten des Gerätes abhängig ist. Die optionalen Felder werden nur dann verwendet, wenn der Inhalt der obligatorischen Felder angibt, daß sie gültig sind. Die obligatorischen Felder bezeichnen also die Fähigkeiten der Karte: darunter auch, ob die anderen Felder verwendbar sind oder nicht.

Interessanterweise sind PCI-Register immer Little-Endian. Obwohl der Standard architekturunabhängig sein soll, tendieren die PCI-Entwickler manchmal ein wenig zur PC-Umgebung. Der Treiber-Programmierer muß daher vorsichtig mit der Byte-Reihenfolge sein, wenn auf mehr-bytige Konfigurationsregister zugegriffen wird. Code, der auf dem PC funktioniert, funktioniert auf anderen Plattformen möglicherweise nicht. Die Linux-Entwickler haben sich dieses Problems angenommen (siehe den nächsten Abschnitt “the Section called Zugriff auf den Konfigurationsraum”), es darf aber trotzdem nicht vergessen werden. Wenn Sie jemals Daten von der Host-Reihenfolge in die PCI-Reihenfolge oder umgekehrt konvertieren müssen, können Sie die in <asm/byteorder.h> definierten Funktionen verwenden, die in Kapitel 10 eingeführt wurden, weil Sie wissen, daß die PCI-Byte-Reihenfolge Little Endian ist.

Die Beschreibung aller Konfigurationselemente geht weit über den Umfang dieses Buches hinaus. Üblicherweise beschreibt die technische Dokumentation jedes Gerätes, welche Register unterstützt werden. Wir interessieren uns hier dafür, wie ein Treiber sein Gerät finden und auf dessen Konfigurationsraum zugreifen kann.

Ein Gerät wird durch drei oder fünf PCI-Register identifiziert: vendorID, deviceID und class werden immer verwendet. Jeder PCI-Hersteller weist diesen Registern nur lesbare Werte zu, und Treiber können sie verwenden, um nach einem Gerät zu suchen. Außerdem verwenden manche Hersteller die Felder subsystem vendorID und subsystem deviceID, um ähnliche Geräte voneinander zu unterscheiden.

Schauen wir uns diese Register etwas detaillierter an:

vendorID

Dieses 16-Bit-Register gibt den Hardware-Hersteller an. Beispielsweise hat jedes Intel-Gerät die gleiche vendor-Nummer, nämlich 0x8086 (ob das nur Zufall ist?). Es gibt eine globale Liste dieser Nummern, bei der sich Hersteller eine eindeutige Nummer zuweisen lassen müssen.

deviceID

Ein weiteres 16-Bit-Register, das der Hersteller auswählen kann. Für deviceID ist keine offizielle Registrierung notwendig. Diese ID wird normalerweise mit der Hersteller-ID zu einem eindeutigen 32-Bit-Identifikator für ein Gerät kombiniert. Wir verwenden im Folgenden den Begriff Signatur für das Paar aus vendorID und deviceID. Ein Gerätetreiber verläßt sich normalerweise auf die Signatur, um ein Gerät zu identifizieren; der Treiber-Programmierer weiß aus der Hardware-Dokumentation, nach welchem Wert er suchen muß.

class

Jedes Peripherie-Gerät gehört zu einer Klasse. Das class-Register ist ein 16-Bit-Wert, dessen obere acht Bits die “Basisklasse” (oder Gruppe) bezeichnen. Beispielsweise sind “Ethernet” und “Token Ring” zwei Klassen, die zur “Netzwerk”-Gruppe gehören, während die Klassen “seriell” und “parallel” zur Gruppe “Kommunikation” gehören. Manche Treiber können mehrere ähnliche Geräte unterstützen, die alle eine unterschiedliche Signatur haben, aber zur gleichen Klasse gehören. Diese Treiber können das class-Register verwenden, um (wie unten gezeigt wird) ihre Peripherie-Geräte zu identifizieren.

subsystem vendorID, subsystem deviceID

Diese Felder können zur weiteren Identifikation eines Geräts verwendet werden. Wenn der Chip selbst ein generischer Schnittstellen-Chip für einen lokalen Bus ist, dann wird dieser oft in völlig unterschiedlichen Rollen verwendet, und der Treiber muß herausfinden, mit welcher Hardware er eigentlich redet. Die Subsystem-Bezeichner sind dafür gedacht.

Mit diesen Bezeichnern können Sie Ihr Gerät finden. In der Kernel-Version 2.4 sind das Konzept eines PCI-Treibers und eine spezialisierte Initialisierungsschnittstelle eingeführt worden. Während diese Schnittstelle die bevorzugte für neue Treiber ist, steht sie für ältere Kernel-Versionen nicht zur Verfügung. Als Alternative zur PCI-Treiber-Schnittstelle können die folgenden Header-Dateien, Makros und Funktionen in PCI-Modulen verwendet werden, um nach Hardware-Geräten zu suchen. Wir haben uns dazu entschieden, diese abwärtskompatible Schnittstelle zuerst einzuführen, weil sie portabel über alle in diesem Buch behandelten Kernel-Versionen ist. Außerdem ist sie etwas zugänglicher, weil sie weniger stark von der direkten Hardware-Verwaltung abstrahiert.

#include <linux/config.h>

Der Treiber muß wissen, welche PCI-Funktionen im Kernel zur Verfügung stehen. Indem er diese Header-Datei einbindet, bekommt er Zugriff auf die CONFIG_-Makros einschließlich CONFIG_PCI, das unten beschrieben wird. Beachten Sie aber, daß jede Quelldatei, die <linux/module.h> einbindet, diese Header-Datei automatisch mit importiert.

CONFIG_PCI

Dieses Makro ist definiert, wenn der Kernel PCI-Aufrufe unterstützt. Nicht jeder Computer verfügt über einen PCI-Bus, weswegen die Kernel-Entwickler sich entschieden haben, PCI zu einer zur Kompilierzeit auswählbaren Option zu machen, um Speicher zu sparen, wenn Linux auf Nicht-PCI-Computern läuft. Wenn CONFIG_PCI nicht eingeschaltet ist, dann sind alle PCI-Funktionsaufrufe so definiert, daß sie einen Fehler zurückmelden. Auf diese Weise kann der Treiber Präprozessor-Anweisungen verwenden, um die PCI-Unterstützung herauszunehmen, muß das aber nicht tun. Wenn der Treiber nur mit PCI-Geräten (und nicht mit anderen) umgeht, dann sollte er einen Compile-Fehler erzeugen, wenn dieses Makro nicht definiert ist.

#include <linux/pci.h>

Diese Header-Datei deklariert alle in diesem Abschnitt eingeführten Prototypen sowie die zu PCI-Registern und -Bits gehörenden symbolischen Namen. Sie sollte immer eingebunden werden und definiert auch symbolische Werte für von den Funktionen zurückgegebene Fehler-Codes.

int pci_present(void);

Weil die PCI-Funktionen auf Nicht-PCI-Computern keinen Sinn ergeben, teilt die Funktion pci_present dem Treiber mit, ob der Rechner PCI unterstützt. Dieser Aufruf sollte in 2.4 nicht mehr verwendet werden, weil er nur noch überprüft, ob sich überhaupt ein PCI-Gerät finden läßt. In 2.0 mußten Treiber aber diese Funktion aufrufen, um unangenehme Fehler beim Suchen nach Geräten zu vermeiden. Neuere Kernel melden einfach, daß es kein Gerät gibt. Die Funktion gibt den Booleschen Wert true zurück, wenn der Host PCI-fähig ist.

struct pci_dev;

Diese Datenstruktur wird als Software-Objekt verwendet, das ein PCI-Gerät repräsentiert. Sie liegt jeder PCI-Operation im System zugrunde.

struct pci_dev *pci_find_device (unsigned int vendor, unsigned int device, const struct pci_dev *from);

Wenn CONFIG_PCI definiert und pci_present true ist, wird diese Funktion verwendet, um die Liste der installierten Geräte nach einem Gerät mit einer bestimmten Signatur zu durchsuchen. Das Argument from wird verwendet, um mehrere Geräte mit der gleichen Signatur zu finden; dieses Argument sollte auf das letzte gefundene Gerät verweisen, so daß die Suche dort fortgesetzt werden kann, anstatt wieder am Anfang der Liste anzufangen. Um das erste Gerät zu finden, wird from als NULL angegeben. Wenn kein (weiteres) Gerät gefunden wird, wird NULL zurückgegeben.

struct pci_dev *pci_find_class (unsigned int class, const struct pci_dev *from);

Diese Funktion ähnelt der letzten, sucht aber nach Geräten einer bestimmten Klasse (die in 16 Bit angegeben wird, sowohl mit der Basisklasse als auch mit der Subklasse). Dies wird heutzutage außer in PCI-Treibern sehr niedriger Ebene selten verwendet. Das from-Argument wird genau wie bei pci_find_device verwendet.

int pci_enable_device(struct pci_dev *dev);

Diese Funktion schaltet das Gerät erst ein. Sie weckt das Gerät und weist in manchen Fällen auch eine Interrupt-Leitung und I/O-Bereiche zu. Dies passiert beispielsweise bei CardBus-Geräten (die auf Treiber-Ebene vollständig äquivalent mit PCI sind).

struct pci_dev *pci_find_slot (unsigned int bus, unsigned int devfn);

Diese Funktion gibt eine PCI-Gerätestruktur anhand eines Bus/Geräte-Paares zurück. Das Argument devfn repräsentiert sowohl das Gerät als auch die Funktion. Dies wird nur sehr selten verwendet (Treiber sollten sich nicht dafür interessieren, in welchem Steckplatz ihr Gerät sitzt) und ist hier nur der Vollständigkeit halber aufgeführt.

Auf der Basis dieser Informationen sieht die Initialisierung eines typischen Gerätetreibers, der nur einen Gerätetyp unterstützt, wie im folgenden Code aus. Der Code ist für ein hypothetisches Gerät namens jail geschrieben, was für Just Another Instruction List steht:


#ifndef CONFIG_PCI
#  error "This driver needs PCI support to be available"
#endif

int jail_find_all_devices(void)
{
    struct pci_dev *dev = NULL;
    int found;

    if (!pci_present())
    return -ENODEV;

    for (found=0; found < JAIL_MAX_DEV;) {
        dev = pci_find_device(JAIL_VENDOR, JAIL_ID, dev);
        if (!dev) /* keine weiteren Geraete */
            break;
        /* geraetespezifische Aktionen ausfuehren, Geraet zaehlen */
        found += jail_init_one(dev);
    }
    return  (index == 0) ? -ENODEV : 0;
}

Die Aufgabe von jail_init_one ist sehr gerätespezifisch, weswegen diese Funktion hier nicht gezeigt wird. Es gibt aber einiges, das man im Kopf behalten sollte, wenn man diese Funktion schreibt:

  • Die Funktion muß möglicherweise zusätzliche Überprüfungen vornehmen, um sicherzustellen, daß das Gerät wirklich eines der unterstützten Geräte ist. Manche PCI-Peripherie-Geräte enthalten einen allgemein verwendbaren PCI-Schnittstellen-Chip und gerätespezifische Schaltkreise. Jedes Peripherie-Gerät, das den gleichen Schnittstellen-Chip verwendet, hat die gleiche Signatur. Weitere Überprüfungen können entweder durch Auslesen der Subsystem-Bezeichner oder durch Auslesen spezifischer Geräteregister (in den später beschriebenen Geräte-I/O-Regionen) vorgenommen werden.

  • Bevor man auf Geräteressourcen (I/O-Bereiche oder Interrupts) zugreift, muß der Treiber pci_enable_device aufrufen. Wenn bei den gerade besprochenen zusätzlichen Überprüfungen auf Gerätespeicher oder I/O-Bereiche zugegriffen werden muß, muß die Funktion vorher aufgerufen werden.

  • Ein Netzwerk-Treiber sollte sicherstellen, daß dev->driver_data auf die zu dieser Schnittstelle gehörende struct net_device zeigt.

Die im obenstehenden Code gezeigte Funktion gibt 0 zurück, wenn das Gerät nicht unterstützt wird, und 1, wenn es akzeptiert wird (möglicherweise auf Basis weiterer Überprüfungen, wie gerade beschrieben).

Der gezeigte Code-Schnipsel ist korrekt, wenn der Treiber es nur mit einer Art von PCI-Gerät zu tun hat, die durch JAIL_VENDOR und JAIL_ID identifiziert wird. Wenn Sie weitere Hersteller/Geräte-Paare unterstützen müssen, dann sollten Sie am besten die später in “the Section called Hardware-Abstraktionen” beschriebene Technik verwenden, sofern Sie keine älteren Kernel unterstützen müssen: In diesem Fall wäre pci_find_class die Funktion der Wahl.

Die Verwendung von pci_find_class verlangt, daß jail_find_all_devices etwas mehr Arbeit als im Beispiel gezeigt leistet. Die Funktion sollte das frisch vorgefundene Gerät mit einer Liste von Hersteller/Geräte-Paaren vergleichen, möglicherweise anhand von dev->vendor und dev->device. Das könnte so aussehen:


struct devid {unsigned short vendor, device} devlist[] = {
    {JAIL_VENDOR1, JAIL_DEVICE1},
    {JAIL_VENDOR2, JAIL_DEVICE2},
    /* ... */
    { 0, 0 }
};

    /* ... */

    for (found=0; found < JAIL_MAX_DEV;) {
        struct devid *idptr;
        dev = pci_find_class(JAIL_CLASS, dev);
        if (!dev) /* keine weiteren Geraete */
            break;
        for (idptr = devlist; idptr->vendor; idptr++) {
           if (dev->vendor != idptr->vendor) continue;
           if (dev->device != idptr->device) continue;
           break;
        }
        if (!idptr->vendor) continue; /* keines von unseren */
        jail_init_one(dev); /* geraetespezifische Initialisierung */
        found++;
    }

Zugriff auf den Konfigurationsraum

Nachdem der Treiber das Gerät entdeckt hat, muß er normalerweise drei Adreßräume auslesen bzw. diese beschreiben: den Speicher, den Port und die Konfiguration. Insbesondere der Zugriff auf den Konfigurationsraum ist für den Treiber sehr wichtig, weil er nur so herausfinden kann, wo das Gerät in den Speicher und in den I/O-Raum eingeblendet wird.

Weil der Mikroprozessor keine Möglichkeit hat, direkt auf den Konfigurationsraum zuzugreifen, muß der Computer-Hersteller dafür eine Möglichkeit bereitstellen. Um auf den Konfigurationsraum zuzugreifen, muß die CPU Register im PCI-Controller lesen und schreiben. Die genaue Implementation dafür ist herstellerabhängig und hier nicht relevant, weil Linux eine Standardschnittstelle für den Zugriff auf den Konfigurationsraum enthält.

Was den Treiber angeht, kann auf den Konfigurationsraum mit 8-Bit-, 16-Bit- und 32-Bit-Datenübertragungen zugegriffen werden. Die Prototypen der relevanten Funktionen stehen in <linux/pci.h>:

int pci_read_config_byte(struct pci_dev *dev, int where, u8 *ptr);, int pci_read_config_word(struct pci_dev *dev, int where, u16 *ptr);, int pci_read_config_dword(struct pci_dev *dev, int where, u32 *ptr);

Eines, zwei oder vier Bytes aus dem Konfigurationsraum des durch dev bezeichneten Geräts auslesen. Das Argument where ist der Byte-Offset vom Anfang des Konfigurationsraums. Der aus dem Konfigurationsraum geholte Wert wird über ptr zurückgegeben; der Rückgabewert dieser Funktionen ist ein Fehler-Code. Die word- und dword-Funktionen konvertieren den gerade gelesenen Wert aus der Little-Endian-Byte-Reihenfolge in die native Byte-Reihenfolge des Prozessors, damit Sie sich nicht darum kümmern brauchen.

int pci_write_config_byte (struct pci_dev *dev, int where, u8 val);, int pci_write_config_word (struct pci_dev *dev, int where, u16 val);, int pci_write_config_dword (struct pci_dev *dev, int where, u32 val);

Ein, zwei oder vier Bytes in den Konfigurationsraum schreiben. Wie immer bezeichnet dev das Gerät, und der zu schreibende Wert wird in val übergeben. Die word- und dword-Funktionen wandeln diesen Wert erst in Little-Endian um, bevor sie ihn auf das Peripheriegerät rausschreiben.

Der bevorzugte Weg zum Lesen der benötigten Konfigurationsvariablen führt über die Felder in der struct pci_dev Ihres Geräts. Gleichwohl brauchen Sie aber die gerade genannten Funktionen, wenn Sie eine Konfigurationsvariable schreiben und zurücklesen müssen. Außerdem brauchen Sie die pci_read_-Funktionen für die Abwärtskompatibilität mit Kerneln vor 2.4.[4]

Die beste Möglichkeit, mit den pci_read_-Funktionen auf die Konfigurationsvariablen zuzugreifen, ist die Verwendung der symbolischen Namen, die in <linux/pci.h> definiert sind. Beispielsweise holt der folgende Zweizeiler die Revisions-ID eines Gerätes, indem er den symbolischen Namen im Argument where von pci_read_config_byte übergibt.


unsigned char jail_get_revision(unsigned char bus, unsigned char fn)
{
    unsigned char *revision;

    pci_read_config_byte(bus, fn, PCI_REVISION_ID, &revision);
    return revision;
}

Wenn Sie auf Mehr-Byte-Werte zugreifen, müssen Sie wie bereits erwähnt daran denken, auf Probleme mit der Byte-Reihenfolge zu achten.

Einen Konfigurations-Snapshot ansehen

Wenn Sie den Konfigurationsraum der PCI-Geräte auf Ihrem System durchsuchen wollen, können Sie auf zweierlei Art und Weise vorgehen. Einfacher ist es, die Ressourcen zu verwenden, die Linux bereits in /proc/bus/pci bereitstellt, auch wenn diese in der Kernel-Version 2.0 noch nicht zur Verfügung standen. Die Alternative, die wir hier verfolgen, ist statt dessen das Schreiben eigenen Codes; dieser Code ist portabel über alle bekannten 2.x-Kernel-Versionen und eine gute Möglichkeit, die Werkzeuge in Aktion zu sehen. Die Quelldatei pci/pcidata.c finden Sie im Beispiel-Code auf dem FTP-Server von O'Reilly.

Dieses Modul erzeugt eine dynamische Datei namens /proc/pcidata, die einen binären Snapshot des Konfigurationsraumes Ihrer PCI-Geräte enthält. Dieser Snapshot wird jedesmal aktualisiert, wenn aus dieser Datei gelesen wird. Die Größe von /proc/pcidata ist auf PAGE_SIZE Bytes beschränkt (eine Einschränkung, die aus den dynamischen /proc-Dateien, die in “the Section called Das /proc-Dateisystem verwenden in Kapitel 4” in Kapitel 4 eingeführt wurden, herrührt). Daher wird nur der Konfigurationsspeicher für die ersten PAGE_SIZE/256 Geräte ausgegeben, also je nach verwendeter Plattform für 16 oder 32 Geräte. Wir haben uns dazu entschlossen, /proc/pcidata binär zu halten, anstatt daraus eine Textdatei wie die anderen /proc-Dateien zu machen. Beachten Sie, daß auch die Dateien in /proc/bus/pci binär sind.

Eine weitere Beschränkung von pcidata besteht darin, daß das Programm nur den ersten PCI-Bus im System absucht. Wenn Ihr Computer Brücken zu anderen PCI-Bussen enthält, werden diese von pcidata ignoriert. Dies sollte kein Problem in Beispiel-Code sein, der nicht für eine echte Verwendung gedacht ist.

Geräte stehen in /proc/pcidata in der gleichen Reihenfolge wie in /proc/bus/pci/devices (aber in umgekehrter Reihenfolge als in der Reihenfolge, die im 2.0-Kernel in /proc/pci verwendet wird).

Beispielsweise erscheint unser Framegrabber zweimal in /proc/pcidata und hat (derzeit) die folgenden Konfigurationsregister:


morgana% dd bs=256 skip=1 count=1 if=/proc/pcidata | od -Ax -t x1
1+0 records in
1+0 records out
000000 86 80 23 12 06 00 00 02 00 00 00 04 00 20 00 00
000010 00 00 00 f1 00 00 00 00 00 00 00 00 00 00 00 00
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000030 00 00 00 00 00 00 00 00 00 00 00 00 0a 01 00 00
000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
000100

Die Zahlen in diesem Auszug repräsentieren die PCI-Register. Anhand von Abbildung 15-2 können Sie die Zahlen entschlüsseln. Alternativ können Sie auch das Programm pcidump, das Sie ebenfalls auf dem FTP-Server finden, verwenden, das die Ausgabe formatiert und beschriftet.

Es lohnt sich nicht, den Code von pcidump hier anzugeben, weil das Programm nur aus einer langen Tabelle sowie zehn Codezeilen besteht, die die Tabelle durchsuchen. Schauen wir uns statt dessen einige ausgewählte Ausgabezeilen an:


morgana% dd bs=256 skip=1 count=1 if=/proc/pcidata | ./pcidump
1+0 records in
1+0 records out
        Compulsory registers:
Vendor id: 8086
Device id: 1223
I/O space enabled: n
Memory enabled: y
Master enabled: y
Revision id (decimal): 0
Programmer Interface: 00
Class of device: 0400
Header type: 00
Multi function device: n
        Optional registers:
Base Address 0: f1000000
Base Address 0 Is I/O: n
Base Address 0 is 64-bits: n
Base Address 0 is below-1M: n
Base Address 0 is prefetchable: n
Does generate interrupts: y
Interrupt line (decimal): 10
Interrupt pin (decimal): 1

pcidata und pcidump können zusammen mit grep hilfreiche Werkzeuge zum Debuggen des Initialisierungscodes eines Treibers sein, selbst wenn ihre Aufgabe bereits durch das in allen neueren Linux-Distributionen enthaltene Paket pciutils übernommen wird. Beachten Sie aber, daß das Modul pcidata.c im Gegensatz zum sonstigen Code in diesem Buch der GPL unterliegt, weil wir die Suchschleife nach PCI-Geräten aus den Kernel-Quellen entnommen haben. Das sollte Sie als Treiber-Programmierer aber nicht stören, weil wir das Modul in den Quellen nur als Hilfsprogramm aufgenommen haben, nicht als einen Programmcode, der in neuen Treibern weiterverwendet werden soll.

Zugriff auf die I/O- und Speicherräume

Ein PCI-Peripherie-Gerät implementiert bis zu sechs Adreßregionen. Jede Region besteht entweder aus Speicher- oder I/O-Bereichen. Die meisten Geräte implementieren ihre I/O-Register in Speicherbereiche, weil das generell sinnvoller ist (wie in “the Section called I/O-Ports und I/O-Speicher in Kapitel 8” in Kapitel 8 erläutert wird). Im Gegensatz zu normalem Speicher sollten aber I/O-Register nicht von der CPU zwischengespeichert werden, weil jeder Zugriff Nebeneffekte haben kann. PCI-Geräte, die I/O-Register als Speicherbereiche implementieren, kennzeichnen den Unterschied dadurch, daß sie ein “memory-is-prefetchable”-Bit in ihrem Konfigurationsregister setzen.[5] Wenn der Speicherbereich so markiert ist, kann die CPU den Inhalt zwischenspeichern und beliebig optimieren, ansonsten darf der Zugriff nicht optimiert werden, weil jeder Zugriff Nebeneffekte haben kann, genau wie das normalerweise bei I/O-Ports der Fall ist. Peripherie-Geräte, die ihre Kontrollregister auf einen Adreßbereich im Speicher abbilden, deklarieren diesen Speicherbereich als “non-prefetchable”, während so etwas wie der Videospeicher auf PCI-Karten “prefetchable” ist. In diesem Abschnitt verwenden wir den Begriff “Region”, um uns damit auf einen PCI-Adreßbereich zu beziehen — unabhängig davon, ob es sich um Speicher oder I/O handelt.

Eine Schnittstellen-Karte meldet die Größe und aktuelle Lage ihrer Regionen mittels Konfigurationsregistern, genauer gesagt durch die sechs 32-Bit-Register, die in Abbildung 15-2 zu sehen sind und deren symbolische Namen PCI_BASE_ADDRESS_0 bis PCI_BASE_ADDRESS_5 lauten. Weil der von PCI definierte I/O-Raum ein 32-Bit-Adreßraum ist, ist es sinnvoll, die gleiche Konfigurationsschnittstelle für Speicher und I/O zu verwenden. Wenn das Gerät einen 64-Bit-Adreß-Bus benutzt, kann es Regionen im 64-Bit-Speicherraum deklarieren, indem es für jede Region zwei aufeinanderfolgende PCI_BASE_ADDRESS-Register verwendet. Ein Gerät kann sowohl 32-Bit- als auch 64-Bit-Regionen anbieten.

PCI-I/O-Ressourcen in Linux 2.4

In Linux 2.4 sind die I/O-Bereiche von PCI-Geräten in die allgemeine Ressourcen-Verwaltung integriert worden. Aus diesem Grund müssen Sie nicht mehr auf die Konfigurationsvariablen zugreifen, um zu wissen, wohin in den Speicher oder I/O-Bereich Ihr Gerät eingeblendet worden ist. Die bevorzugte Schnittstelle für das Erfragen von Bereichsinformationen besteht aus den folgenden Funktionen:

unsigned long pci_resource_start(struct pci_dev *dev, int bar);

Diese Funktion gibt die erste Adresse (Speicheradresse oder I/O-Port-Nummer) zurück, die zu einem der sechs PCI-I/O-Bereiche gehört. Der Bereich wird durch den Integer-Wert bar (Base Address Register) ausgewählt und kann zwischen 0 und 5 (einschließlich) liegen.

unsigned long pci_resource_end(struct pci_dev *dev, int bar);

Diese Funktion gibt die letzte Adresse zurück, die zum I/O-Bereich bar gehört. Beachten Sie, daß dies die letzte verwendbare Adresse ist, nicht die erste Adresse nach dem Bereich.

unsigned long pci_resource_flags(struct pci_dev *dev, int bar);

Diese Funktion gibt die zu dieser Ressource gehörenden Flags zurück.

Ressourcen-Flags werden verwendet, um einige Merkmale der einzelnen Ressourcen zu definieren. Für PCI-Ressourcen, die zu PCI-I/O-Speicherbereichen gehören, wird die Information aus den Basis-Adreßregistern extrahiert, sie kann aber für andere Ressourcen auch aus anderen Quellen stammen.

Alle Ressourcen-Flags sind in <linux/ioport.h> definiert. Wir nennen hier nur die wichtigsten:

IORESOURCE_IO, IORESOURCE_MEM

Wenn der zugehörige I/O-Bereich existiert, wird genau eines dieser Flags gesetzt.

IORESOURCE_PREFETCH, IORESOURCE_READONLY

Diese Flags sagen aus, ob ein Speicher-Bereich prefetchable und/oder schreibgeschützt ist. Das zweite Flag wird für PCI-Ressourcen nie gesetzt.

Durch Verwendung der pci_resource_-Funktionen kann ein Gerätetreiber die zugrundeliegenden PCI-Register vollständig ignorieren, weil das System diese bereits zur Strukturierung der Ressourcen-Informationen verwendet hat.

Ein Blick auf die Basis-Adreßregister

Wenn Sie den direkten Zugriff auf die PCI-Register vermeiden, bekommen Sie eine bessere Hardware-Abstraktion und Aufwärtskompatibilität, aber keine Abwärtskompatibilität. Wenn Ihr Gerätetreiber mit älteren Linux-Versionen als 2.4 arbeiten soll, können Sie diese schöne Schnittstelle nicht verwenden, sondern müssen direkt mit den PCI-Registern arbeiten.

In diesem Abschnitt schauen wir uns an, wie sich Basis-Adreßregister verhalten und wie man darauf zugreifen kann. All dies ist natürlich überflüssig, wenn Sie die oben gezeigte Ressourcen-Verwaltung verwenden können.

Wir werden hier nicht zu sehr auf die Details der Basis-Adreßregister eingehen, denn wenn Sie einen PCI-Treiber schreiben wollen, dann brauchen Sie das Hardware-Handbuch für das Gerät sowieso. Insbesondere werden wir hier weder das Prefetchable-Bit verwenden noch die beiden “Typ”-Bits der Register, und wir werden uns in der Diskussion auch auf 32-Bit-Peripherie-Geräte beschränken. Trotzdem ist es interessant zu sehen, wie solche Sachen im allgemeinen Fall implementiert werden und wie die Linux-Treiber mit PCI-Speicher umgehen.

Die PCI-Spezifikation legt fest, daß die Hersteller jede implementierte Region auf eine konfigurierbare Adresse abbilden müssen. Das bedeutet, daß das Gerät für jede implementierte Region einen programmierbaren 32-Bit-Adreßdecoder haben muß und daß es außerdem auf jedem Gerät, das die 64-Bit-PCI-Erweiterung verwendet, einen programmierbaren 64-Bit-Adreßdecoder geben muß.

Die eigentliche Implementation und die Verwendung eines programmierbaren Decoders werden dadurch erleichtert, daß die Anzahl der Bytes in einer Region normalerweise eine Zweierpotenz ist, also zum Beispiel 32 Bytes, 4 KBytes oder 2 MBytes. Außerdem wäre es nicht sehr sinnvoll, eine Region an eine nicht ausgerichtete Adresse abzubilden. 1-MByte-Regionen werden natürlicherweise an einer Adresse ausgerichtet, die ein Vielfaches von 1 MByte ist, und 32-Byte-Regionen an einem Vielfachen von 32. Die PCI-Spezifikation nutzt diese Ausrichtung aus. Sie legt fest, daß der Adreßdecoder nur die höherwertigen Bits auf dem Adreß-Bus ansehen darf und daß nur diese auch programmierbar sind. Diese Konvention bringt es auch mit sich, daß die Größe jeder Region eine Zweierpotenz sein muß.

Das Einblenden einer PCI-Region in den physikalischen Adreßraum erfolgt dadurch, daß ein passender Wert in den höherwertigen Bits eines Konfigurationsregisters gesetzt wird. Beispielsweise wird eine 1-MByte-Region, die einen Adreßraum von 20 Bits hat, umgeblendet, indem die zwölf höherwertigen Bits des Registers gesetzt werden. Um also die Karte auf den Adreßbereich von 64 MByte bis 65 MByte reagieren zu lassen, können Sie in das Register jede Adresse im 0x040xxxxx-Bereich schreiben. In der Praxis werden nur sehr hohe Adressen verwendet, um PCI-Regionen einzublenden.

Diese Technik der “teilweisen Decodierung” hat den zusätzlichen Vorteil, daß die Software jetzt die Größe einer PCI-Region bestimmen kann, indem sie die Anzahl der nicht-programmierbaren Bits im Konfigurationsregister ermittelt. Zu diesem Zweck legt der PCI-Standard fest, daß unbenutzte Bits immer als 0 ausgelesen werden. Indem eine minimale Größe von 8 Bytes für I/O-Regionen und 16 Bytes für Speicherregionen festgelegt wird, kann der Standard einige zusätzliche Informationen im gleichen PCI-Register unterbringen:

  • Bit 0 ist das “Space”-Bit. Es ist 0, wenn der Bereich in den Speicher-Adreßraum eingeblendet wird, und 1, wenn der Bereich in den I/O-Adreßraum eingeblendet wird.

  • Die Bits 1 und 2 sind die “Typen”-Bits: Speicherbereiche können als 32-Bit-Bereiche, 64-Bit-Bereiche oder als “32 Bit-Bereiche, die unterhalb von 1 MByte eingeblendet werden müssen” (eine veraltete, x86-spezifische Idee; wird nicht mehr verwendet) markiert werden.

  • Bit 3 ist das “prefetchable” Bit, das für Speicherbereiche verwendet wird.

Das Ermitteln der Größe einer PCI-Region wird durch die Verwendung mehrerer Bitmasken erleichtert, die in <linux/pci.h> definiert sind: PCI_BASE_ADDRESS_SPACE ist eine Bitmaske, die auf PCI_BASE_ADDRESS_SPACE_MEMORY gesetzt ist, wenn es sich um einen Speicherbereich handelt, sowie auf PCI_BASE_ADDRESS_SPACE_IO, wenn es sich um einen I/O-Bereich handelt. Um die tatsächliche Adresse herauszufinden, an der ein Speicherbereich eingeblendet ist, können Sie das PCI-Register mit PCI_BASE_ADDRESS_MEM_MASK logisch UND-verknüpfen, um die oben genannten unteren Bits zu verwerfen. Für I/O-Bereiche verwenden Sie entsprechend PCI_BASE_ADDRESS_IO_MASK. Beachten Sie, daß PCI-Regionen von Geräte-Herstellern in beliebiger Reihenfolge alloziert werden können; es ist also nicht ungewöhnlich, daß ein Gerät den ersten und dritten Bereich verwendet und den zweiten unbenutzt läßt.

Typischer Code zum Ermitteln der aktuellen Lage und Größe der PCI-Regionen sieht etwa so aus wie der Code im pciregions-Modul, das Sie im gleichen Verzeichnis wie pcidata finden. Dieses Modul erzeugt unter Verwendung des oben gezeigten Codes zum Erzeugen der Daten eine /proc/pciregions-Datei. Das Programm schreibt Einsen in alle Konfigurationsregister und liest diese wieder aus, um herauszufinden, wie viele Bits der Register programmiert werden können. Während das Programm die Konfigurationsregister antestet, ist das Gerät am oberen Ende des physikalischen Adreßraumes eingeblendet, weswegen das Melden von Interrupts währenddessen abgeschaltet ist (damit kein Treiber auf den Bereich zugreift, während dieser an der falschen Stelle eingeblendet ist).

Obwohl die PCI-Spezifikationen angeben, daß der I/O-Adreßraum 32 Bits breit ist, tun einige Hersteller in ihrer x86-zentrischen Sicht so, als wären es nur 64 KByte und implementieren nicht alle 32 Bits des Basis-Adreßregisters. Deswegen ignoriert der folgende Code (und der Kernel selbst) die hohen Bits der Adreßmaske für I/O-Bereiche:


 
static u32 addresses[] = {
    PCI_BASE_ADDRESS_0,
    PCI_BASE_ADDRESS_1,
    PCI_BASE_ADDRESS_2,
    PCI_BASE_ADDRESS_3,
    PCI_BASE_ADDRESS_4,
    PCI_BASE_ADDRESS_5,
    0
};

int pciregions_read_proc(char *buf, char **start, off_t offset,
                   int len, int *eof, void *data)
{
    /* Dieses Makro haelt die folgenden Zeilen kurz. */
#define PRINTF(fmt, args...) sprintf(buf+len, fmt, ## args)
    len=0;

    /* Die Geraete durchlaufen (Code hier nicht abgedruckt) */

        /* Die Adreßbereiche dieses Geraets ausgeben */
        for (i=0; addresses[i]; i++) {
            u32 curr, mask, size;
            char *type;

            pci_read_config_dword(dev, addresses[i],&curr);
            cli();
            pci_write_config_dword(dev, addresses[i],˜0);
            pci_read_config_dword(dev, addresses[i],&mask);
            pci_write_config_dword(dev, addresses[i],curr);
            sti();

            if (!mask)
                continue; /* es kann noch andere Bereiche geben */

            /*
             * Die I/O- oder Speichermaske auf die aktuelle Position
             * anwenden. Beachten Sie, daß I/O auf 0xffff beschraenkt
             * ist und 64 Bit von dieser einfachen Implementation
             * nicht unterstuetzt werden.
             */
            if (curr & PCI_BASE_ADDRESS_SPACE_IO) {
                curr &= PCI_BASE_ADDRESS_IO_MASK;
            } else {
                curr &= PCI_BASE_ADDRESS_MEM_MASK;
            }

            len += PRINTF("\tregion %i: mask 0x%08lx, now at 0x%08lx\n",
                        i, (unsigned long)mask,
                           (unsigned long)curr);
            /* den Typ und die programmierbaren Bits extrahieren */
            if (mask & PCI_BASE_ADDRESS_SPACE_IO) {
                type = "I/O"; mask &= PCI_BASE_ADDRESS_IO_MASK;
                size = (˜mask + 1) & 0xffff; /* Bleah */
            } else {
                type = "mem"; mask &= PCI_BASE_ADDRESS_MEM_MASK;
                size = ˜mask + 1;
            }
            len += PRINTF("\tregion %i: type %s, size %i (%i%s)\n", i,
                          type, size,
                          (size & 0xfffff) == 0 ? size >> 20 :
                            (size & 0x3ff) == 0 ? size >> 10 : size,
                          (size & 0xfffff) == 0 ? "MB" :
                            (size & 0x3ff) == 0 ? "KB" : "B");
            if (len > PAGE_SIZE / 2) {
                len += PRINTF("... more info skipped ...\n");
                *eof = 1; return len;
            }
        }
    return len;
}



Hier sehen Sie als Beispiel, was /proc/pciregion für unseren Framegrabber meldet:



Bus 0, device 13, fun  0 (id 8086-1223)
        region 0: mask 0xfffff000, now at 0xf1000000
        region 0: type mem, size 4096

Interessanterweise kann die vom obigen Programm gemeldete Größe des Speichers übertrieben sein. Beispielsweise meldet /proc/pciregions, daß unsere Videokarte 16 MByte hat, wo es eigentlich nur 1 MByte ist. Diese Lüge ist aber akzeptabel, weil die Größeninformation nur von der Firmware verwendet wird, um Adreßbereiche zu verwenden. Übergroße Bereiche sind kein Problem für Treiber-Autoren, die die internen Details des Geräts kennen und mit dem von der Firmware zugewiesenen Adreßbereich korrekt umgehen könnnen. In diesem Fall könnte später Geräte-RAM hinzugefügt werden, ohne daß währenddessen das Verhalten der PCI-Register verändert werden müßte.

Ein solches Übertreiben bei der Größenangabe spiegelt sich in der Ressourcen-Schnittstelle wieder; pci_resource_size meldet die übertriebene Größe.

PCI-Interrupts

Was die Interrupts angeht, ist PCI einfach zu benutzen. Wenn Linux bootet, hat die Firmware des Rechners dem Gerät bereits eine eindeutige Interrupt-Nummer zugeteilt, und der Treiber muß diese einfach nur noch verwenden. Die Interrupt-Leitung wird im Konfigurationsregister 60 (PCI_INTERRUPT_LINE) abgelegt, das ein Byte breit ist und damit bis zu 256 Interrupt-Leitungen erlaubt, auch wenn die tatsächliche Obergrenze von der verwendeten CPU abhängt. Der Treiber muß sich nicht darum kümmern, die Interrupt-Nummer zu überprüfen, denn der Wert in PCI_INTERRUPT_LINE ist garantiert richtig.

Wenn das Gerät keine Interrupts unterstützt, dann ist das Register 61 (PCI_INTERRUPT_PIN) 0, ansonsten hat es einen von Null verschiedenen Wert. Da der Treiber aber weiß, ob ein Gerät Interrupt-gesteuert ist oder nicht, muß er normalerweise PCI_INTERRUPT_PIN nicht auslesen.

PCI-spezifischer Code muß also nur zur Verwaltung von Interrupts das Konfigurationsbyte auslesen, um die Interrupt-Nummer zu bekommen, die in einer lokalen Variable gespeichert wird, wie der folgende Code zeigt. Ansonsten gilt weiterhin das in Kapitel 9 Gesagte.


result = pci_read_config_byte(dev, PCI_INTERRUPT_LINE, &myirq);
if (result) { /* Fehler behandeln */ }

Der Rest dieses Abschnitts enthält zusätzliche Informationen für neugierige Leser, ist aber zur Programmierung von Treibern nicht weiter notwendig.

Ein PCI-Steckverbinder hat vier Pins für Interrupts, und eine Peripherie-Karte kann beliebige davon verwenden. Jeder Pin wird einzeln zum Interrupt-Controller der Hauptplatine durchgeführt, so daß Interrupts ohne elektrische Probleme gemeinsam genutzt werden können. Der Interrupt-Controller ist dafür verantwortlich, die Interrupt-Drähte (-Anschlüsse) mit der Hardware des Prozessors zu verbinden. Diese plattformabhängige Operation wird dem Controller überlassen, damit der Bus selbst plattformunabhängig bleibt.

Das nur-lesbare Konfigurationsregister bei PCI_INTERRUPT_PIN wird verwendet, um dem Computer mitzuteilen, welcher Pin eigentlich verwendet wird. Man sollte nicht vergessen, daß jede Karte bis zu acht Geräte enthalten kann; jedes Gerät benutzt einen einzelnen Interrupt-Pin und teilt diesen über sein eigenes Konfigurationsregister mit. Verschiedene Geräte auf derselben Karte können verschiedene Interrupt-Pins verwenden oder einen gemeinsam nutzen.

Das Register PCI_INTERRUPT_LINE kann dagegen auch beschrieben werden. Wenn der Computer hochgefahren wird, sucht die Firmware die PCI-Geräte und setzt dieses Register für jedes Gerät entsprechend dem Interrupt-Pin, der für diesen PCI-Slot verwendet wird. Der Wert wird von der Firmware zugewiesen, weil nur diese weiß, wie die Hauptplatine die verschiedenen Interrupt-Pins an den Prozessor weiterleitet. Der Gerätetreiber kann das Register PCI_INTERRUPT_LINE nur lesen. Interessanterweise können neuere Versionen des Linux-Kernels unter manchen Umständen Interrupt-Leitungen ohne Hilfe des BIOS zuweisen.

Hot-Pluggable-Geräte

Während der 2.3-Entwicklung überholten die Kernel-Entwickler die PCI-Programmierschnittstelle, um die Arbeit zu vereinfachen und Hot-Pluggable-Geräte zu unterstützen, also solche Geräte, die während der Laufzeit zum System hinzugefügt oder aus ihm entfernt werden können (wie CardBus-Geräte). Was in diesem Abschnitt besprochen wird, steht im 2.2-Kernel und davor nicht zur Verfügung, ist aber für neu entwickelte Treiber der bevorzugte Weg.

Dieser Ansatz basiert auf der Idee, daß alle verfügbaren Gerätetreiber überprüfen müssen, ob ein neues Gerät, das während der Laufzeit des Systems erscheint, ihres ist oder nicht. Daher müssen Hot-Plug-fähige Treiber anstelle der klassischen init- und cleanup-Einsprungpunkte ein Objekt beim Kernel registrieren; die probe-Funktion dieses Objekts wird dann gebeten, jedes Gerät im System zu überprüfen und sich zu eigen zu machen (oder auch nicht).

Dieser Ansatz hat keine Nachteile: Der Standardfall einer statischen Geräteliste wird durch einmaliges Durchsuchen der Geräteliste pro Gerät beim Booten des Systems erledigt; modularisierte Treiber werden wie üblich entladen, wenn kein Gerät vorhanden ist, und ein externer Prozeß, der den Bus überwacht, lädt diese bei Bedarf. Genauso hat das PCMCIA-Subsystem schon immer funktioniert, und die Integration dieses Systems in den Kernel selbst erlaubt einen konsistenteren Umgang mit ähnlichen Fragen in anderen Hardware-Umgebungen.

Sie werden vielleicht einwenden, daß Hot-Plug-PCI heutzutage nicht besonders geläufig ist, aber die neue Treiber-Objekt-Technik ist auch bei Nicht-Hot-Plug-Treibern, die mit einer Reihe alternativer Geräte klarkommen müssen, sehr nützlich. Der Initialisierungscode wird vereinfacht und gestrafft, weil er nur das aktuelle Gerät mit einer Liste bekannter Geräte vergleichen muß, anstatt durch das Durchlaufen von pci_find_class oder pci_find_device aktiv auf dem PCI-Bus nach dem Gerät zu suchen.

Schauen wir uns jetzt aber etwas Code an: Das Design ist um eine in <linux/pci.h> definierte Struktur namens struct pci_driver herumgebaut. Diese Struktur definiert die implementierten Operationen und enthält auch eine Liste der unterstützten Geräte (um unnütze Aufrufe des Codes darin zu vermeiden). Hier sehen Sie die Initialisierung und das Aufräumen eines hypothetischen “Hot-Plug-PCI-Moduls” (HPPM):


struct pci_driver hppm_driver = { /* .... */ };

int hppm_init_module(void)
{
    return pci_module_init(&hppm_driver);
}

int hppm_cleanup_module(void)
{
    pci_unregister_driver(&hppm_driver);
}

module_init(hppm);
module_exit(hppm);

Das ist schon alles. Es ist geradezu unglaublich einfach. Die versteckte Zauberei verteilt sich auf die Implementation von pci_module_init sowie die Interna der Treiber-Struktur. Wir arbeiten besser top-down und fangen mit den relevanten Funktionen an:

int pci_register_driver(struct pci_driver *drv);

Diese Funktion fügt einen Treiber in einer verketteten Liste ein, die vom System verwaltet wird. Auf diese Weise erledigen einkompilierte Treiber ihre Initialisierung; von modularisiertem Code wird diese Funltion nicht direkt verwendet. Der Rückgabewert ist die Anzahl der vom Treiber abgedeckten Geräte.

int pci_module_init(struct pci_driver *drv);

Diese Funktion ist ein Wrapper um die eben genannte Funktion und sollte von modularisiertem Initialisierungscode aufgerufen werden. Sie gibt 0 im Erfolgsfall und -ENODEV, wenn kein Gerät gefunden wurde, zurück. Dies soll ein Modul daran hindern, im Speicher zu verbleiben, wenn gerade kein Gerät vorhanden ist (mit dem Hintergedanken, daß das Modul automatisch geladen wird, wenn ein passendes Gerät auftaucht). Weil diese Funktion als inline definiert ist, ändert sich das Verhalten je nachdem, ob MODULE definiert ist oder nicht; sie kann also selbst in nicht-modularisiertem Code als unveränderter Ersatz für pci_register_driver verwendet werden.

void pci_unregister_driver(struct pci_driver *drv);

Diese Funktion entfernt den Treiber aus der verketteten Liste bekannter Treiber.

void pci_insert_device(struct pci_dev *dev, struct pci_bus *bus);, void pci_remove_device(struct pci_dev *dev);

Diese beiden Funktionen implementieren die aktive Komponente des Hot-Plug-Systems; sie werden von den Ereignisbehandlungsroutinen aufgerufen, die das Hinzufügen oder Entfernen von Geräten auf einem Bus überwachen. Die Struktur dev wird dazu verwendet, die Liste der registrierten Treiber zu durchsuchen. Gerätetreiber müssen diese Funktionen nicht aufrufen, sie stehen hier nur, um Ihnen ein vollständiges Bild des Designs rund um PCI-Treiber zu geben.

struct pci_driver *pci_dev_driver(const struct pci_dev *dev);

Dies ist eine Hilfsfunktion, die den Treiber zu einem Gerät (so vorhanden) heraussucht. Sie wird von den Hilfsfunktionen für /proc/bus verwendet und sollte von Gerätetreibern nicht aufgerufen werden.

Die Struktur pci_driver

Die Datenstruktur pci_driver ist der zentrale Bestandteil der Hot-Plug-Unterstützung; wir beschreiben sie daher hier detailliert, um das Gesamtbild zu vervollständigen. Die Struktur ist recht klein; sie besteht nur aus einigen Methoden und der Geräte-ID-Liste.

struct list_head node;

Wird dazu verwendet, eine Liste von Treibern zu verwalten. Dies ist ein Beispiel für die generischen Listen, die in “the Section called Verkettete Listen in Kapitel 10” in Kapitel 10 eingeführt wurden, und sollte nicht von Gerätetreibern verwendet werden.

char *name;

Der Name des Treibers, wird nur zu Informationszwecken verwendet.

const struct pci_device_id *id_table;

Ein Array, das die von diesem Treiber unterstützten Geräte aufführt. Die Methode probe wird nur für Geräte aufgerufen, die auf eines der Elemente in diesem Array passen. Wenn das Feld mit NULL belegt wird, wird die probe-Funktion für jedes Gerät im System aufgerufen. Wenn das Feld nicht NULL ist, dann muß das letzte Element im Array 0 sein.

int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);

Diese Funktion muß das übergebene Gerät initialisieren und im Erfolgsfalle 0 sowie im Fehlerfall einen negativen Fehler-Code zurückgeben (der Fehler-Code wird derzeit nicht verwendet, aber man kann getrost einen errno-Fehler-Code zurückgeben, nicht nur einfach -1 ).

void (*remove)(struct pci_dev *dev);

Die Methode remove wird dazu verwendet, dem Gerätetreiber mitzuteilen, daß er das Gerät herunterfahren und sich nicht mehr damit beschäftigen sowie allen zugehörigen Speicher freigeben soll. Die Funktion wird entweder aufgerufen, wenn das Gerät aus dem System entfernt wird oder wenn der Treiber pci_unregister_driver aufruft, um aus dem System entladen zu werden. Im Gegensatz zu probe wird ist diese Methode PCI-Geräte-spezifisch und gilt nicht für alle von diesem Treiber abgedeckten Geräte; das jeweilige Gerät wird als Argument übergeben.

int (*suspend)(struct pci_dev *dev, u32 state);, int (*resume)(struct pci_dev *dev);

Dies sind die Power Management-Funktionen für PCI-Geräte. Wenn der Gerätetreiber das Power Management unterstützt, sollten diese beiden Methoden verwendet werden, um das Gerät herunterzufahren und zu reaktivieren; sie werden zu den passenden Zeitpunkten von den höheren Schichten aufgerufen.

Das PCI-Treiber-Objekt ist ziemlich offensichtlich aufgebaut und angenehm zu verwenden. Zur Aufzählung der Felder gibt es wenig hinzuzufügen, weil der normale Code zum Umgang mit der Hardware gut in diese Abstraktion paßt, ohne entsprechend verbogen werden zu müssen.

Jetzt müssen wir nur noch das struct pci_device_id-Objekt beschreiben. Die Struktur enthält mehrere ID-Felder. Das jeweilige Gerät, das einen Treiber benötigt, wird mit allen Feldern verglichen. Alle Felder können auf PCI_ANY_ID gesetzt werden, um dem System mitzuteilen, es zu ignorieren.

unsigned int vendor, device;

Die Hersteller- und Geräte-IDs des Geräts, an dem dieser Treiber interessiert ist. Die Werte werden mit den Registern 0x00 und 0x02 des PCI-Konfigurationsraums verglichen.

unsigned int subvendor, subdevice;

Die Unter-IDs, die mit den Registern 0x2C und 0x2E des PCI-Konfigurationsraums verglichen werden. Sie werden beim Suchen des Geräts verwendet, weil ein Hersteller/Geräte-ID-Paar manchmal eine ganze Gruppe von Geräten identifiziert, der Treiber aber möglicherweise nur mit einigen davon umgehen kann.

unsigned int class, class_mask;

Wenn ein Gerätetreiber eine ganze Klasse oder eine Teilmenge davon abdecken will, dann kann er die obengenannten Felder auf PCI_ANY_ID setzen und statt dessen die Klassen-Bezeichner verwenden. Die class_mask gibt es, um sowohl Treiber zu ermöglichen, die eine Basisklasse abdecken wollen, als auch solche, die nur an einer Subklasse interessiert sind. Wenn die Geräte-Auswahl über die Hersteller/Geräte-Bezeichner geschieht, müssen diese Felder auf 0 gesetzt werden (nicht auf PCI_ANY_ID, weil die Überprüfung über ein logisches UND mit dem Maskenfeld geschieht).

unsigned long driver_data;

Ein Feld zur Verwendung durch den Gerätetreiber. Es kann beispielsweise dazu verwendet werden, die einzelnen Geräte zur Kompilierzeit zu unterscheiden, und so umständliche Arrays bedingter Abfragen zur Laufzeit vermeiden.

> > > > Interessanterweise ist die Datenstruktur pci_device_id nur ein Hinweis vom System; Gerätetreiber selbst dürfen trotzdem noch 0 aus der probe-Methode zurückgeben und so das Gerät zurückweisen, auch wenn es auf das Array der Geräte-Bezeichner gepaßt hat. Wenn also beispielsweise mehrere Geräte mit der gleichen Signatur existieren, dann kann der Treiber nach weiteren Informationen suchen, anhand derer er entscheiden kann, ob er für das Peripherie-Gerät geeignet ist oder nicht.

Hardware-Abstraktionen

Wir vervollständigen die Behandlung von PCI hier durch einen schnellen Blick darauf, wie das System mit der Unzahl von PCI-Controllern umgeht, die es auf dem Markt gibt. Dies ist lediglich zur Information gedacht, um neugierigen Lesern zu zeigen, wie sich das objektorientierte Layout des Kernels bis in die untersten Schichten hinunter erstreckt.

Der Mechanismus zur Implementation der Hardware-Abstraktion ist die übliche Struktur mit enthaltenen Methoden. Dies ist eine leistungsfähige Technik, die nur den minimalen zusätzlichen Aufwand erfordert einen Zeiger über den normalen Funktionsaufruf hinaus zu dereferenzieren. Im Falle der PCI-Verwaltung sind die einzigen Hardware-abhängigen Operationen diejenigen, die die Konfigurationsregister auslesen und beschreiben, weil alles andere in der PCI-Welt durch direktes Lesen und Schreiben der I/O- und Speicher-Adreßräume geschieht und diese unter direkter Kontrolle der CPU stehen.

Die Struktur für die Hardware-Abstraktion enthält dementsprechend nur sechs Felder:


struct pci_ops {
    int (*read_byte)(struct pci_dev *, int where, u8 *val);
    int (*read_word)(struct pci_dev *, int where, u16 *val);
    int (*read_dword)(struct pci_dev *, int where, u32 *val);
    int (*write_byte)(struct pci_dev *, int where, u8 val);
    int (*write_word)(struct pci_dev *, int where, u16 val);
    int (*write_dword)(struct pci_dev *, int where, u32 val);
};

Diese Struktur wird in <linux/pci.h> definiert und von drivers/pci/pci.c verwendet, wo auch die eigentlichen öffentlichen Funktionen definiert sind.

Die sechs Funktionen, die auf dem PCI-Konfigurationsraum arbeiten, bringen mehr Aufwand als das Dereferenzieren eines Zeigers mit sich, weil sie aufgrund des hohen Grades an Objektorientierung kaskadierende Zeiger verwenden; dies ist aber angesichts der Operationen, die selten (und nie in zeitkritischen Situationen) ausgeführt werden) kein Problem. Die Implementation von pci_read_config_byte(dev) expandiert beispielsweise zu:


dev->bus->ops->read_byte();

Die diversen PCI-Bus-Systeme im System werden beim Booten erkannt. Zu diesem Zeitpunkt werden auch die struct pci_bus-Elemente erzeugt und mit ihren Merkmalen, darunter dem ops-Feld, verknüpft.

Das Implementieren von Hardware-Abstraktionen via "Hardware-Operations-Datenstrukturen" ist typisch für den Linux-Kernel. Ein wichtiges Beispiel ist die Datenstruktur struct alpha_machine_vector. Diese ist in <asm-alpha/machvec.h> definiert und deckt alle Unterschiede ab, die es zwischen den einzelnen Alpha-basierten Computern gibt.

Fußnoten

[1]

Bitte beachten Sie, daß das hier Gesagte wie üblich auf der Version 2.4 des Kernels basiert und wir die Fragen der Abwärtskompatibilität am Ende des Kapitels behandeln werden.

[2]

Die Konfiguration ist übrigens nicht auf das Booten des Systems begrenzt; sogenannte “Hot-pluggable-Geräte” stehen beispielsweise nicht beim Booten zur Verfügung, sondern erscheinen erst später. Bei diesen Geräten müssen die Gerätetreiber die Adressen von I/O- und Speicherbereichen nicht ändern.

[3]

Die ID eines jeden Gerätes sollte im Handbuch zur Hardware stehen. In der Datei pci.ids im Package pciutils sowie in den Kernelquellen finden Sie ebenfalls eine Liste. Diese behauptet gar nicht erst, vollständig zu sein, enthält aber die bekanntesten Hersteller und Geräte.

[4]

Die Feldnamen in struct pci_dev wurden zwischen 2.2 und 2.4 geändert, weil sich das erste Layout als nicht ganz optimal herausgestellt hatte. In 2.0 gab es keine pci_dev-Struktur; die Struktur, die Sie dann verwenden, ist eine einfache Emulation aus der Header-Datei pci-compat.h.

[5]

Diese Information steckt in den niedrigwertigen Bits der Basisadressen-PCI-Register. Die Bits sind in <linux/pci.h> definiert.