5.5. Hardware anbinden

Der Treiberentwickler muss nicht nur die Applikationen bedienen, sondern auch die untere Schnittstelle – nämlich die zur Hardware hin. Aufgabe dabei ist es, Daten von der Hardware zu holen, um sie dann der Applikation zur Verfügung zu stellen, beziehungsweise umgekehrt den Datentransfer von der Applikation hin zur Hardware zu gewährleisten. Abbildung Funktionen zum Datentransfer zwischen Applikation und Hardware veranschaulicht die dafür zur Verfügung stehenden Funktionen.

Abbildung 5-4. Funktionen zum Datentransfer zwischen Applikation und Hardware

Das folgende Kapitel beschäftigt sich mit der Hardware-Erkennung und dem Hardwarezugriff, den jeder Treiber zu leisten imstande sein muss. Dabei werden die folgenden Aspekte behandelt:

5.5.1. Ressourcen-Management

Es liegt im Aufgabenbereich des Treiberentwicklers, Hardware-Ressourcen zu reservieren. Andernfalls – bei Nutzung vorhandener Rechner-Ressourcen, ohne dass diese »gebucht« worden wären – könnten die Ressourcen verbotenerweise von mehreren Rechenprozessen genutzt werden.

Alle Ressourcen werden vom Betriebssystemkern verwaltet. Dabei teilt der Kern einzelnen Komponenten die Ressourcen implizit oder nach einer Anforderung durch das Subsystem zu. Implizit zugeteilte Ressourcen sind vor allem Code- und Daten-Segmente. Über diese Ressourcen muss sich der Treiberentwickler nur dann Gedanken machen, wenn er Treiber für Systeme mit sehr knappen Ressourcen entwickelt. Ansonsten sind die anzufordernden Betriebsmittel

5.5.1.1. Ressourcen reservieren

Welche Ressourcen eine Hardware belegt, kann der Entwickler entweder der Hardwarespezifikation entnehmen oder – wie im Fall von PCI – sich vom Kernel mitteilen lassen. In einigen Fällen muss bzw. kann eine Hardwaredetektion durchgeführt werden, um festzustellen, welche Ressourcen durch eine Hardware belegt werden. Anzufordernde Ressourcen dürfen vom Treiber erst verwendet werden, nachdem sie vom Kernel zugeteilt wurden. Aufgrund der Anforderung teilt der Betriebssystemkern dem Treiber mit, ob die Ressource verfügbar ist oder nicht. Ist die Ressource nicht verfügbar, muss im Treiber eine entsprechende Fehlerbehandlung ablaufen.

Wird das Betriebsmittel vom Treiber nicht mehr benötigt, sollte es freigegeben werden. Für das Anfordern (Reservieren) eines Betriebsmittels und dessen Freigabe stellt der Kernel die folgenden Funktionen zur Verfügung.

Erst wenn die Adresslagen der Hardwareressourcen bekannt sind, können diese beim Betriebssystemkern reserviert werden. Dieser Vorgang ist Teil der so genannten Geräteinitialisierung, die entweder direkt in der Treiberfunktion init_module oder – beispielsweise bei PCI – in der Funktion pci_probe durchgeführt wird.

Beispiel 5-19. Reservierung von Ressourcen

static int __init driver_init(void)                        (1)
{
    if(register_chrdev(DRIVER_MAJOR, "MyDriver", &fops) != 0) {
        return -EIO;
    }
    ...
    // Hardware-Erkennung                                  (2)
    ...
	// Geraeteinitialisierung
    if( request_irq(my_irq,PIsr,SA_INTERRUPT|SA_SHIRQ,"MyDriver",NULL) ) {(3)
        unregister_chrdev(DRIVER_MAJOR,"MyDriver");        (4)
        return -EIO;
    }
    if( request_region( ioport, iolen, "MyDriver" )==NULL ) {(5)
        unregister_chrdev(DRIVER_MAJOR,"MyDriver");        (6)
        free_irq( my_irq, "MyDriver" );
        return -EIO;
    }
    if( request_mem_region( memstart, memlen, "MyDriver" )==NULL ) {(7)
        unregister_chrdev(DRIVER_MAJOR,"MyDriver");
        free_irq( my_irq, "MyDriver" );
        release_region( ioport, iolen );
        return -EIO;
    }
    return 0;
}

static void driver_exit( void )                            (8)
{
    free_irq( my_irq, "MyDriver" );
    release_region( ioport, iolen );
    release_mem_region( memstart, memlen );
    unregister_chrdev(DRIVER_MAJOR,"MyDriver");
}
(1)
Die Reservierung von Ressourcen kann beispielsweise innerhalb der Funktion init_module (hier driver_init) oder in der Probe-Funktion eines »hotpluggable«-Gerätes stattfinden.
(2)
Die Hardware-Erkennung findet vor der Reservierung statt. Sie ist für jedes Gerät unterschiedlich.
(3)
Beim Betriebssystemkern wird ein Interrupt angefordert. Interrupts gehören zu den Betriebsmitteln, die von mehreren Komponenten verwendet werden können, wenn sich diese an bestimmte Konventionen halten (siehe Interruptsharing). Ob ein Treiber in der Lage ist, den Interrupt zu teilen, macht er anhand des Flags SA_SHIRQ deutlich.
(4)
Steht der Interrupt nicht zur Verfügung, weil er beispielsweise bereits durch einen Treiber reserviert wurde, der kein Interruptsharing ermöglicht, gibt der Kernel einen entsprechenden Rückgabewert aus. Im Treiber muss eine entsprechende Fehlerbehandlung durchgeführt werden. In diesem Beispiel wird dazu zunächst der Treiber beim Kernel abgemeldet und dann das Laden abgebrochen; der Treiber kann ohne den Interrupt nicht verwendet werden.
(5)
In dieser Zeile werden die IO-Ports reserviert.
(6)
Schlägt die Reservierung der Ports fehl, müssen alle vorher beim Kernel durchgeführten Anmeldungen und Reservierungen rückgängig gemacht werden: Der Treiber meldet sich wieder ab und gibt den bereits erfolgreich reservierten Interrupt frei.
(7)
In dieser Zeile werden die Speicherbereiche reserviert.

(8)
In der zum Treiber gehörigen cleanup_module-Funktion werden die allozierten Ressourcen wieder freigegeben und der Treiber meldet sich beim Kernel ab.

Ein Beispiel für die Reservierung von Ressourcen im Fall eines so genannten »hotpluggable«-Gerätes, wie es PCI-Geräte typischerweise sind, ist im Abschnitt PCI-Treiber-Initialisierung zu finden.

5.5.1.2. Interruptsharing

Im Gegensatz zu den Port-, Speicher- oder DMA-Ressourcen kann ein Interrupt durch mehrere Module simultan verwendet werden. Dieses so genannte Interruptsharing muss jedoch sowohl von der Hardware als auch von der treiberspezifischen Interrupt-Service-Routine (ISR) unterstützt werden.

Der Treiber übergibt mit der bereits bekannten Funktion request_irq dem Betriebssystemkern die Adresse seiner ISR. Dabei gibt der Treiber mit Hilfe des Flags SA_SHIRQ zusätzlich an, dass er Interruptsharing unterstützt.

    if(request_irq(irq_nr, driver_isr, SA_SHIRQ|SA_INTERRUPT, "sx", board)) {
        // Interrupt nicht frei, Fehlerbehandlung
        ...
    }

Ist der Interrupt frei bzw. ist der Interrupt bisher nur durch Treiber belegt, die Interruptsharing unterstützen, trägt der Kernel die übergebene Adresse der Interrupt-Service-Routine in seine Funktionsliste ein. Die hier eingetragenen Funktionen werden nacheinander aufgerufen, sobald der entsprechende Interrupt auftritt.

Dieses Vorgehen bedeutet aber: Die Interrupt-Service-Routine wird möglicherweise aufgerufen, obwohl das zugehörige Gerät keinen Interrupt ausgelöst hat. Damit die aufgerufene ISR den Interrupt nicht fälschlicherweise bearbeitet, muss sie feststellen können, ob die Hardware einen Interrupt ausgelöst hat oder nicht. Im Regelfall liest sie dazu auf der Hardware ein Statusregister aus.

Die Interruptsharing unterstützende ISR ist damit so aufgebaut, dass sie zunächst feststellt, ob der Interrupt durch die eigene Hardware ausgelöst wurde oder nicht (siehe Beispiel Eine einfache Interrupt-Service-Routine). Falls ja, wird der Interrupt normal behandelt. Sie gibt IRQ_HANDLED zurück. Im anderen Fall beendet sich die Funktion sofort und gibt dabei IRQ_NONE zurück.

