Projekte.PCI-Tutorial-MasterKernel (Struktur)


Treiber für den PCI-Initiator mit Burstunterstützung

Übersicht

Die Quelldateien sind: [xilinx_pci.c] [read-mem.c] [target.c] [Makefile] [write-mem.c]

Lesen der Target-Register ohne Kerneltreiber

Das Programm [target.c] ermöglicht das Auslesen der Target-Register der PCI-Karte ohne Kerneltreiber: Über /dev/mem wird auf den Target-Speicherbereich der PCI-Karte zugegriffen. Als Kommandozeilenparameter muss die Basisadresse der Karte angegeben werden, die mit
    cat /proc/pci
ermittelt werden kann. Aufrufbeispiel:
   ./target.c 0xd5001000

Überblick über den Treiber

Die Datei xilinx_pci.c enthält den Treiber-Code. Der Treiber verwendet
  • kmalloc() zur Allokation des DMA-Puffers im Hauptspeicher und
  • Streaming-DMA-Einblendung (streaming DMA mapping) für den geräteseitigen Zugriff.
Diese beiden Aspekte werden umfassend in den folgenden Abschnitten erläutert. Zudem benutzt der Treiber Funktionen zum Schlafenlegen des Prozesses während des laufenden DMA-Transfers und ein Semaphor zur Zugriffsregelung auf das PCI-Gerät (mutual exclusion), was im Quelltext kommentiert ist und hier nicht weiter beschrieben wird. Das Semaphor ist zwingend notwendig, um zu verhindern, dass während eines laufenden DMA-Transfers über die Target-Schnittstelle ein neuer DMA-Transfer initialisiert werden kann.

Zunächst aber der Überblick über den Treiber:

Ablauf eines DMA-Transfers

  1. Ein Prozess, d.h. ein laufendes Programm im Userspace, hat das PCI-Gerät /dev/xilinx_pci geöffnet und startet nun einen write()-Systemaufruf zum Schreiben bzw. einen read()-Systemaufruf zum Lesen von /dev/xilinx_pci durch die entsprechenden Dateifunktionen.
  2. Der Kerneltreiber reagiert auf den read()- bzw. write()-Aufruf, indem er zunächst einen DMA-Puffer im physischen Hauptspeicher alloziert. Im Falle eines Schreibtransfers werden die zu schreibenden Daten aus dem Speicherbereich des Prozesses (Userspace, virtuell) in den DMA-Speicherbereich kopiert.
    [Funktion im Treiber: xilinx_pci_write() bzw. xilinx_pci_read()]
  3. Der Kerneltreiber initialisiert die PCI-Karte (Adresse des DMA-Puffers, Anzahl der zu übertragenden Datenwörter), gibt den Transfer-Startbefehl und versetzt den Prozess in den Zustand "schlafend".
    [Funktion im Treiber: xilinx_pci_transfer()]
  4. Die Hardware führt den Transfer ohne Beteiligung der CPU durch und löst nach Abschluss einen Interrupt aus.
  5. Der Interrupthandler des Treibers bestätigt den Interrupt und weckt den Prozess wieder.
    [Funktion im Treiber: xilinx_pci_intr_handler]
  6. Die xilinx_pci_read()- bzw. xilinx_pci_write()-Funktion wird fortgesetzt. Nach einer Überprüfung, ob der Transfer erfolgreich war, wird im Falle eines Lesetransfers wird zunächst der Inhalt des DMA-Speicherbereichs in den dafür vorgesehenen Userspace-Bereich des Programms umkopiert. Der DMA-Puffer kann wieder freigegeben werden.
    [Funktionen im Treiber: xilinx_pci_write() bzw. xilinx_pci_read() und xilinx_pci_check_transfer()]

Die Userspace-Demoprogramme

Nachdem das Kernelmodul geladen ist, kann mit dem Programm write-mem.c der Inhalt einer Datei bzw. max. die ersten 4 KByte einer Datei, deren Name als Kommandozeilenparameter übergeben wird, in den Puffer der PCI-Karte transferiert werden.
Aufrufbeispiel:
    ./write-mem xilinx_pci.c
Das Programm read-mem.c liest den Puffer der PCI-Karte aus und gibt ihn als ASCII-Text und als 32 Bit Hexzahlen aus. Als Kommandozeilenparameter wird die Anzahl der zu lesenden Bytes (max. 4096) erwartet.
Aufrufbeispiel:
    ./read-mem.c 1000
    ./read-mem.c 4096 | less

