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]
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
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
- 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.
- 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()]
- 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()]
- Die Hardware führt den Transfer ohne Beteiligung der CPU durch und löst nach Abschluss einen Interrupt aus.
- Der Interrupthandler des Treibers bestätigt den Interrupt und weckt den Prozess wieder.
[Funktion im Treiber: xilinx_pci_intr_handler]
- 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
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 2
9=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 log
2N 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.
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