Beispiel 5-20. Eine einfache Interrupt-Service-Routine

static int driver_isr( int irq_nr, void *dev_id, struct pt_regs *regs )
{
    u8 status_wort;                                        (1)

    status_wort = inb( STATUSWORT );                       (2)
    if( (interrupt_register & INTERRUPTBIT)==0 ) {         (3)
        return IRQ_NONE; // fremder Interrupt
    }
    // Interruptbehandlung
    interrupt_quittieren(); // falls notwendig
    ...
    return IRQ_HANDLED;                                    (4)
}
(1)
In diese Variable wird der Wert des Hardwareregisters abgelegt.
(2)
Das Statuswort der Hardware wird eingelesen. Die Portadresse des Registers verbirgt sich hinter der Variablen oder dem Define STATUSWORT und ist von Hardware zu Hardware unterschiedlich.
(3)
Bei der hier verwendeten Hardware gibt ein Bit innerhalb des Statusworts Auskunft darüber, ob der Interrupt ausgelöst wurde oder nicht. Ist dieses Bit nicht gesetzt, muss der Interrupt auch nicht behandelt werden. Oftmals wird ein solches Interrupt-Bit durch die Hardware automatisch zurückgesetzt, sobald das Statuswort gelesen wurde. Ist dies nicht der Fall, müsste der Treiber selbst das Bit zurücksetzen.
(4)
Ist das Bit jedoch gesetzt (die Bedingung war nicht erfüllt), beendet sich die Interrupt-Service-Routine mit IRQ_HANDLED.

5.5.1.3. Dynamische Speicherverwaltung

Zu den Ressourcen, auf die der Treiberentwickler zugreift, zählen neben Interrupts vor allem Speicherbereiche. Linux bietet die Möglichkeit, dynamisch, also während der Laufzeit, Speicher zu allozieren. Dazu existiert eine zum bekannten malloc korrespondierende Funktion im Kernel: kmalloc.

Diese Funktion ist in ihrem Verhalten durch den Treiberentwickler parametrierbar. So wird der Anforderung Rechnung getragen, dass sich eine Speicherallozierung abhängig vom Kontext, in dem sich der Kernel gerade befindet, unterschiedlich verhalten soll.

Wird innerhalb einer durch die Applikation getriggerten Treiberfunktion kmalloc aufgerufen, kann die zugehörige Treiberinstanz in den Wartezustand versetzt werden, falls gegenwärtig nicht genügend Speicher zur Verfügung steht. Innerhalb einer durch den Kernel getriggerten Funktion (z.B. ISR oder Softirq, siehe Kapitel Fortgeschrittene Treiberentwicklung) ist das nicht möglich.

Darüber hinaus kann als zusätzliches Qualitätsmerkmal der Speicherbereich mit angegeben werden, der von kmalloc ausgewählt wird. Drei Speicherbereiche (so genannte Speicherzonen) werden unterschieden:

  1. Normaler Speicherbereich

  2. Speicher, der für DMA-Transfers genutzt werden kann, und

  3. so genanntes High Memory (Speicherbereiche, auf die nur per DMA zugegriffen werden kann).

Das Verhalten und die Eingrenzung auf eine mögliche Speicherzone werden der Funktion über ein Flag mitgegeben. Die möglichen Werte dieses Flags sind in der Header-Datei <linux/mm.h> spezifiziert.

Für die Treiberprogrammierung werden im Wesentlichen drei der dort spezifizierten Kombinationen benötigt:

GFP_USER

Dieses Flag wird vor allem für User-Prozesse (Applikationen) verwendet. Es teilt kmalloc mit, dass die zugehörige Treiberinstanz in den Zustand »wartend« (INTERRUPTIBLE) versetzt werden kann, falls gegenwärtig nicht genügend Speicher zur Verfügung steht.

GFP_KERNEL

Mit diesem Flag reagiert kmalloc ähnlich wie bei GFP_USER, es wird allerdings im Bereich von Kernel-Threads eingesetzt. Ein weiterer Unterschied besteht darin, dass zunächst versucht wird, den Speicher aus einem Reservebereich zu entnehmen, anstatt die zugehörige Treiberinstanz sofort in den Wartezustand zu versetzen.

GFP_ATOMIC

Ist bei kmalloc dieses Flag gesetzt, wird der Zugriffswunsch auf den Speicher grundsätzlich bedient. Der Betriebssystemkern versucht nicht, den gerade aktiven Prozess in den Wartezustand zu versetzen. Diese Form der Funktion muss bei der Speicherallozierung im Kernel-Kontext, also in ISRs, Softirqs, Tasklets und Timer-Funktionen verwendet werden (siehe Fortgeschrittene Treiberentwicklung).

Allozierter Speicher muss selbstverständlich wieder freigegeben werden, wenn ihn die Applikation nicht mehr benötigt. Hierzu dient die Funktion kfree.

Neben kmalloc gibt es zur dynamischen Speicherreservierung auch noch die Funktion vmalloc. Während sich mit kmalloc nur »kleine« Speicherbereiche reservieren lassen (typischerweise bis zu 128 kByte), sind mit vmalloc auch Speichergrößen im Megabyte-Bereich, wie sie beispielsweise für eine Ramdisk benötigt werden, möglich.

Ein weiterer Unterschied besteht darin, dass der allozierte Speicherbereich bei vmalloc nur virtuell, nicht aber physikalisch zusammenhängend ist (das »v« in vmalloc steht für »virtuell«).

Zur Freigabe wird die Funktion vfree verwendet.

5.5.1.4. Management implizit zugeteilter Ressourcen

Wird ein Treiber geladen, teilt ihm der Kernel implizit und automatisch Ressourcen zu. Im Wesentlichen sind dies die Speicherressourcen, in denen sich Code und Daten des Treibers befinden.

Um alle im System befindlichen Ressourcen bestmöglich zu nutzen, können – zumindest im Fall von Built-in-Treibern – unter Linux Initialisierungsdaten bzw. Initialisierungscode nach der erfolgten Initialisierung wieder aus dem Speicher entfernt werden. Der damit frei gewordene Speicher steht nun den Applikationen zur Verfügung. Code und Speicher, der für eine Deinitialisierung benötigt wird, wird unter Umständen erst gar nicht geladen. Das wird möglich, wenn Code und Daten durch den Linker in eigene Init- bzw. Exit-Segmente abgelegt werden.

Dem Programmierer stehen dazu die Schlüsselworte

  • __init

  • __exit

  • __initdata

  • __exitdata

  • __devinit

  • __devexit

  • __devinitdata

  • __devexitdata

zur Verfügung. Diese Schlüsselworte werden bei der Definition und Deklaration der Funktionen und Variablen verwendet. Die ersten 4 Schlüsselworte werden für »normale« Funktionen und Variablen verwendet, die übrigen 4 bei der Definition und Deklaration von Funktionen und Variablen, die »hotpluggable devices« bedienen.

Abbildung 5-5. Wirkung der Schlüsselworte init und exit

Diese Unterscheidung ist notwendig, um je nach Kernelkonfiguration den Code richtig zu übersetzen. Gegenwärtig werden die Schlüsselworte bei der Generierung eines Modultreibers ignoriert, so dass Initialisierungs-Code und -Daten nicht gesondert behandelt werden.

Das Gleiche gilt für Code und Daten, die mit den Schlüsselworten für »hotpluggable devices« versehen sind und durch den Kernel mit der Option HOTPLUG übersetzt werden. Werden diese Code- und Datenteile ohne HOTPLUG-Unterstützung im Kernel als Built-in-Treiber generiert, werden Initialisierungs-Code bzw. Initialisierungs-Daten während des Boot-Vorganges wieder freigegeben und Deinitialisierungs-Code und -Daten erst gar nicht geladen.

Die Schlüsselworte __init, __exit, __devinit und __devexit werden bei der Definition einer Funktion direkt vor den Namen der Funktion gesetzt, bei der zugehörigen Deklaration nach der schließenden Klammer und dem Semikolon.

extern void init_function(int) __init;                     (1)

void __init init_function(int var1)                        (2)
{
    ...
}
(1)
Deklaration einer Initialisierungsfunktion.
(2)
Definition einer Initialisierungsfunktion.

Die Schlüsselworte __initdata, __exitdata, __devinitdata und __devexitdata werden zwischen dem Namen der Variablen und dem Semikolon bzw. bei gleichzeitiger Zuweisung eines Wertes zwischen dem Namen und dem Gleichheitszeichen gesetzt. Das gilt sowohl für die Variablendefinition als auch für die Variablendeklaration.