DMA-Puffer reservieren

Es ist das Hauptproblem des DMA-Puffers, dass dieser zusammenhängende Seiten im Speicher belegen muss, wenn er mehr als eine Seite braucht. Die Speicherseiten müssen deswegen nebeneinander liegen, weil das Gerät, das Daten über den PCI-Bus transportiert, mit der physischen Adresse arbeitet. Manche Architekturen (nicht der PC) können zwar auch virtuelle Adressen auf dem PCI-Bus verwenden, aber ein portabler Treiber kann sich darauf nicht verlassen.

Außderdem muss darauf geachtet werden, dass die richtige Art von Speicher alloziert wird, wenn der Speicher für DMA-Operationen verwendet werden soll, denn nicht alle Bereiche sind geeignet: Manche PCI-Geräte implementieren den PCI-Standard nicht korrekt oder nicht vollständig und können nicht mit 32-Bit-Adressen arbeiten. Und ISA-Geräte sind natürlich ohnehin auf 16-Bit-Adressen beschränkt. Die meisten Geräte auf modernen Bus-Systemen wie auch alle hier vorgestellten Beispielentwürfe können mit 32-Bit-Adressen umgehen, weswegen die "normale" Speicherallokationen genügt.

Die folgende Aufstellung gibt einen Überblick über die geläufigen Methoden zur Speichereservierung. Details finden sich in den Man-Pages und in der angegebenen Literatur zur Linux-Geträtetreiberprogrammierung.

Allokation mit kmalloc()

Der Allokationsmechanismus von kmalloc() ist ein mächtiges Werkzeug, das wegen seiner Ähnlichkeit zu malloc() leicht erlernt werden kann. Die Funktion ist schnell — sofern sie nicht blockiert — und leert den erworbenen Speicher nicht; der allozierte Bereich enthält immer noch den vorherigen Inhalt. Er ist auch im physikalischen Speicher zusammenhängend.
kmalloc() kann maximal 128 KByte allozieren, bei 2.0-Kerneln geringfügig weniger. Wenn mehr als ein paar Kilobytes benötigt werden, gibt es aber bessere Möglichkeiten, den Speicher anzufordern.
    void *kmalloc (size_t size, int flags);
    void kfree (void *obj);
Das Argument size gibt die gewünschte Speichergröße an und das Argument flags enthält Angaben über die Art und Position des Speicherbereichs (symbolische Konstanten!). Rückgabewert ist bei Erfolg ein Zeiger auf den allozierten Speicher, bei Misserfolg NULL.

Allokation mit __get_free_pages()

Wenn ein Modul große Speicherblöcke allozieren muss, ist es besser, eine seitenorientierte Technik zu verwenden. Die Funktion __get_free_pages() versucht, die angegebene Anzahl physisch zusammenhängender Seiten im Hauptspeicher zu allozieren. Die Größe einer Seite wird über das Makro PAGE_SIZE bestimmt.
In den in den Kernelversion nach Kernel 2.0 können maximal 29=512 Seiten alloziert werden, was bei der üblichen Seitengröße von 4 KB immerhin schon 2 MB ergibt.
    unsigned long __get_free_pages (int flags, unsigned long order);
    void free_pages (unsigned long addr, unsigned long order);
Das Argument flags enthält Angaben über die Art und Position des Speicherbereichs (symbolische Konstanten wie bei kmalloc()). order ist die Zweierpotenz der Anzahl der Seiten, die angefordert oder freigegeben werden sollen (also log2N gerundet mit N = Anzahl der gewünschten Seiten), z.B. order=0 für eine Seite, oder order=3 für acht Seiten. Wenn order zu groß ist, da kein zusammenhängender Speicherbereich der angegebenen Größe vorhanden ist, schlägt die Allokation fehl.

Selbstgemachte Allokation (Do-it-yourself allocation)

Für noch größere Puffer kann man sich das obere Ende des physischen RAMs von vornherein reservieren. Das geschieht durch Übergabe des Arguments mem= an den Kernel.

Beispiel: Sind 32 MByte RAM vorhanden, hält das Argument mem=31MB den Kernel davon ab, das obere MByte zu benutzen. Um auf diesen Speicher zuzugreifen, kann dann im Modul die ioremap()-Funktion verwendet werden:

    dmabuf = ioremap( 0x1F00000 /* Startadresse 31MB */, 0x100000 /* Puffergröße 1MB */);

