Projekte.PCI-Tutorial-INTKernel (Struktur)


Kernelmodul mit Interrupthandler

Übersicht

Die Quelldateien sind das Kernelmodul [xilinx_pci.c] und das [Makefile] .

Der hier vorgestellte Kerneltreiber zeigt, wie man einen Interrupthandler für PCI-Geräte schreibt und einbindet. Da der zugehörige Hardwareentwurf bis auf die Interruptsignalisierung keine weitere Funktionalität beinhaltet, besteht die Aufgabe des Treiber nur darin, eine Meldung auszugeben, wenn der entsprechende Interrupt auftritt.

Die verwendeten Kernelfunktionen

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ätes auslesen. Das Argument where ist der Byte-Offset vom Anfang des Konfigurationsraums, der durch entsprechende symbolische Konstanten in linux/pci.h definiert wird. Der aus dem Konfigurationsraum geholte Wert wird über ptr zurückgegeben; der Rückgabewert dieser Funktionen ist ein Fehlercode. Die word- und dword-Funktionen konvertieren den gerade gelesenen Wert aus der Little-Endian-Byte-Reihenfolge des Busses automatisch in die native Byte-Reihenfolge des Prozessors.
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);
Zu den Lesefunktionen gibt es auch Funktionen, die ein, zwei oder vier Bytes in den Konfigurationsraum schreiben. dev bezeichnet 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 schreiben.

Bemerkung 1: Der bevorzugte Weg zum Lesen der benötigten Konfigurationsvariablen führt über die Elemente der Struktur struct pci_dev des jeweiligen Gerätes. Dort sind schon die gewünschten Informationen vorhanden (Aufbau der Struktur siehe linux/pci.h). Gleichwohl werden aber auch die gerade genannten Funktionen gebraucht, um eine Konfigurationsvariable zu schreiben und zurücklesen zu können. Da sich Bezeichnungen innerhalb der Struktur im Kernel 2.4 geändert haben (und es im 2.0 Kernel noch gar keine pci_dev-Struktur gab), werden die direkten Lesezugriffe wegen der Abwärtskompatibilität gerne benutzt.

Bemerkung 2: Der vom Speicherraum und den I/O-Ports separate dritte PCI-Konfigurationsspeicherraum ist für den Prozessor nicht direkt erreichbar. Der Datentransfer erfolgt durch das PCI-BIOS über zwei dafür reservierte 32 Bit I/O-Ports, dem Configuration Address Port (Portadressen 0x0CF8 bis 0x0CFB) und dem Configuration Data Port (Portadressen 0x0CFC bis 0x0CFF).

int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),
                unsigned long flags, const char *dev_name, void *dev_id);
Der Kernel verwaltet eine Liste der Interruptleitungen, ähnlich wie die Liste der I/O-Ports. Ein Modul kann und einen Interruptkanal (IRQ) erst nach der Anforderung mit request_irq verwenden. Der Rückgabewert ist 0 im Erfolgsfall und ein negativer Fehlercode im Fehlerfall. Es ist nicht ungewöhnlich, dass die Funktion -EBUSY zurückgibt, um mitzuteilen, dass ein anderer Treiber bereits die gewünschte Interruptleitung belegt. Die Argumente haben die folgenden Bedeutungen:
  • irq ist die angeforderte Interruptnummer.
  • (*handler)(int, void *, struct pt_regs *) ist ein Zeiger auf die zu installierende Handler-Funktion, siehe unten.
  • flags ist eine Bitmaske von Optionen (siehe unten) aus dem Bereich der Interrupt-Verwaltung. Folgende Bits können gesetzt werden: SA_INTERRUPT, SA_SHIRQ und SA_SAMPLE_RANDOM. Wichtig für PCI-Treiber ist das Flag SA_SHIRQ, das einen Interrupt kennzeichnet, den mehrere Geräte teilen können (Shared Interrupt). Mehrere Interrupthandler, die nacheinander aufgerufen werden, können dann auf einen IRQ registriert werden.
  • Der im Parameter dev_name übergebene String wird in /proc/interrupts verwendet, um den Eigentümer des Interrupts anzuzeigen.
  • Bei gemeinsam genutzten Interrupts verwaltet der Kernel eine Liste von Handlern zu dem Interrupt. Zwischen diesen wird mit dev_id unterschieden. Um die Eindeutigkeit sicherzustellen, kann man auf ein Element einer Gerätestruktur des Treibers verweisen. Wenn zwei Treiber NULL als ihre Signatur auf ein- und demselben Interrupt registrieren würden, dann würde spätestens beim Entladen einiges durcheinanderkommen und der Kernel beim Eintreffen eines Interrupts eine Oops-Meldung ausgeben.