static int init_variable __initdata;                       (1)
static char linux_logo[] __initdata = { 0x32, 0x36, ... }; (2)
(1)
Definition einer nicht initialisierten Variablen, die im Fall eines Built-in-Treibers in das Daten-Init-Segment kommt.
(2)
Definition einer initialisierten Variable, die im Fall eines Built-in-Treibers in das Daten-Init-Segment kommt.

Für die Deinitialisierung gilt Ähnliches. Kennzeichnet ein Programmierer seine Funktionen (bzw. Daten) als Deinitialisierungsfunktionen (bzw. Daten, die bei der Deinitialisierung benötigt werden) durch die Schlüsselworte __exit und __exitdata, werden diese im Fall von Built-in-Treibern nicht einkompiliert. Wird der gleiche Code jedoch als Modul übersetzt, erfolgt eine Übersetzung.

Tabelle 5-3. Schlüsselworte der impliziten Ressourcenverwaltung

Schlüssel- wortVerwendungBuilt-in-TreiberModultreiber
__init Deklaration und Definition von Funktionen. Code wird in das Init-Textsegment abgelegt, welches nach der Initialisierung aus dem Speicher entfernt wird. Code wird normal kompiliert.
__exit Deklaration und Definition von Funktionen. Code wird in das Exit-Textsegment abgelegt, welches nicht verwendet wird (unused). Code wird normal kompiliert.
__initdata Deklaration und Definition globaler Variablen. Variable wird in das Init-Datensegment abgelegt. Variable wird im Datensegment abgelegt.
__exitdata Deklaration und Definition globaler Variablen. Variable wird in das Exit-Datensegment abgelegt, welches jedoch nicht verwendet wird (unused). Variable wird im Datensegment abgelegt.
__devinit Deklaration und Definition einer Funktion, die »hotpluggable devices« bedient. Ist der Support für »hotpluggable devices« bei der Kompilierung aktiviert, wird der Code normal abgelegt. Ist der Support dagegen deaktiviert, wird die Funktion in das Init-Codesegment gelegt, welches nach der Initialisierung wieder aus dem Speicher entfernt wird. Variable wird im Datensegment abgelegt.
__devexit Deklaration und Definition einer Funktion, die »hotpluggable devices« bedient. Ist der Support für »hotpluggable devices« aktiviert, wird der Code ins normale Codesegment abgelegt. Ist der Support deaktiviert, wird der Code in das Exit-Codesegment gelegt, welches erst gar nicht geladen wird. Code wird in das normale Codesegment abgelegt.
__devinitdata Deklaration und Definition globaler Variablen, die bei der Initialisierung eines Treibers für »hotpluggable devices« verwendet werden. Variable wird dann in das Init-Datensegment abgelegt, wenn die Option HOTPLUG nicht aktiviert ist. Variable wird im Datensegment abgelegt.
__devexitdata Deklaration und Definition globaler Variablen, die bei der Deinitialisierung eines Treibers für »hotpluggable devices« verwendet werden. Variable wird dann in das Exit-Datensegment abgelegt, wenn die Option HOTPLUG nicht aktiviert ist. Variable wird im Datensegment abgelegt.

5.5.2. Datentypen und Datenablage

Zur Treiberentwicklung gehört auch, dass sich der Programmierer mit dem – je nach Hardware unterschiedlichen – Speicherbedarf für Datentypen und einer möglichst effizienten Datenablage auseinander zu setzen hat.

Datentypen. Der Speicherbedarf verschiedener Datentypen (z.B. int oder long) ist nämlich nicht festgelegt und variiert von Prozessortyp zu Prozessortyp. Auf einem PC mit x86-Prozessor werden zum Abspeichern einer Integervariablen (Typ int) 4 Bytes benötigt, eine Variable vom Typ long belegt ebenfalls 4 Bytes.

Für den Treiberentwickler ist es beim Zugriff auf Hardware in vielen Fällen wichtig, mit einem Zugriff eine genau definierte Anzahl Bytes zu lesen oder zu schreiben. Für diese Art des Zugriffes sind unter Linux spezifische Datentypen definiert (für 1, 2, 4 oder 8 Byte), und zwar sowohl für die Applikationen als auch für den Betriebssystemkern. Die Definitionen für diese Datentypen befinden sich in der Header-Datei <asm/types.h>.

An der Applikationsschnittstelle gibt es die Datentypen:

Im Treiber stehen die gleichen Datentypen, jedoch ohne »__« zur Verfügung:

Gepackte Datenablage. Variablen werden im Normalfall durch den Compiler auf die Speicheradressen abgelegt, die ihrem Typ entsprechen. Eine 2-Byte-Variable (u16) wird demnach auf eine durch zwei teilbare Adresse abgelegt, eine 4-Byte-Variable vom Typ s32 wird auf eine durch vier teilbare Adresse abgelegt.

Abbildung 5-6. Ausgerichtete Daten

Abbildung Ausgerichtete Daten stellt diese so genannte »ausgerichtete« (aligned) Datenablage noch einmal dar. Statt die 4 Byte-Variable c auf die nächste freie Adresse 0xbffffa8e zu legen, erhält sie die Adresse 0xbffffa90. Die Adresse 0xbffffa8e ist nicht durch 4 teilbar, 0xbffffa90 dagegen wohl.

Zwar wird durch diese Art der Variablenablage freier Speicherplatz verschenkt, doch im Gegenzug ist der Zugriff auf die ausgerichteten Variablen erheblich beschleunigt.

Bei Peripheriegeräten – insbesondere, wenn diese über einen gemeinsamen Speicher (Dualport RAM) mit dem Hostsystem verbunden sind – ist aber die Ablage von Variablen oftmals genau definiert. Hier muss der Compiler angewiesen werden, die Ausrichtung (das Alignment) aufzugeben und die Variablen kompakt (gepackt) abzulegen. Dazu dient beim GNU-Compiler das Schlüsselwort attribute im Kontext mit dem Schlüsselwort packed (__attribute__((packed))).

Abbildung 5-7. »Gepackte« Datenstruktur

Abbildung »Gepackte« Datenstruktur stellt die vorgestellte Datenstruktur in gepackter Form dar.

5.5.3. Direkter Hardwarezugriff

Heutzutage wird die Peripherie meist wie ganz normaler Speicher an eine CPU angekoppelt. In diesem Fall spricht man von »Memory Mapped IO«. Die Speicherzellen, die die Hardware im Adressraum des Prozessors belegt, nennt man Register. Aus Sicht der CPU gibt es zunächst keinen Unterschied zwischen einer Speicherzelle und einem Register. Sie greift wie gewohnt zu. Der Treiberentwickler sollte jedoch im Hinterkopf haben, dass die Zugriffe auf Register im Regelfall langsamer sind, die Zugriffszeit ist also höher als bei normalem Speicher. Noch wichtiger: Die Inhalte der Register können nicht nur durch die CPU manipuliert werden, sondern auch durch die Hardware. So muss der Programmierer damit rechnen, dass nach einer Schreiboperation auf ein Register ein anderer Wert zurückgelesen wird.

Neben der Möglichkeit, Peripherie »Memory Mapped« anzuschließen, bieten einige Prozessoren einen eigenen Prozessorbus an, über den die Hardwarekopplung erfolgt. Hierbei besitzt die CPU einen eigenen IO-Adressraum neben dem üblichen Speicher-Adressraum. Die einzelnen Adressen des IO-Adressraums werden »Ports« genannt. Der Zugriff auf die Ports, also auf die Register der Hardware, erfolgt über spezielle Prozessor-Befehle (Portbefehle, beispielsweise in und out).

Unabhängig davon, ob ein Register über Port- oder Memory-Mapped angeschlossen ist, wird die Hardware durch Lesen und Schreiben auf die Register kontrolliert. Um den Zustand einer Hardware zu erfahren, wird von einem Register gelesen. Um Funktionen auszulösen, wird auf das Register geschrieben.

Welche Zustände sich hinter welchen Registern beziehungsweise hinter welchen Bits einzelner Register verbergen, muss man der Hardwarebeschreibung des jeweiligen Gerätes entnehmen. Das Gleiche gilt für ausgelöste Funktionen, die ebenfalls der Hardwarebeschreibung zu entnehmen sind.