Allokation zur Boot-Zeit (Boot-Time Allocation)

Die Allokation zur Boot-Zeit ist sehr unelegant und unflexibel. Über entsprechende Funktionen lässt sich physischer Speicher zur Boot-Zeit reservieren. Das setzt jedoch voraus, dass der Treiber fest bzw. direkt in den Kernel gelinkt ist. Oder anders gesagt: Module können keinen Speicher zur Boot-Zeit allozieren.

DMA auf dem PCI-Bus

Der 2.4-Kernel enthält einen flexiblen Mechanismus, der PCI-DMA (das auch als Bus Mastering bezeichnet wird) unterstützt. Dieser kümmert sich um die Details der Puffer-Allokation und auch um Situationen, in denen sich ein Puffer in einer nicht-DMA-fähigen Zone des Speichers befindet - wenn auch nur auf manchen Plattformen und mit dem Nachteil zus&auuml;tzlichen Berechnungsaufwandes. Da alle Beispielentwürfe den vollen 32 Bit Adressraum unterstützen, wird auf diese Spezialfälle nicht näher eingegangen.

DMA-Einblendungen (DMA mappings)

Eine DMA-Einblendung ist eine Kombination aus der Allokation eines DMA-Puffers und dem Erzeugen einer Adresse für diesen Puffer, auf den das PCI-Gerät zugreifen kann. In vielen Fällen bekommt man diese Adresse einfach mit der Funktion virt_to_bus(), manche Hardware benötigt aber das Einrichten von Einblendungsregistern in der Bus-Hardware. Diese Register sind das Peripherie-Äquivalent zu virtuellem Speicher. Auf Systemen, auf denen diese Register verwendet werden, haben die Peripherie-Geräte einen relativ kleinen, reservierten Adressbereich, in dem sie DMA durchführen können. Diese Adressen werden über die Einblendungsregister auf das System-RAM abgebildet. Einblendungsregister haben einige nette Merkmale; darunter können sie mehrere nicht zusammenhängende Seiten im Adressraum des Geräts als zusammenhängend erscheinen lassen. Nicht alle Architekturen haben aber Einblendungsregister, insbesondere die beliebte PC-Plattform nicht.

Die DMA-Einblendung führt einen neuen Typ namens dma_addr_t ein, um Bus-Adressen zu repräsentieren. Die einzigen zulässigen Operationen sind die Übergabe an die DMA-Hilfsroutinen und an das Gerät selbst.

Der PCI-Code unterscheidet zwischen zwei Typen von DMA-Abbildungen, je nachdem, wie lange der DMA-Puffer vorgehalten werden soll:

Konsistente DMA-Einblendungen (consistent DMA mappings)

Diese Einblendungen existieren während der Lebenszeit des Treibers. Ein konsistent eingeblendeter Puffer muss gleichzeitig sowohl der CPU als auch dem Peripherie-Gerät zur Verfügung stehen. Der Puffer sollte auch, wenn möglich, keine Caching-Probleme haben, die dazu führen könnten, dass Aktualisierungen der einen Partie von der jeweils anderen nicht gesehen werden können.
    void *pci_alloc_consistent(struct pci_dev *pdev, size_t size,
                               dma_addr_t *bus_addr);
Diese Funktion erledigt sowohl die Allokation als auch die Einblendung des Puffers. Die ersten beiden Argumente sind die PCI-Gerätestruktur und die Größe des benötigten Puffers in Bytes. Die Funktion gibt das Ergebnis der DMA-Einblendung an zwei Stellen zurück. Der Rückgabewert ist eine virtuelle Kernel-Adresse des Puffers, die vom Treiber verwendet werden kann. Die zugehörige Bus-Adresse wird dagegen in bus_addr zurückgegeben. Die Allokation wird in dieser Funktion erledigt, damit der Puffer an einer Stelle eingerichtet wird, die mit DMA funktioniert; normalerweise wird der Speicher einfach mit get_free_pages() alloziert.

Wenn der Puffer nicht mehr benötigt wird, was normalerweise beim Entladen des Moduls der Fall ist, sollte er mit

    void pci_free_consistent(struct pci_dev *pdev, size_t size,
                             void *cpu_addr, dma_handle_t bus_addr);