void free_irq(unsigned int irq, void *dev_id);
Freigabe des zuvor belegten IRQs irq. Auch hier ist dev_id wieder die eindeutige Signatur, die den Treiber identifiziert.

Beschreibung des Kernelmoduls

Eine allgemeine Beschreibung der Zusammenhänge zwischen den PCI-Interruptleitungen und den bekannten IRQ-Leitungen der Interruptcontroller des PCs sowie des Zuweisungsmechanismus in der Konfigurationsphase (Register Interrupt Line und Interrupt Pin des PCI Configuration Headers) werden im Abschnitt Der PCI Configuration Header beschrieben.

Initialisierung des Moduls: init_module ()

Nach den schon bekannten PCI-Geräteüberprüfungen und dem Ausfassen des I/O-Speicherbereichs wird das Register Interrupt Pin (PCI_INTERRUPT_PIN) des PCI Configuration Headers des Geräts gelesen, um festzustellen, ob eine PCI-Interruptleitung durch das Gerät benutzt wird. (Wenn nicht, erfüllt das Gerät offensichtlich nicht die vom Treiber erwartete Funktion.) Bei Erfolg wird das Konfigurationsbyte Interrupt Line (PCI_INTERRUPT_LINE) gelesen, in das das PCI-BIOS (oder später ein die Hardware-Ressourcen selbst verwaltendes Betriebssystem) in der Konfigurationsphase den zugeteilten Interruptkanal geschrieben hat.
Anschließend wird der Interrupthandler mit request_irq() registriert. Parameter sind neben der gerade ermittelten IRQ-Nummer die Anfangsadresse des Interrupthandlers, die eindeutige Kennzeichnung für diesen Treiber (Zeiger auf die im Treiber verwendete Gerätestruktur), ein Name für den Handler und als Optionen das gesetzte Shared-Interrupt-Flag.

Entladen des Moduls: cleanup_module ()

Mit free_irq() wird die Registrierung des Interrupthandlers wieder im System gelöscht.

Der Interrupthandler: xilinx_pci_intr_handler()

Der Interrupthandler ist eine Funktion beliebigen Namens, bei dessen Aufruf die Interruptsteuerung des Betriebssystems drei Parameter übergibt:
    void xilinx_pci_intr_handler (int irq, void *dev_id, struct pt_regs *regs)
Das erste Argument, irq, gibt die Interruptnummer an. Das zweite Argument, dev_id, ist die bei request_irq() eingetragene eindeutige Kennzeichnung, die hier unverändert an den Handler weitergereicht wird. Normalerweise übergibt man in dev_id einen Zeiger auf die Gerätedatenstruktur, so dass ein Treiber, der mehrere Instanzen des gleichen Gerätes verwaltet, keinen zusätzlichen Code im Interrupthandler benötigt, um herauszufinden, welches Gerät für den aktuellen Interrupt zuständig ist. Das letzte Argument, regs, wird selten verwendet. Es enthält einen Snapshot des Kontexts des Prozessors, bevor dieser in den Interruptcode eintrat. Die Register können zur Überwachung und zum Debuggen verwendet werden, für normale Aufgaben eines Gerätetreibers braucht man sie nicht.

Da es der Interrupthandler dieses Beispiels mit einem Shared Interrupt zu tun haben kann, zerfällt er in zwei Teile. Zunächst wird durch die Abfrage des Statusregisters des PCI-Geräts festgestellt, ob das Gerät Auslöser des Interrupts ist. Ist dies nicht der Fall, beendet sich der Interrupthandler so schnell wie möglich, damit ein weiterer auf diesen IRQ registrierter Handler aufgerufen werden kann. Im zweiten Teil erfolgt dann die Bearbeitung des Interrupts. In diesem Beispiel wird lediglich eine Meldung ausgegeben. Das Interrupt-Flag im Statusregister des PCI-Gerätes wurde schon mit seiner Abfrage ggf. gelöscht, so dass keine weiteren Zugriffe auf das Gerät nötig sind.


Autor: gkemnitz, Letzte Änderung: 14.04.2011 15:09:59


 TU Clausthal 2020  Impressum