Neben der direkten Ankopplung von Hardware an die CPU etablieren sich zunehmend intelligente Bussysteme. Hierbei wird die CPU entweder Memory-Mapped oder Port-Mapped mit einem Buscontroller verbunden. Der Controller wiederum bringt eigene Signalleitungen sowie ein eigenes Protokoll zur Ansteuerung weiterer Hardware mit. Beispiele für derartige Bussysteme sind USB, SCSI oder IrDA. Für Hardware, die über solche Bussysteme angeschlossen sind, ist ein direkter Hardwarezugriff nicht nötig. Vielmehr existieren in diesem Fall Subsysteme (z.B. der USB-Core) im Kernel, die Funktionen für den Zugriff auf die Peripheriegeräte ermöglichen. Diese Art des Hardwarezugriffes wird in Kapitel Sonstige Treibersubsysteme beschrieben.

5.5.3.1. Memory Mapped IO

Aus Sicht des Programmierers gibt es zwischen Memory Mapped IO und sonstigem Speicher auf einer PC-Plattform keinen Unterschied. Der Treiberentwickler könnte direkt auf die Hardware – z.B. über einen Pointer – zugreifen.

    unsigned char *io_adress = 0xd00000;

    ...
    *io_adress = 0xff; // Kommando RESET
    ...

Für Plattformen wie beispielsweise Alpha gilt dies allerdings nicht. Daher sollte der Entwickler der Einheitlichkeit wegen auch auf Memory Mapped IO stets nur über Kernel-Funktionen zugreifen.

Dazu dienen die im Headerfile <asm/io.h> definierten Makros:

Um also ein einzelnes Byte zu lesen, verwendet man readb, um ein einzelnes Byte zu schreiben writeb. Auf einer PC-Plattform wird dieser Zugriff zu einer normalen Zuweisung expandiert.

#include <asm/io.h>
    ...
    unsigned char *mem_io_address = 0xd00000;

    ...
    writeb( 0xff, mem_io_address ); // Kommando RESET
    ...

Um größere Speicherbereiche zu kopieren bzw. zu initialisieren, gibt es ebenfalls Makros:

Auf der X86-Plattform werden diese letztlich auf die Funktionen memcpy und memset abgebildet.

    unsigned char *mem_io_address = 0xd00000;
    unsigned char internal_buffer[128];

    ...
    // Kopiere den internen Buffer in den Speicher der Peripherie
    memcpy_toio( mem_io_address, internal_buffer, sizeof(internal_buffer) );
    ...

5.5.3.2. Portzugriff

Da für den Zugriff auf IO-Ports auf einigen Prozessor-Architekturen (z.B. Intel) eigene Befehle zur Verfügung stehen, kann auf diese Hardware nur über Makros zugegriffen werden. Insgesamt stehen drei Gruppen von Makros zur Verfügung:

  1. Makros für den normalen Zugriff auf IO-Ports:

  2. Da für bestimmte Hardware der normale Zugriff zu schnell ist, gibt es Makros, die nach dem Zugriff noch eine kurze Pause einlegen:

  3. Makros für wiederholten Zugriff auf IO-Ports (String-Funktionen):

Der Zugriff auf Hardware vollzieht sich damit beispielsweise folgendermaßen:

#include <asm/io.h>
    ...
    unsigned long io_status;

    ...
    io_status = inl( 0x3f8 ); // lese 4 Bytes
    ...

Beim Zugriff auf Hardware ist es manchmal notwendig, eine sehr kurze Pause einzulegen. Ist diese Pause nur einige Mikrosekunden lang, lohnt sich ein Prozesswechsel nicht. In diesem Fall ist es günstiger, die Wartezeit »aktiv« (durch Nichtstun) zu überbrücken. Man spricht von einer so genannten »Busyloop«, bei der der Betriebssystemkern für eine definierte Zeit eine Variable hochzählt.

Dem Treiberentwickler steht dazu die Funktion udelay zur Verfügung. Diese verzögert die Bearbeitung um die als Parameter übergebene Zeit in Mikrosekunden. Die mittels udelay maximal zu überbrückende Zeit beträgt 1ms (siehe Abschnitt Aktives Warten).

    outb( 0x01, 0x300 ); // starte Auftrag
    udelay( 20 );        // warte 20 Mikrosekunden
    result=inb( 0x301 ); // lies das Ergebnis ein

Abbildung 5-8. Das Steuerregister des PIT

Beispiel Zugriff auf Portadressen am Beispiel PC-Speaker zeigt einen Treiber, der auf den im PC eingebauten Lautsprecher (auch Speaker genannt) Töne ausgibt. Drei Portadressen bedienen den Speaker: 0x42, 0x43 und 0x61. Die ersten beiden Adressen gehören zum PIT-Chip, dem Programmable Interval Timer. Der Port 0x42 stellt ein Datenregister dar, welches den PIT-Zählerwert aufnimmt. Der Port 0x43 stellt die Betriebsart des Bausteins ein. In dieses Register muss 0xb6 geschrieben werden, damit der PIT ein Rechtecksignal erzeugt, das sich aus dem über Port 0x42 eingestelltem Timerwert und CLOCK_TICK_RATE herleitet (siehe Abbildung Das Steuerregister des PIT). Der eigentliche Timerwert ist ein 2-Byte-Wert, der auch Byte-weise geschrieben werden muss; zunächst das niederwertige, danach das höherwertige Byte. Werden jetzt die beiden niederwertigen Bits von Port 0x61 auf »1« gesetzt, wird das Rechtecksignal auf den Lautsprecher durchgeschaltet und ein Geräusch erzeugt.