an das System zuückgegeben werden. Diese Funktion benötigt sowohl die CPU-Adresse als auch die Bus-Adresse.

Streaming-DMA-Einblendungen (streaming DMA mappings)

Diese Einblendungen werden für eine einzelne Operation eingerichtet. Manche Architekturen ermöglichen in diesem Fall nennenswerte Optimierungen, aber diese Einblendungen unterliegen auch strengeren Zugriffsregeln. Die Kernel-Entwickler empfehlen, die Verwendung von Streaming-Einblendungen gegenüber konsistenten Einblendungen zu bevorzugen, wo immer das möglich ist. Dafür gibt es zwei Gründe: Zunächst verwendet jede DMA-Einblendung auf Systemen, die Einblendungsregister unterstützen, eines oder mehrere dieser Register. Konsistente Einblendungen mit ihrer langen Lebensdauer können diese Register lange Zeit belegen, selbst wenn sie diese gerade nicht brauchen. Der zweite Grund besteht darin, dass Streaming-Einblendungen auf mancher Hardware auf eine Weise optimiert werden können, die mit konsistenten Einblendungen nicht möglich ist.

Streaming-Einblendungen erwarten, es mit einem Puffer zu tun zu haben, der bereits vom Treiber alloziert worden ist. Sie haben es daher mit Adressen zu tun, die sie nicht selbst gewählt haben. So kann der übergebene Puffer auch in einem Bereich liegen, der für den DMA-Transfer ungeeignet ist. Manche Archtekturen geben dann einfach auf, andere behelfen sich mit sogenannten Bounce-Puffern. In dem hier vorgestellten Beispiel wird diese Situation von vornherein vermieden. Außerdem muss bei der Einrichtung einer Streaming-Einblendung dem Kernel mitgeteilt werden, in welcher Richtung sich die Daten bewegen sollen. Dafür sind einige Symbole definiert worden:

PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE
Wenn Daten an das Gerät geschickt werden (vielleicht als Antwort auf einen write()-Systemaufruf), dann sollte PCI_DMA_TODEVICE verwendet werden, bei Daten zur CPU statt dessen PCI_DMA_FROMDEVICE
PCI_DMA_BIDIRECTIONAL
Bidirektionaler Datenverkehr.
PCI_DMA_NONE
Dieses Symbol steht nur als Debugging-Hilfe zur Verfügung. Wenn man versucht, Puffer mit dieser "Richtung" anzulegen, bekommt man eine Kernel-Panik.
Auf manchen Plattformen wird man mit einem Performance-Verlust bestraft, wenn man nicht den richtigen exakten Wert für die Richtung einer Streaming-DMA-Einblendung angibt und stattdessen einfach immer PCI_DMA_BIDIRECTIONAL wählt.

Wenn nur ein einziger Puffer übertragen werden soll, wird er mit

    dma_addr_t pci_map_single (struct pci_dev *pdev, void *buffer,
                               size_t size, int direction);
eingeblendet. Der Rückgabewert ist die Bus-Adresse, die dann an das PCI-Gerät übergeben werden kann, oder NULL, wenn etwas schiefgegangen ist.

Wenn die Übertragung abgeschlossen ist, sollte die Einblendung mit

    void pci_unmap_single (struct pci_dev *pdev, dma_addr_t bus_addr,
                           size_t size, int direction);
wieder aufgeheben werden. Erst dann darf der Treiber auf den Pufferinhalt zugreifen. Die Argumente size und direction müssen identisch mit den zuvor verwendeten sein.

Bei der Verwendung von Streaming-DMA-Einblendungen gelten die folgenden wichtigen Regeln:

  • Der Puffer darf nur für Übertragungen verwendet werden, die mit der bei der Einblendung angegebenen Richtung übereinstimmen.
  • Wenn ein Puffer eingeblendet worden ist, gehört er dem Gerät, nicht dem Prozessor. Bis der Puffer wieder ausgeblendet worden ist, sollte der Treiber den Inhalt in keiner Weise anfassen. Diese Regel impliziert unter anderem, dass ein Puffer, der auf das Gerät geschrieben wird, nicht eingeblendet werden kann, bevor nicht alle zu schreibenden Daten vorliegen.
  • Der Puffer darf nicht ausgeblendet werden, solange die DMA-Übertragung noch in Gang ist, ansonsten ist eine ernsthafte Systeminstabilität geradezu garantiert.


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


 TU Clausthal 2020  Impressum