Nach dem Übersetzen des Treibers und dem Anlegen einer Gerätedatei ((root)# mknod Speaker c 240 0) kann man mit dem Kommando echo dem Lautsprecher Töne entlocken:

(root)# echo "440" > Speaker
(root)#

Wird auf das Gerät eine »0« geschrieben, schaltet sich der Speaker wieder ab:

(root)# echo "0" > Speaker
(root)#

Anzumerken bleibt, dass der vorgestellte Code unzulänglich ist: Der Treiber verwendet Ressourcen, ohne sie vorher zu reservieren. Wir haben den PC-Speaker gehijackt, da die zugehörigen Ressourcen bereits durch einen anderen Treiber reserviert wurden.

Beispiel 5-21. Zugriff auf Portadressen am Beispiel PC-Speaker

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/io.h>

MODULE_LICENSE("GPL");

static ssize_t driver_write( struct file *instanz, __user const char *user,
    size_t count, loff_t *offs)
{
    u16 tonwert;
    s8 save;
    char buffer[6];

    if( count > sizeof(buffer) ) {
        count = sizeof(buffer);
    }
    copy_from_user( buffer, user, count );
    buffer[sizeof(buffer)-1] = '\0';
    tonwert = (u16) simple_strtoul( buffer, NULL, 0 );
    printk("tonwert=%d\n", tonwert );
    if( tonwert ) {
        tonwert = CLOCK_TICK_RATE/tonwert;
        printk("tonwert=%x\n", tonwert );
        outb( 0xb6, 0x43 );
        outb_p(tonwert & 0xff, 0x42);
        outb((tonwert>>8) & 0xff, 0x42);
        save = inb( 0x61 );
        outb( save | 0x03, 0x61 );
    } else {
        outb(inb_p(0x61) & 0xFC, 0x61);
    }
    return count;
}

static struct file_operations fops = {
    .owner=THIS_MODULE,
    .write=driver_write,    /* write   */
};

static int __init buf_init(void)
{
    if(register_chrdev(240, "PC-Speaker", &fops) == 0) {
        return 0;
    }
    return -EIO;
}

static void __exit buf_exit(void)
{
    outb(inb_p(0x61) & 0xFC, 0x61);
    unregister_chrdev(240,"PC-Speaker");
}

module_init( buf_init );
module_exit( buf_exit );

5.5.4. Hardware erkennen

Bevor auf Hardware zugegriffen werden kann, muss diese mit ihren Ressourcen durch den Treiber erkannt werden. In Zeiten moderner PCI-Hardware ist das sehr einfach geworden. Der Treiber bekommt die Adressen bzw. Kenndaten der Hardware über das IO-Subsystem des Betriebssystemkerns mitgeteilt. Mögliche Adresskonflikte sind dabei – soweit möglich – bereits durch das PCI-Bios aufgelöst worden.

Auch wenn USB-Geräte bedient werden sollen, entfällt für den Treiberentwickler das lästige Suchen der Hardware bzw. das Suchen der zugehörigen Hardwareadressen. Bei einem USB-Treiber sind diese ohnehin nicht notwendig, stellt doch das zugehörige Subsystem Funktionen für den Zugriff auf das Gerät zur Verfügung.

Soll jedoch auf Geräte zugegriffen werden, die direkt mit dem Prozessor über den Systembus gekoppelt sind, oder auch im PC den alten ISA-Bus verwenden, muss zunächst eine Hardwaredetektion programmiert werden.

Für diese Hardwaredetektion ist die Kenntnis über mindestens ein spezifisches Merkmal notwendig. Ein solches Merkmal könnte ein markanter Wert (z.B. »0xaa55«) an einer spezifischen Speicherstelle bzw. Portadresse sein. Es könnte aber ebenso die definierte Reaktion des Gerätes auf das Schreiben einer zum Gerät gehörigen Speicherstelle bzw. Portadresse sein. In vielen Fällen verändert die so angesprochene Hardware gezielt den Inhalt einer möglicherweise anderen Speicherstelle.

Abbildung 5-9. Struktogramm Automatische Hardware-Erkennung

Eine Hardware kann im Regelfall auch nicht an jeder x-beliebigen Adresse im Adressraum des Mikroprozessors eingeblendet werden; die möglichen Adresslagen einer Hardware sind beschränkt. Damit ergibt sich der Ablauf der Hardware-Erkennung wie folgt (siehe Abbildung Struktogramm Automatische Hardware-Erkennung): Für jede Adresslage versucht der Treiber, die Adressen beim Betriebssystem zu reservieren und eine Hardware aufgrund des bekannten Merkmals zu identifizieren. Auch wenn ein Gerät erkannt wurde, sollten die übrigen Adresslagen überprüft werden. Schließlich könnten sich in einem Rechner durchaus mehrere Karten des gleichen Typs befinden – und ein Treiber muss derart programmiert sein, dass er mehrere Karten des gleichen Typs auch bedienen kann. Beispiel Hardware-Erkennung stellt ein Implementierungsbeispiel dar.

Beispiel 5-22. Hardware-Erkennung

#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/ioport.h>
#include <asm/io.h>

MODULE_LICENSE("GPL");

#define MAX_BOARDS 4                                       (1)
static int io[] __initdata = { 0x200, 0x300, 0x400, 0x500, 0 };(2)
static int boardnr=0;

struct _board {                                            (3)
    int io;
    // und andere Parameter ...
} board[MAX_BOARDS];

static int __init check_card( int io_port )                (4)
{
    u32 magic;

    if( request_region( io_port, 16, "MeineHardware" )==NULL ) {(5)
        magic = inw( io_port );                            (6)
        if( magic == 0x11223344 ) { // Hardware gefunden   (7)
            pr_info("Hardware gefunden bei 0x%x!\n", io_port );
            return 0;
        }
        release_region( io_port, 16 );
    }
    return -1;
}

static int __init init_driver(void)
{
    int i;

    for( i=0; io[i]!=0; i++ ) {       // Fuer alle Adresslagen(8)
        if( check_card( io[i] )==0 ) { // Karte gefunden   (9)
            board[boardnr].io=io[i];  // HW-Parameter abspeichern(10)
            if( ++boardnr > MAX_BOARDS )
                break;
        }
    }
    if( boardnr == 0 ) {
        return -EIO; // Keine Karten gefunden.
    }
    //...                                                  (11)
    return 0;
}

static void __exit exit_driver(void)
{
    for( ; boardnr; boardnr-- )
        release_region( board[boardnr].io, 16 );
}

module_init( init_driver );
module_exit( exit_driver );
(1)
Ein Treiber sollte mit mehreren Boards (physikalischen Geräten) zurechtkommen. Hier sind maximal vier Boards vorgesehen.
(2)
Dieses Feld definiert die möglichen Portadressen der Hardware. Die Länge des Feldes (Liste) kann einfach bestimmt werden, da das letzte Element zu 0 gesetzt wurde.
(3)
Die Hardware-Parameter werden in einer eigenen Datenstruktur abgespeichert, die für jede unterstützte Hardware einmal vorhanden ist.
(4)
Diese Initialisierungsfunktion ist für die eigentliche Hardwaredetektion zuständig.
(5)
Der IO-Bereich der Hardware wird reserviert. Nur bei erfolgreicher Reservierung darf auf die Peripherie zugegriffen werden. Die Länge des IO-Bereiches beträgt bei dieser Hardware 16 Byte.
(6)
Das erste Wort des Portbereiches enthält das Magic (spezifisches Kennzeichen).
(7)
Falls der an der Portadresse gelesene Wert mit dem bekannten Magic für diese Hardware (0x11223344) übereinstimmt, wurde das Gerät gefunden.
(8)
An allen Portadressen, an denen die spezifische Hardware liegen könnte, wird gesucht.
(9)
Ist ein Gerät identifiziert worden, wird der zugehörige Portbereich für den Treiber reserviert.
(10)
Für jedes physikalische Gerät werden im Treiber notwendige Informationen abgespeichert. Hier ist es der gefundene Portadressbereich.
(11)
Nach der Hardwaredetektion werden die übrigen Initialisierungsaufgaben des Treibers durchgeführt. So muss sich der Treiber beispielsweise noch beim Kernel anmelden.

Der Treiberentwickler muss den Vorgang der Hardwaredetektion entsprechend der eingesetzten Hardware auf das richtige Erkennungsmerkmal und die zu reservierenden und wieder freizugebenden Ressourcen abgleichen. Die erkannte Hardware kann durch den Treiber nur dann bedient werden, wenn alle notwendigen Hardwareressourcen (Speicherbereiche, Interrupts und DMA-Kanäle) erreichbar sind.

Falls möglich, sollte eine Hardwaredetektion immer automatisiert erfolgen.

Ressourcen im Adressraum des Arbeitsspeichers. Die Erkennung von Ressourcen im Adressraum des normalen Arbeitsspeichers (Memory Mapped IO, feste bzw. vordefinierte Speicherbereiche) findet im Regelfall genauso statt wie die beschriebene Erkennung der Portadressen. Gerade bei moderner Hardware lässt sich die Adresslage dieser Bereiche konfigurieren, beispielsweise auch über Portadressen. Ob ein möglicher Speicherbereich bereits belegt oder noch frei ist, erkennt der Treiber anhand des Rückgabewertes der Funktion zur Speicherallozierung (request_mem_region).

Interrupts. Interrupts lassen sich automatisch erkennen, falls es für einen Treiber möglich ist, gezielt einen solchen auszulösen.

Das Prinzip der automatischen Interruptdetektion besteht darin, für alle freien Interrupts eine spezielle Interrupt-Service-Routine zu installieren, deren Aufgabe nur darin besteht, zu protokollieren, ob die ISR aufgerufen wurde oder nicht. Wird durch die Hardware ein Interrupt ausgelöst, kann danach festgestellt werden, welche ISR aufgerufen wurde; der Interrupt wurde erkannt.

Dieser Vorgang wird durch den Betriebssystemkern unterstützt. Die Funktion probe_irq_on installiert die spezifischen Interrupthandler. Die Funktion probe_irq_off gibt den gefundenen Interrupt zurück und macht die zuvor durchgeführten Modifikationen an der Interrupt-Vektor-Tabelle wieder rückgängig. Zwischen dem Aufruf dieser beiden Funktionen muss die Hardware noch vom Treiber angewiesen werden, den Interrupt auszulösen. Damit ergibt sich der folgende Ablauf:

static int irq_probing()
{
    unsigned long irqs;
    int irq;

    sti();
    irqs = probe_irq_on();
    ... // Interrupt auslösen
    irq = probe_irq_off( irqs );
    if( irq<= 0 ) {
        printk("can't determine interrupt\n");
        return -EIO;
    }
    // Eventuell noch Interrupt im Gerät bedienen (resetten)
    return irq;
}

5.5.5. PCI

Die meisten Treiberentwickler dürften es bei der Hardwareanbindung mit dem PCI-Bus (Peripheral Component Interconnect) zu tun bekommen – schließlich ist dieser das in aktuellen Rechnern wohl verbreitetste parallele Bussystem zur Ankopplung von Geräten (Hardware) an den Systembus der CPU. Deswegen sei ihm an dieser Stelle ein eigenes Kapitel zugeeignet. Die Besonderheit des PCI-Bussystems im Unterschied zu anderen parallelen Bussystemen liegt vor allem darin, dass den Geräten automatisiert Ressourcen (Speicheradresse, Portadressen und Interruptleitungen) mitgeteilt werden, so dass es zu keinen Ressourcenkonflikten kommt.

Ist ein Gerät über den PCI-Bus angekoppelt, übernimmt der Betriebssystemkern in Form des PCI-Subsystems die eigentliche Hardware-Erkennung. Mit Hilfe der durch das PCI-Subsystem übergebenen Informationen kann die Reservierung der zugehörigen Ressourcen (siehe Kapitel Ressourcen reservieren) und der eigentliche Zugriff auf die Hardware (siehe Kapitel Direkter Hardwarezugriff) in bereits beschriebener Weise durchgeführt werden.

Damit entfällt bei der Erstellung eines Treibers schon einmal der Schritt der Hardware-Erkennung. Der wichtigste Unterschied zu den bisher vorgestellten Treibern besteht jedoch in der Aufsplittung der Initialisierungsphase in eine Treiber- und eine Geräteinitialisierung. Die Treiberinitialisierung findet wie gewohnt in der Funktion init_module statt. Hier meldet sich der Treiber sowohl beim IO-Subsystem (mit register_chrdev) als auch beim PCI-Subsystem (pci_module_init oder eventuell pci_register_driver) an.

Die Geräteinitialisierung hingegen erfolgt in einer ausgelagerten Funktion (pci_probe, im Kernel auch gerne als pci_init_one bezeichnet), deren Adresse dem PCI-Subsystem bei der Anmeldung (pci_module_init bzw. pci_register_driver) mit übergeben wird.

Zur Anmeldung beim PCI-Subsystem stehen die beiden Funktionen pci_module_init und pci_register_driver zur Verfügung. Im Regelfall fährt der Treiberentwickler mit der ersten Funktion (pci_module_init) besser. Ihr Vorteil: Um Ressourcen zu sparen, wird der Treiber nur dann erfolgreich initialisiert, wenn auch ein Gerät vorhanden ist. Dabei wird beim Kompiliervorgang berücksichtigt, ob ein nachträgliches Laden des Treibers möglich ist oder nicht.

Die Differenzierung zwischen der Treiberinitialisierung und der Geräteinitialisierung ist aus dem Grunde notwendig, weil der Treiber erst dann eine Reservierung der Hardwareressourcen und die Initialisierung des Gerätes vornehmen kann, wenn das Gerät physikalisch vorhanden ist. PCI-Geräte sind aber prinzipiell »hotpluggable devices«, auch wenn die PCI-Hardware in den meisten Fällen fest eingebaut ist und damit während des Betriebes weder entfernt noch hinzugesteckt werden kann. Bei einem »hotpluggable device« kann der Entwickler nicht davon ausgehen, dass das Gerät beim Laden bzw. bei der Initialisierung des Treibers vorhanden ist (insbesondere nicht, wenn es sich um einen Built-in-Treiber handelt).

Abbildung 5-10. Zusammenspiel mit dem PCI-Subsystem

So wie die Treiberinitialisierung und die Geräteinitialisierung getrennt wurden, sind auch die Gerätedeinitialisierung und die Treiberdeinitialisierung getrennt worden. Sobald ein Gerät entfernt wird oder sich der Treiber vom PCI-Subsystem wieder abmeldet, ruft dieses die vom Treiber bei der Treiberinitialisierung übergebene Funktion pci_remove auf. Diese gibt die für den Treiber allozierten Ressourcen wieder frei.

Damit das PCI-Subsystem den zu einem Gerät gehörenden Treiber identifizieren kann, besitzt jedes Gerät eine eindeutige Identifikation. Der Treiber seinerseits übergibt dem PCI-Subsystem eine Liste solcher Identifikationsnummern, deren zugehörigen Geräte er bedienen kann. Sobald das PCI-Subsystem ein Gerät identifiziert hat, ruft es die Probe-Funktion des entsprechenden Treibers auf. Der Treiber kann jetzt selbst entscheiden, ob er das Gerät bedienen möchte oder nicht. Wenn ja, gibt er am Ende der Probe-Funktion eine »0« zurück. Jeder andere Wert dagegen bedeutet, dass das PCI-Subsystem einen anderen Treiber suchen muss.

Die Identifikation eines Gerätes besteht aus den in Abbildung Die Initialisierung der Struktur pci_device_id dargestellten Elementen, wobei die Geräteklasse aus 3 Teilen besteht:

Abbildung 5-11. Die Initialisierung der Struktur pci_device_id

Wird der Wert »0« für Klasse, Unterklasse und Programmierinterface verwendet, so gilt die Klasse als nicht spezifiziert. Das PCI-Subsystem wird die Probe-Funktion unabhängig von der Geräteklasse aufrufen.

Ähnlich wirkt das Define PCI_ANY_ID für die in Abbildung Die Initialisierung der Struktur pci_device_id dargestellten vier Hersteller- bzw. Gerätekennungen. Dieses Define steht für »alle Kennungen«. Übergibt also ein Treiber für die vier Kennungen PCI_ANY_ID und für die Geräteklasse den Wert »0«, wird die Probe-Funktion des Treibers bei jedem neu entdeckten bzw. bisher noch durch keinen Treiber bedienten Gerät aufgerufen.

Die Kennungen werden in der Datenstruktur struct pci_device_id spezifiziert. Neben den vier Kennungen, der Geräteklasse und der Gerätemaske enthält diese Datenstruktur noch ein Feld, um private, treiberspezifische Daten unterzubringen (siehe wiederum Abbildung Die Initialisierung der Struktur pci_device_id). Der hier abgelegte Wert (zumeist als Zeiger auf eine treiberinterne Datenstruktur verwendet) wird der Probe-Funktion beim Aufruf durch das PCI-Subsystem mit übergeben.

Die (treiberspezifische) Instanzierung der Datenstruktur struct pci_device_id wird in eine Datenstruktur vom Typ struct pci_driver eingehängt. In diesem Objekt werden zusätzlich der Name des Treibers und die Adressen der Probe- und der Remove-Funktionen spezifiziert. Unterstützt der PCI-Treiber auch Powermanagementfunktionen, sind weitere Funktionsadressen anzugeben (siehe Deklaration der Datenstruktur in <linux/pci.h>).

Beispiel 5-23. PCI-Treiber-Initialisierung

static struct pci_device_id pci_drv_tbl[] __devinitdata = {(1)
    { MY_VENDOR_ID, MY_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 },
    { 0, }                                                 (2)
};

static struct pci_driver pci_drv = {                       (3)
    .name=     "pci_drv",
    .id_table= pci_drv_tbl,
    .probe=    device_init,                                (4)
    .remove=   device_deinit,
};

static int __init pci_drv_init(void)                       (5)
{
    if((my_major_nr=register_chrdev(0, "PCI-Driver", &pci_fops))) {(6)
        if( pci_module_init(&pci_drv) < 0 ) {              (7)
            unregister_chrdev(my_major_nr,"PCI-Driver");
            return -EIO;
        }
        return 0;
    }
    return -EIO;
}

static void __exit pci_drv_exit(void)
{
    pci_unregister_driver( &pci_drv );                     (8)
    unregister_chrdev(my_major_nr,"PCI-Driver");
}

module_init(pci_drv_init);
module_exit(pci_drv_exit);
(1)
Eine Datenstruktur vom Typ struct pci_device_id mit dem Namen pci_drv_tbl wird hier instanziert. Der Treiber bedient ein Gerät mit der Kennung MY_DEVICE_ID vom Hersteller mit der Kennung MY_VENDOR_ID. Die Geräteklasse (hier »0«) und die zugehörige Maske (ebenfalls »0«) sind nicht angegeben. Ebenso gibt es keine spezifischen Daten (»0«).
(2)
Ein Treiber kann mehrere Einträge spezifizieren. Das letzte Element in der Tabelle ist durch eine Vendor-ID von »0« gekennzeichnet.
(3)
Ein Objekt vom Typ struct pci_driver muss angelegt werden. Dieses Objekt wird mit dem Namen des Treibers, den Adressen der Probe- und der Remove-Funktion und der Tabelle mit den Gerätekennungen initialisiert.
(4)
Die Geräte-Initialisierungsfunktion ist in diesem (unvollständigen) Beispiel noch nicht auskodiert.
(5)
Modulinitialisierungsfunktion.
(6)
Der Treiber meldet sich normal als Character-Device beim IO-Subsystem an.
(7)
Der Treiber meldet sich beim PCI-Subsystem an und übergibt diesem das Objekt pci_drv.
(8)
Wird das Modul entladen, muss vorher die Gerätedeinitialisierung stattfinden. Das PCI-Subsystem ruft die entsprechende Funktion auf, sobald sich der Treiber abmeldet.

Die eigentliche Geräteinitialisierung mit der zugehörigen Reservierung der Ressourcen findet in der Probe-Funktion des Treibers statt. Falls der Treiber das angebotene Gerät übernehmen möchte, wird dieser das Gerät mit Hilfe der Funktion pci_enable_device aktivieren. Außerdem muss als Teil der Geräteinitialisierung die Ressourcen-Reservierung vorgenommen werden. Hierzu wiederum ist es erforderlich, herauszufinden, an welchen Adressen die Register des Gerätes zu finden sind.

Abbildung 5-12. PCI-Konfigurationsspeicher

Jedes PCI-Gerät kann bis zu 6 unterschiedliche Speicher- oder IO-Bereiche besitzen. Die Adressen, die Art und die Größe dieser Bereiche werden in Zellen, die im Konfigurationsspeicher der PCI-Hardware mit den Namen Base Address 0 bis Base Address 5 gekennzeichnet sind, abgelegt. Da die Informationen über Adresslage, Art und Größe in einem 32-Bit-Wert kodiert sind, muss der Treiber eine entsprechende Dekodierung der Werte vornehmen.

Dabei helfen dem Treiberentwickler im Headerfile <linux/pci.h> definierte Makros. Alle Makros haben zwei Parameter, zum einen ein Zeiger auf Daten zum Gerät (struct pci_dev), welche bei Aufruf der Geräteinitialisierungsfunktion (Probe-Funktion) mit übergeben wurde, und die Nummer der Zelle im Konfigurationsspeicher (von 0 bis 5). Das Makro pci_resource_start liefert die Anfangsadresse des Speicherbereiches zurück. Jeder Speicherbereich hat eine Mindestlänge von 16 Byte. Die genaue Länge lässt sich über das Makro pci_resource_len in Erfahrung bringen. Darüber hinaus kann sich der Treiberentwickler das Ende des Speicherbereiches mit Hilfe des Makros pci_resource_end angeben lassen. Ob es sich bei der definierten Ressource um eine Speicher- oder eine IO-Adresse (Zugriff über normalen Speicherzugriff oder über Portbefehle) handelt, lässt sich mit Hilfe des Makros pci_resource_flags herausbekommen. In der Header-Datei <linux/ioport.h> sind die Flags IORESOURCE_IO, IORESOURCE_MEM, IORESOURCE_IRQ und IORESOURCE_DMA definiert.

Da im Regelfall vorher bekannt ist, welche Ressourcen eine PCI-Hardware enthalten muss, dienen die Flags vorwiegend der »Gegenprobe«.

Ob sich die in Base Address 0 angegebene Ressource im IO-Bereich des Prozessors oder im Bereich des Speichers befindet, lässt sich mit folgendem Codefragment verifizieren:

    if( (pci_resource_flags(dev,0) & IORESOURCE_IO) ) {
        // Ressource im IO-Adressraum
        ...
    } else if( (pci_resourceflags(dev,0) & IORESOURCE_MEM) ) {
        // Ressource im Speicher-Adressraum
        ...
    } ...

Neben den auch in anderen Subsystemen des Linux-Kernels verwendeten Flags gibt es noch spezifische PCI-Flags. Diese können bei PCI gleichwertig zu den vorgestellten Flags verwendet werden. Das gilt vor allem für die Flags PCI_BASE_ADDRESS_SPACE_IO und PCI_BASE_ADDRESS_SPACE_MEMORY. Insbesondere müssen die PCI-Flags verwendet werden, wenn es um weitere Informationen geht, die im Ressourcenfeld kodiert sind. Dabei geht es um die Frage, ob die Adresse eine 32-Bit (PCI_BASE_ADDRESS_MEM_TYPE_32) oder eine 64-Bit-Adresse (PCI_BASE_ADDRESS_MEM_TYPE_64) ist und darum, ob die PCI-Bridge aus Optimierungsgründen vorzeitige Zugriffe durchführen darf (PCI_BASE_ADDRESS_MEM_PREFETCH) oder eben nicht. Ein gesetztes Bit bedeutet hier, dass nicht im Prefetch-Modus zugegriffen wird. Im Prefetch-Modus speichert die PCI-Bridge (d.h. der Baustein, der den PCI-Bus an den Prozessorbus ankoppelt) Daten zwischen, bevor die eigentliche Übertragung zur CPU (lesen oder schreiben) gestartet wird. Dieser Modus ermöglicht also einen optimierten Zugriff auf das Gerät.

Beispiel 5-24. PCI-Geräteinitialisierung

...
static unsigned long ioport=0L, iolen=0L, memstart=0L, memlen=0L;
...
static int device_init(struct pci_dev *device, const struct pci_device_id *id)(1)
{
    pci_enable_device( device ); // Geraet aktivieren
    if( request_irq(device->irq,pci_isr,SA_INTERRUPT|SA_SHIRQ,"pci_drv",device) ) {(2)
        return -EIO;
    }
    ioport = pci_resource_start( device, 0 );              (3)
    iolen = pci_resource_len( device, 0 );
    if( request_region( ioport, iolen, device->dev.kobj.name )==NULL ) {(4)
        free_irq( Device->irq, device );
        return -EIO;
    }
    memstart = pci_resource_start( device, 1 );            (5)
    memlen = pci_resource_len( device, 1 );
    if( request_mem_region( memstart, memlen, device->dev.kobj.name )==NULL ) {
        release_region( ioport, iolen );
        return -EIO;
    }
    return 0;
}

static void device_deinit( struct pci_dev *device )
{
    free_irq( device->irq, device );
    if( ioport )
        release_region( ioport, iolen );
    if( memstart )
        release_mem_region( memstart, memlen );
}
...
(1)
Die Struktur »device« enthält alle notwendigen Informationen über die PCI-Hardware, insbesondere bezüglich der zu reservierenden Ressourcen.

Die Struktur »id« enthält die Identifikation (Kennungen) der PCI-Hardware. Da bei der Treiberinitialisierung nur eine Identifikation mit angegeben wurde, mithin also nur ein Gerät angesprochen ist, müssen die Kennungen in diesem Fall nicht nochmals verifiziert werden.

(2)
Eine Hardware-Erkennung ist nicht mehr notwendig. Der Treiber kann die vom PCI-Bios erkannten bzw. zugewiesenen Ressourcen direkt reservieren. Hier wird der Interrupt reserviert. Dabei ist es notwendig, eine dev_id anzugeben. Damit diese eindeutig ist, wird hier die Adresse der struct pci_dev (hier mit dem Namen »Device«) selbst verwendet. Um den Interrupt wieder freizugeben, wird genau die gleiche Adresse verwendet. Sobald ein Interrupt auftritt, wird die Funktion pci_isr aufgerufen.
(3)
Über die Makros werden die Startadresse der IO-Ressource und die Länge der IO-Ressource bestimmt. Die Ressource ist im Feld Base Address 0 des PCI-Konfigurationsspeichers spezifiziert.
(4)
Dass es sich um einen IO-Bereich handelt, ist vorher (aus dem Hardwaremanual) bekannt gewesen. Der IO-Bereich kann reserviert werden.
(5)
Eine Speicherressource ist im Feld Base Address 1 spezifiziert.

Die modifizierte Treiber- und Geräteinitialisierung macht den wesentlichen Unterschied zu den bisher vorgestellten Treibern aus. Die eigentlichen Hardwarezugriffe können nach der Geräteinitialisierung wie gewohnt erfolgen (siehe Kapitel Direkter Hardwarezugriff).

Direkter Zugriff auf den Konfigurationsspeicher. Der 256 Byte große PCI-Konfigurationsspeicher (siehe Abbildung PCI-Konfigurationsspeicher) enthält neben den erwähnten Feldern für die Ressourcenspezifikation weitere Informationen, die über die Funktionen pci_read_config_byte, pci_read_config_word oder pci_read_config_dword ausgelesen werden können:

Vendor ID

Der Hersteller eines Boards wird über die Vendor ID identifiziert. Die Vendor ID für Intel lautet beispielsweise »0x8086«, die für AMD »0x1022«.

Device ID

Ein Gerät bzw. ein Board wird über die Device ID identifiziert. Diese Nummer wird vom jeweiligen Hersteller vergeben. Die Kombination von Vendor ID und Device ID identifiziert ein Board eindeutig.

Command Register

Über dieses Register lassen sich einige Funktionalitäten (beispielsweise der Busmaster Transfer) kontrollieren.

Status Register

Gerätezustände werden über das Status Register angezeigt, so zum Beispiel Paritätsfehler.

Class Code

Geräteklassen legen die Funktionalität und das Zugriffsverfahren auf bestimmte Geräte (z.B. Videogerät) fest. Damit soll ermöglicht werden, dass ein Treiber Geräte unterschiedlicher Hersteller bedienen kann (z.B. für Tastatur oder Maus).

Base Address

Bis zu 6 unterschiedliche Speicher- oder IO-Bereiche kann ein Gerät besitzen. Die Adressen dieser Bereiche werden in den Zellen Base Address 0 bis Base Address 5 abgelegt. In einer solchen Speicherzelle sind die Adresslage, die Art und auch die Größe des Speicherbereiches kodiert.

Ein Speicherbereich hat eine Mindestgröße von 16 Byte und ist immer ein Vielfaches von 2. Die vier unteren Bits stehen für zusätzliche Kodierungen zur Verfügung. Bit 0 gibt Auskunft darüber, ob es sich um eine IO-Adresse (der Zugriff muss über Port-Befehle erfolgen) oder eine Memory-Adresse handelt. Handelt es sich um eine Memory-Adresse, geben Bit 1 und 2 den Adresstyp an. »00« signalisiert, dass es sich um eine gewöhnliche 32-Bit-Adresse handelt. Die Kombination »01« bedeutet, dass die Adresse unterhalb der aus alten Zeiten bekannten 1-Megabyte-Grenze liegt, und die Kombination »10« wird für eine 64-Bit-Adresse verwendet. Bei einer 64-Bit-Adresse werden zwei »Base Address«-Einträge zur Adressbestimmung gebraucht.

Bit 3 schließlich gibt Auskunft darüber, ob auf diesen Adressbereich im Prefetch-Modus zugegriffen werden darf oder nicht.

Laut Spezifikation sind nur die Bereiche der Base Address auch schreibbar, die die Adresslage festlegen. Die übrigen Bits bleiben »0«. Dieser Umstand wird genutzt, um die Größe eines Adressbereiches zu bestimmen. Dazu beschreibt man das Adressregister mit lauter Einsen und liest das Register wieder aus. Die Bits, die jetzt mit »0« belegt sind, identifizieren die Adressbereichgröße.

Das folgende Codefragment zeigt, wie die Größe des Speicherbereiches bestimmt werden kann:

    pci_read_config_dword( pcidev, PCI_BASE_ADDRESS_0, &originalvalue );
    pci_write_config_dword( pcidev, PCI_BASE_ADDRESS_0, 0xffffffff );
    pci_read_config_dword( pcidev, PCI_BASE_ADDRESS_0, &base );
    pci_write_config_dword( pcidev, PCI_BASE_ADDRESS_0, originalvalue );
    printk("size of base address 0: %i\n", ~base+1 );

IRQ-Line

In dieser Speicherzelle findet sich der Interrupt, der dem Board vom PCI-Bios zugeteilt wurde.

IRQ-Pin

Ob das Gerät Interrupts unterstützt oder nicht, wird in dieser Speicherzelle durch die Hardware abgelegt.

Tabelle 5-4. Die 7 Elemente des PCI-Treibers [EDITORS NOTE: als Kasten ausprägen]

NummerFunktionBeschreibung
1. struct pci_device_id Spezifikation der Geräte, für die der Treiber zuständig ist.
2. struct pci_driver Spezifikation der Funktionen zur Geräteinitialisierung und Gerätedeinitialisierung.
3. pci_module_init Anmelden des Treibers beim PCI-Subsystem.
4. device_init Geräteinitialisierung: Reservierung der Ressourcen.
5. pci_enable_device Aktivierung der PCI-Hardware.
6. device_deinit Gerätedeinitialisierung: Freigabe der Ressourcen.
7. pci_unregister_driver Abmelden des Treibers beim PCI-Subsystem, aufgerufen in der Funktion zur Treiberdeinitialisierung.

Beispiel Template für einen eigenen PCI-Treiber stellt in einem lauffähigen Codefragment die für einen PCI-Treiber im Wesentlichen zu kodierenden Besonderheiten vor.

Beispiel 5-25. Template für einen eigenen PCI-Treiber

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/interrupt.h>

MODULE_LICENSE("GPL");

#define MY_VENDOR_ID 0x10b7                                (1)
#define MY_DEVICE_ID 0x5157

static unsigned long ioport=0L, iolen=0L, memstart=0L, memlen=0L;(2)

static irqreturn_t pci_isr( int irq, void *dev_id, struct pt_regs *regs )
{
    return IRQ_HANDLED;
}

static int device_init(struct pci_dev *dev, const struct pci_device_id *id)(3)
{
    if(request_irq(dev->irq,pci_isr,SA_INTERRUPT|SA_SHIRQ,"pci_drv",dev)) {(4)
        printk( KERN_ERR "pci_drv: IRQ %d not free.\n", dev->irq );
        return -EIO;
    }
    ioport = pci_resource_start( dev, 0 );                 (5)
    iolen = pci_resource_len( dev, 0 );
    if( request_region( ioport, iolen, dev->dev.kobj.name )==NULL ) {
        printk(KERN_ERR "I/O address conflict for device \"%s\"\n",
                dev->dev.kobj.name);
        goto cleanup_irq;
    }
    memstart = pci_resource_start( dev, 1 );
    memlen = pci_resource_len( dev, 1 );
    if( request_mem_region( memstart, memlen, dev->dev.kobj.name )==NULL ) {
        release_region( ioport, iolen );
        printk(KERN_ERR "Memory address conflict for device \"%s\"\n",
                dev->dev.kobj.name);
        goto cleanup_ports;
    }
    pci_enable_device( dev );                              (6)
    return 0;
cleanup_ports:
    release_region( ioport, iolen );
cleanup_irq:
    free_irq( dev->irq, dev );
    return -EIO;
}

static void device_deinit( struct pci_dev *pdev )          (7)
{
    free_irq( pdev->irq, pdev );
    if( ioport )
        release_region( ioport, iolen );
    if( memstart )
        release_mem_region( memstart, memlen );
}

static struct file_operations pci_fops;

static struct pci_device_id pci_drv_tbl[] __devinitdata = {(8)
    { MY_VENDOR_ID, MY_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 },
    { 0, }
};

static struct pci_driver pci_drv = {                       (9)
    .name= "pci_drv",
    .id_table= pci_drv_tbl,
    .probe= device_init,
    .remove= device_deinit,
};

static int __init pci_drv_init(void)                       (10)
{
    if(register_chrdev(240, "PCI-Driver", &pci_fops)==0) {
        if( pci_module_init(&pci_drv) == 0 )
            return 0;
        unregister_chrdev(240,"PCI-Driver");
    }
    return -EIO;
}

static void __exit pci_drv_exit(void)                      (11)
{
    pci_unregister_driver( &pci_drv );
    unregister_chrdev(240,"PCI-Driver");
}

module_init(pci_drv_init);
module_exit(pci_drv_exit);
(1)
Hier müssen die zum eigenen Gerät gehörenden Vendor- und Device-IDs eingetragen werden.
(2)
Die Adressen der Ressourcen werden normalerweise in einer eigenen Datenstruktur abgelegt. Diese Datenstruktur ist dann so oft vorhanden, wie physikalische Geräte vom Treiber unterstützt werden. Um den Code übersichtlich zu halten, werden hier die Ressourcen direkt in einigen globalen Variablen abgelegt. Allerdings sind hier nur exemplarisch zwei Variablen ausgewählt worden. Sie müssen auch diesen Teil auf ihre konkrete Hardware anpassen.
(3)
Dies ist die eigentliche Geräteinitialisierung. Diese Funktion wird aufgerufen, wenn das PCI-Subsystem glaubt, ein Gerät identifiziert zu haben, welches durch den Treiber bedient werden könnte. Innerhalb dieser Funktion wird jetzt festgestellt, ob das Gerät wirklich zum Treiber gehört und welche Ressourcen es belegt. Die Ressourcen werden reserviert.
(4)
Alle Parameter zum Gerät befinden sich in der Datenstruktur struct pci_dev, so auch die Interruptnummer.
(5)
Unsere Hardware habe im Konfigurationsspeicher 0 eine Port-Ressource spezifiziert. Der Portbereich wird anschließend reserviert.
(6)
Das PCI-Gerät wird schließlich eingeschaltet.
(7)
Dies ist die Geräte-Deinitialisierungsfunktion. Sie wird entweder aufgerufen, wenn das Gerät entfernt wird oder wenn der Treiber entladen wird. Innerhalb der Funktion müssen die reservierten Ressourcen wieder freigegeben werden.
(8)
Dies ist die Liste der PCI-Identifikationen. Die Liste enthält hier nur einen Eintrag. Sie wird mit einem Null-Eintrag abgeschlossen.
(9)
Diese Datenstruktur kennzeichnet das Treiberobjekt.
(10)
Dies ist die Treiberinitialisierung. Während der Initialisierung meldet sich der Treiber beim IO-Subsystem des Kernels und beim PCI-Subsystem an.
(11)
Während der Treiber-Deinitialisierung meldet sich der Treiber sowohl beim PCI-Subsystem als auch beim IO-Subsystem des Kernels wieder ab.


Lizenz