Direct Memory Access und Bus Mastering

Direct Memory Access oder DMA ist ein fortgeschrittenes Thema, mit dem wir unseren Rundgang durch Speicherfragen beenden. Es handelt sich dabei um den Hardware-Mechanismus, mit dem Peripherie-Geräte ihre I/O-Daten direkt in den oder aus dem Hauptspeicher transportieren können, ohne daß der Systemprozessor an der Übertragung beteiligt sein muß. Die Verwendung dieses Mechanismus kann den Durchsatz vom und zum Gerät deutlich verbessern, weil ein großer Teil des Verarbeitungsaufwandes eliminiert wird.

Um die DMA-Fähigkeiten seiner Hardware ausnutzen zu können, muß der Gerätetreiber in der Lage sein, die DMA-Übertragung korrekt einzurichten und mit der Hardware zu synchronisieren. Wegen seiner Hardware-nahen Natur ist DMA aber leider sehr systemabhängig. Jede Architektur hat ihre eigenen Techniken, um DMA-Übertragungen zu verwalten, und die Programmierschnittstellen sind jedesmal anders. Der Kernel kann keine einheitliche Schnittstelle anbieten, weil ein Treiber nicht genug von der zugrundeliegenden Hardware abstrahieren kann. Einige Schritte in dieser Richtung sind aber in den neuesten Kerneln schon unternommen worden.

Dieses Kapitel konzentriert sich hauptsächlich auf den PCI-Bus, weil er der verbreitetste Peripherie-Bus ist. Viele der Konzepte lassen sich jedoch auch auf andere Bus-Systeme übertragen. Wir werden außerdem kurz behandeln, wie andere Bus-Systeme wie ISA und SBus mit DMA umgehen.

Überblick über eine DMA-Datenübertragung

Bevor wir die Details der Programmierung erklären, schauen wir uns zunächst einmal an, wie eine DMA-Übertragung stattfindet. Dabei beschränken wir uns der Einfachheit halber auf Eingaben.

Datenübertragungen können auf zwei Arten ausgelöst werden: Entweder fragt die Software nach Daten (über eine Funktion wie read), oder die Hardware schiebt die Daten asynchron ins System.

Im ersten Fall können die Schritte folgendermaßen zusammengefaßt werden:

ark=bullet>

Der zweite Fall tritt ein, wenn DMA asynchron verwendet wird. Das passiert beispielsweise bei Datenerfassungsgeräten, die selbst dann Daten erzeugen, wenn niemand diese liest. In diesem Fall sollte der Treiber einen Puffer verwalten, so daß ein späteres read alle angesammelten Daten in den User-Space überträgt. Dabei sind die folgenden Schritte notwendig:

ark=bullet>

Bei Netzwerkkarten sieht man oft eine Variante des asynchronen Ansatzes. Diese Karten erwarten oft einen Ring-Puffer (den sogenannten DMA-Ring-Puffer), der in Speicher untergebracht wird, der zusammen mit dem Prozessor benutzt wird; jedes eingehende Paket wird in den nächsten freien Puffer im Ring gestellt und dann wird ein Interrupt gemeldet. Der Treiber übergibt die Netzwerkpakete dann an den Rest des Kernels und stellt einen neuen DMA-Puffer in den Ring.

Anhand der für beide Fälle erforderlichen Arbeitsschritte sehen Sie schon, daß eine effiziente DMA-Behandlung Interrupts benötigt. Es wäre zwar möglich, DMA mit einem ständig nach Daten fragenden Treiber zu implementieren, würde aber keinen Sinn ergeben, weil ein solcher Treiber die Performance-Gewinne von DMA gegenüber herkömmlicher und einfacherer prozessorgesteuerter I/O wieder verschenken würde.

Ein weiteres wichtiges Element, das hier eingeführt wurde, ist der DMA-Puffer. Um von DMA Gebrauch machen zu können, muß der Treiber einen speziellen Puffer allozieren können, der für DMA geeignet ist. Die meisten Treiber allozieren ihren Puffer übrigens bei der Initialisierung und verwenden ihn bis zum Entladen des Treibers — in der obigen Beschreibung bedeutet allozieren also “auf einen schon allozierten Puffer zugreifen”.

Allozieren der DMA-Puffer

Dieser Abschnitt behandelt die Allokation von DMA-Puffern auf einer niedrigen Ebene; wir führen die Schnittstelle auf höherer Ebene in Kürze ein, aber Sie sollten das hier Beschriebene trotzdem wissen.

Es ist das Hauptproblem des DMA-Puffers, daß dieser zusammenhängende Seiten im Speicher belegen muß, wenn er mehr als eine Seite braucht. Die Speicherseiten müssen deswegen nebeneinander liegen, weil das Gerät die Daten über den ISA- oder den PCI-Bus transportiert, die mit der physikalischen Adresse arbeiten. Diese Einschränkung gilt übrigens interessanterweise nicht für den Bus (siehe “the Section called SBus in Kapitel 15” in Kapitel 15), der virtuelle Adressen benutzt. Manche Architekturen können auch virtuelle Adressen auf dem PCI-Bus verwenden, aber ein portabler Treiber kann sich darauf nicht verlassen.

DMA-Puffer können entweder beim Hochfahren des Systems oder zur Laufzeit alloziert werden, Module können das nur zur Laufzeit tun. In Kapitel 7 wurden diese Techniken eingeführt: “” beschrieb die Allokation zur Boot-Zeit, während “” und “the Section called get_ free_ page und Freunde in Kapitel 7” die Allokation zur Laufzeit erläuterten. Autoren von Gerätetreibern müssen aufpassen, daß sie die richtige Art von Speicher allozieren, wenn der Speicher für DMA-Operationen verwendet werden soll, denn nicht alle Zonen sind geeignet. Insbesondere funktioniert hoher Speicher auf den meisten Systemen nicht für DMA — die Peripherie-Geräte können einfach nicht mit so hohen Adressen umgehen.

Die meisten Geräte auf modernen Bus-Systemen können mit 32-Bit-Adressen umgehen, weswegen normale Speicher-Allokationen dafür problemlos funktionieren. Manche PCI-Geräte implementieren den PCI-Standard aber 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.

Für Geräte mit solchen Einschränkungen sollte der Speicher aus der DMA-Zone alloziert werden, indem das Flag GFP_DMA an kmalloc oder get_free_pages übergeben wird. Wenn dieses Flag angegeben wird, wird nur Speicher alloziert, der mit 16 Bit adressiert werden kann.

Selbstgemachte Allokation

Sie haben schon gelernt, daß get_free_pages (und damit auch kmalloc) nicht mehr als 128 KByte (oder allgemeiner gesprochen 32 Seiten) zusammenhängenden Speichers zurückgeben können. Die Speicheranforderung kann aber auch fehlschlagen, wenn weniger als 128 KByte alloziert werden, weil der Systemspeicher mit der Zeit fragmentiert wird.[1]

Wenn der Kernel nicht die gewünschte Speichermenge zurückgeben kann oder Sie mehr als 128 KByte benötigen (was beispielsweise häufig bei PCI-Framegrabbern der Fall ist), dann kann man auch den Speicher zur Boot-Zeit allozieren oder das obere Ende des physikalischen RAMs für den eigenen Puffer reservieren, anstelle -ENOMEM zurückzugeben. Wir haben die Allokation zur Boot-Zeit in “” in beschrieben, aber Modulen steht diese Technik nicht zur Verfügung. Das Reservieren des oberen RAMs geschieht durch Übergabe des mem=-Arguments an den Kernel. Wenn Sie zum Beispiel 32 MByte haben, können Sie den Kernel mit dem Argument mem=31MB davon abhalten, das obere MByte zu benutzen. Ihr Modul könnte dann später den folgenden Code verwenden, um auf diesen Speicher zuzugreifen:



dmabuf = ioremap( 0x1F00000 /* 31MB */, 0x100000 /* 1MB */);

Es gibt auch noch eine andere Möglichkeit, DMA-Speicher zu allozieren: Sie können aggressive Allokationen durchführen, bis Sie genügend zusammenhängende Seiten bekommen haben, um einen Puffer zu bilden. Wir raten von dieser Allokationstechnik aber dringend ab, wenn es noch irgendeine andere Möglichkeit gibt, Ihr Ziel zu erreichen. Die aggressive Allokation führt zu einer hohen Systemlast und möglicherweise zu einem Einfrieren des Systems, wenn der Grad der Aggressivität nicht richtig angepaßt ist. Andererseits gibt es manchmal aber keine andere Möglichkeit.

In der Praxis ruft der Code kmalloc(GFP_ATOMIC) auf, bis der Aufruf fehlschlägt. Dann wartet der Code, bis der Kernel einige Seiten freigegeben hat, und alloziert alles noch einmal.

Wenn Sie den Pool allozierter Seiten im Auge behalten, werden Sie feststellen, daß früher oder später Ihr DMA-Puffer mit zusammenhängenden Seiten erschienen ist. Dann können Sie alle Seiten außer dem ausgewählten Puffer wieder freigeben. Dieses Verhalten ist aber etwas riskant, weil es zu einem Deadlock führen kann. Wir empfehlen Ihnen, einen Kernel-Timer zu verwenden, um alle Seiten freizugeben, falls die Allokation vor dem Ablauf eines bestimmten Timeouts nicht erfolgreich war.

Wir zeigen den Code hier nicht; Sie finden ihn aber in misc-modules/allocator.c. Der Code ist gründlich kommentiert und dafür gedacht, von anderen Modulen aufgerufen zu werden. Im Gegensatz zu allem anderen Code dieses Buches steht der Allocator unter der GPL. Dazu haben wir uns entschlossen, weil der Code weder besonders schön noch besonders schlau ist, und wenn jemand den Allocator verwenden will, dann sollte der Quelle-Code auf jeden Fall mit dabei sein.

Bus-Adressen

Wenn man mit DMA arbeitet, muß der Gerätetreiber sich mit der an den Bus angeschlossenen Hardware unterhalten, die physikalische Adressen benutzt. Der Programmcode aber verwendet virtuelle Adressen.

Tatsächlich ist die Situation noch ein bißchen komplizierter. DMA-basierte Hardware verwendet keine physikalischen, sondern Bus-Adressen. Auf dem PC sind zwar ISA- und PCI-Adressen mit den physikalischen Adressen identisch, aber das gilt nicht für jede Plattform. Manchmal ist der Bus über einen Brückenschaltkreis angebunden, der die I/O-Adressen auf verschiedene physikalische Adressen abbildet. Manche Systeme haben sogar ein Seiteneinblendungssystem, bei dem beliebige Seiten auf dem Peripherie-Bus als zusammenhängend erscheinen können.

Auf der niedrigsten Ebene (wir werden uns eine Lösung höherer Ebene in Kürze anschauen), stellt der Linux-Kernel eine portable Lösung durch Exportieren der folgenden in <asm/io.h> definierten Funktionen bereit:



unsigned long virt_to_bus(volatile void * address);
void * bus_to_virt(unsigned long address);

Die virt_to_bus-Konvertierung muß verwendet werden, wenn der Treiber eine Adreßinformation an ein I/O-Gerät (wie eine Erweiterungskarte oder den DMA-Controller) schickt, während bus_to_virt benutzt werden muß, wenn Adreßinformationen von an den Bus angeschlossener Hardware ausgelesen wird.

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 kann Bus-Hardware auch für Übertragungen mehrerer Seiten auf einmal einrichten, wenn die Hardware das unterstützt. Dieser Code kümmert sich 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ätzlichen Berechnungsaufwandes (wie wir noch sehen werden).

Die Funktionen in diesem Abschnitt benötigen eine struct pci_dev-Struktur für Ihr Gerät. Die Details zur Einrichtung eines PCI-Geräts werden in Kapitel 15 behandelt. Die hier beschriebenen Routinen können aber auch für ISA-Geräte verwendet werden; in diesem Fall sollte der struct pci_dev-Zeiger einfach als NULL übergeben werden.

Treiber, die die folgenden Funktionen verwenden, sollten <linux/pci.h> einbinden.

Mit schwieriger Hardware umgehen

Die erste Frage, die man vor einer DMA-Übertragung beantworten muß, ist die, ob das jeweilige Gerät eine solche Operation auf dem aktuellen Rechner auch unterstützt. Viele PCI-Geräte implementieren den vollständigen 32-Bit-Bus-Adreßraum nicht, oft, weil sie nur modifizierte Versionen alter ISA-Hardware sind. Der Linux-Kernel versucht zwar, mit solchen Geräten zu arbeiten, aber immer ist das nicht möglich.

Die Funktion pci_dma_supported sollte bei allen Geräten mit Adreß-Einschränkungen aufgerufen werden:


int pci_dma_supported(struct pci_dev *pdev, dma_addr_t mask);

mask ist hier eine einfache Bitmaske, die beschreibt, welche Adreß-Bits das Gerät benutzen kann. Wenn der Rückgabewert von 0 verschieden ist, dann ist DMA möglich, und Ihr Treiber sollte das dma_mask-Feld in der PCI-Gerätestruktur auf den Maskenwert setzen. Bei einem Gerät, das nur mit 16-Bit-Adressen umgehen kann, könnten Sie etwa folgenden Code verwenden:


if (pci_dma_supported (pdev, 0xffff))
    pdev->dma_mask = 0xffff;
else {
    card->use_dma = 0;   /* Wir muessen ohne DMA leben */
    printk (KERN_WARN, "mydev: DMA not supported\n");
}

Ab Kernel 2.4.3 gibt es eine neue Funktion namens pci_set_dma_mask. Diese Funktion hat folgenden Prototyp:


int pci_set_dma_mask(struct pci_dev *pdev, dma_addr_t mask);

Wenn DMA mit der angegebenen Maske unterstützt werden kann, gibt diese Funktion 0 zurück und setzt das Feld dma_mask, ansonsten wird -EIO zurückgegeben.

Bei Geräten, die mit 32-Bit-Adressen umgehen können, ist es nicht sinnvoll, pci_dma_supported aufzurufen.

DMA-Einblendungen

Eine DMA-Einblendung ist eine Kombination aus der Allokation eines DMA-Puffers und dem Erzeugen einer Adresse für diesen Puffer, auf den das Gerät zugreifen kann. In vielen Fällen bekommt man diese Adresse einfach mit 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 Adreßbereich, 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 Adreßraum des Geräts als zusammenhängend erscheinen lassen. Nicht alle Architekturen haben aber Einblendungsregister, insbesondere die beliebte PC-Plattform nicht.

Das Einrichten einer nützlichen Adresse für das Gerät erforert in manchen Fällen auch die Einrichtung eines Bounce-Buffers. Bounce-Buffer werden erzeugt, wenn ein Treiber versucht, DMA mit einer Adresse durchzuführen, die für das Peripherie-Gerät nicht erreichbar ist — etwa mit einer Adresse im hohen Speicher. Dann werden Daten nach Bedarf in und aus dem Bounce-Buffer kopiert. Damit der Code korrekt mit Bounce-Buffern zusammenarbeitet, muß man einigen Regeln folgen, wie wir noch sehen werden.

Die DMA-Einblendung führt einen neuen Typ namens dma_addr_t ein, um Bus-Adressen zu repräsentieren. Variablen des Typs dma_addr_t sollten vom Treiber als opak betrachtet werden; die einzigen zulässigen Operationen sind die Übergabe an die DMA-Hilfsroutinen und 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

Diese Einblendungen existieren während der Lebenszeit des Treibers. Ein konsistent eingeblendeter Treiber muß gleichzeitig sowohl der CPU als auch dem Peripherie-Gerät zur Verfügung stehen (andere Arten von Abbildungen, die wir später anschauen werden, können nur jeweils einem von beiden zur Verfügung stehen). Der Puffer sollte auch, wenn möglich, keine Caching-Probleme haben, die dazu führen könnten, daß Aktualisierungen der einen Partie von der jeweils anderen nicht gesehen werden können.

Streaming-DMA-Einblendungen

Diese Einblendungen werden für eine einzelne Operation eingerichtet. Manche Architekturen ermöglichen in diesem Fall nennenswerte Optimierungen, wie wir noch sehen werden, 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, daß Streaming-Einblendungen auf mancher Hardware auf eine Weise optimiert werden können, wie das mit konsistenten Einblendungen nicht möglich ist.

Die beiden Einblendungstypen müssen unterschiedlich manipuliert werden; schauen wir uns jetzt die Details an.

Konsistente DMA-Einblendungen einrichten

Ein Treiber kann eine konsistente Einblendung durch Aufrufen von pci_alloc_consistent einrichten:


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 unsere PCI-Gerätestruktur und die Größe des benötigten Puffers. 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 (beachten Sie aber, daß die Größe in Bytes und nicht in einer Größenordnung (Zweierpotenz) angegeben wird).

Die meisten Architekturen, die PCI unterstützen, führen die Allokation mit der Priorität GFP_ATOMIC durch und schlafen daher nicht. Die ARM-Portierung ist die Ausnahme von dieser Regel.

Wenn der Puffer nicht mehr benötigt wird (was normalerweise beim Entladen des Moduls der Fall ist), sollte er mit pci_free_consistent an das System zurückgegeben werden:


void pci_free_consistent(struct pci_dev *pdev, size_t size,
                         void *cpu_addr, dma_handle_t bus_addr);

Beachten Sie, daß diese Funktion sowohl die CPU-Adresse als auch die Bus-Adresse benötigt.

Streaming-DMA-Einblendungen einrichten

Streaming-Einblendungen haben eine kompliziertere Schnittstelle als ihr konsistenes Gegenstück. Dafür gibt es eine Reihe von Gründen. Diese Einblendungen erwarten, es mit einem Puffer zu tun zu haben, der bereits von einem Treiber alloziert worden ist, und haben es daher mit Adressen zu tun, die sie nicht selbst gewählt haben. Auf manchen Architekturen können Streaming-Abbildungen auch mehrere, nicht zusammenhängende Seiten und mehrteilige “Scatter-Gather-Buffer” haben.

Wenn Sie eine Streaming-Einblendung einrichten, müssen Sie dem Kernel mitteilen, in welcher Richtung sich die Daten bewegen sollen. Dafür sind einige Symbole definiert worden:

PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE

Diese beiden Symbole sollten einigermaßen selbsterklärend sein. 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 verwenden Sie statt dessen PCI_DMA_FROMDEVICE.

PCI_DMA_BIDIRECTIONAL

Wenn sich die Daten in beide Richtungen bewegen können, dann verwenden Sie PCI_DMA_BIDIRECTIONAL.

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.

Aus einer Reihe von Gründen, die wir gleich kurz ansprechen werden, ist es wichtig, den richtigen Wert für die Richtung einer Streaming-DMA-Einblendung zu wählen. Es kann verlockend sein, immer einfach PCI_DMA_BIDIRECTIONAL zu wählen, aber auf manchen Plattformen wird diese Wahl mit einem Performance-Verlust bestraft.

Wenn Sie nur einen einzigen Puffer übertragen wollen, dann blenden Sie diesen mit pci_map_single ein:


dma_addr_t pci_map_single(struct pci_dev *pdev, void *buffer,
                          size_t size, int direction);

Der Rückgabewert ist die Bus-Adresse, die Sie an das Gerät übergeben können, oder NULL, wenn etwas schiefgegangen ist.

Wenn die Übertragung abgeschlossen ist, sollte die Einblendung mit pci_unmap_single wieder gelöscht werden:


void pci_unmap_single(struct pci_dev *pdev, dma_addr_t bus_addr,
                      size_t size, int direction);

Die Argumente size und direction müssen identisch mit den zuvor bei pci_map_single 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. Erst nach dem Aufruf von pci_unmap_single kann der Treiber gefahrlos auf den Inhalt des Puffers zugreifen (mit einer Ausnahme, die wir gleich sehen werden). Diese Regel impliziert unter anderem, daß 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.

Sie fragen sich vielleicht, warum der Treiber nicht mehr mit dem Puffer arbeiten darf, nachdem dieser eingeblendet worden ist. Dafür gibt es sogar zwei Gründe: Zunächst einmal muß der Kernel sicherstellen, daß alle Daten in diesem Puffer wirklich in den Speicher geschrieben worden sind, wenn ein Puffer für DMA eingeblendet wird. Es ist wahrscheinlich, daß noch Daten im Cache des Prozessors stehen und explizit herausgeschrieben werden müssen. Daten, die vom Prozessor nach dem Herausschreiben in den Puffer geschrieben werden, sind für das Gerät möglicherweise nicht sichtbar.

Zweitens sollten Sie sich überlegen, was passiert, wenn sich der einzublendende Puffer in einem Speicherbereich befindet, der für das Gerät nicht erreichbar ist. Manche Architekturen geben in diesem Fall einfach auf, andere erzeugen aber einen Bounce-Buffer. Der Bounce-Buffer ist einfach ein separater Speicherbereich, der für das Gerät garantiert erreichbar ist. Wenn ein Puffer mit der Richtung PCI_DMA_TODEVICE eingeblendet wird und ein Bounce-Buffer notwendig ist, dann wird der Inhalt des ursprünglichen Puffers als Teil der Einblendung kopiert. Natürlich sind Änderungen am ursprünglichen Puffer nach dem Kopieren nicht für das Gerät sichtbar. Entsprechend werden PCI_DMA_FROMDEVICE-Bounce-Buffer von pci_unmap_single in den ursprünglichen Puffer zurückkopiert; die Daten vom Gerät stehen nicht zur Verfügung, bevor diese Operation nicht abgeschlossen ist.

Bounce-Buffer sind einer der Gründe, warum es wichtig ist, die richtige Richtung zu wählen. PCI_DMA_BIDIRECTIONAL-Bounce-Buffer werden vor und nach der Operation kopiert, was oft eine Verschwendung von CPU-Taktzyklen ist.

Manchmal braucht ein Treiber Zugriff auf den Inhalt eines Streaming-DMA-Puffers, ohne diesen ausblenden zu wollen. Dafür steht eine Funktion bereit:


void pci_sync_single(struct pci_dev *pdev, dma_handle_t bus_addr,
                     size_t size, int direction);

> > Diese Funktion sollte aufgerufen werden, bevor der Prozessor auf einen PCI_DMA_FROMDEVICE-Buffer zugreift sowie nach einem Zugriff auf einen PCI_DMA_TODEVICE-Buffer.

Scatter-Gather-Einblendungen

Scatter-Gather-Einblendungen sind ein Sonderfall vom Streaming-DMA-Einblendungen. Angenommen, Sie haben mehrere Puffer, die alle zum oder vom Gerät transportiert werden müssen. Dies kann aus verschiedenen Gründen der Fall sein, etwa als Folge eines readv- oder writev-Systemaufrufs, einer Cluster-I/O-Anfrage oder einer Liste von Seiten in einem eingeblendeten Kernel-I/O-Puffer. Sie könnten natürlich einen Puffer nach dem anderen einblenden und die gewünschte Operation ausführen, aber es hat seine Vorteile, die gesamte Liste auf einmal einzublenden.

Manche schlauen Geräte können eine Scatter-Liste von Array-Zeigern und -Längen entgegennehmen und alle in einer einzigen DMA-Operation übertragen; "kopierlose" Netzwerkfunktionen sind beispielsweise einfacher, wenn Pakete aus mehreren Stücken zusammengebaut werden können. Linux wird von solchen Geräten in der Zukunft wahrscheinlich viel besser Gebrauch machen. Ein weiterer Grund dafür, Scatter-Listen als Ganzes einzublenden, besteht darin, daß man Systeme besser ausnutzen kann, die Einblendungsregister in der Bus-Hardware haben. Auf solchen Systemen können physikalisch nicht zusammenhängende Seiten aus Sicht des Geräts zu einem einzigen zusammenhängenden Array zusammengebaut werden. Diese Technik funktioniert nur dann, wenn die Länge der Einträge in der Scatter-Liste gleich der Seitengröße ist (mit Ausnahme der ersten und letzten Seite), aber wenn das der Fall ist, dann können mehrere Operationen zu einer einzigen DMA-Operation zusammengefaßt und die Übertragung so deutlich beschleunigt werden.

Wenn schließlich ein Bounce-Buffer verwendet werden muß, dann macht es Sinn, die ganze Liste in einen einzigen Puffer zu zwingen (weil sie ohnehin kopiert wird).

Sie sind jetzt sicherlich überzeugt, daß das Einblenden von Scatter-Listen in manchen Situationen die Mühe lohnt. Der erste Schritt dazu besteht darin, ein Array von struct scatterlist-Strukturen zu erzeugen und aufzufüllen. Dieses Array beschreibt die zu übertragenden Puffer. Diese Struktur ist architekturabhängig und wird in <linux/scatterlist.h> beschrieben. Sie enthält aber immer die beiden folgenden Felder:

char *address;

Die Adresse eines Puffers, der in der Scatter-Gather-Operation verwendet wird

unsigned int length;

Die Länge dieses Puffers

Um eine Scatter-Gather-DMA-Operation einzublenden, sollte Ihr Treiber für jeden zu übertragenden Puffer die Felder address und length in einem struct scatterlist-Eintrag füllen und dann folgende Funktion aufrufen:


int pci_map_sg(struct pci_dev *pdev, struct scatterlist *list,
               int nents, int direction);

Der Rückgabewert ist die Anzahl der zu übertragenden DMA-Puffer. Dies kann weniger als nents, die Anzahl der übergebenen Scatter-List-Einträge sein.

Ihr Treiber sollte jeden von pci_map_sg zurückgegebenen Puffer übertragen. Die Bus-Adresse und die Länge werden in den struct scatterlist-Einträgen gespeichert, aber deren Lage in der Struktur unterscheidet sich von einer Architektur zur nächsten. Zwei Makros sind definiert worden, um portablen Code schreiben zu können:

dma_addr_t sg_dma_address(struct scatterlist *sg);

Gibt die Bus-(DMA-)Adresse dieses Scatter-Listen-Eintrags zurück.

unsigned int sg_dma_len(struct scatterlist *sg);

Gibt die Länge dieses Puffers zurück.

Denken Sie wieder daran, daß sich die Adressen und Längen der zu übertragenden Puffer von denen unterscheiden können, die an pci_map_sg übergeben wurde.

Wenn die Übertragung abgeschlossen ist, wird eine Scatter-Gather-Einblendung durch Aufruf von pci_unmap_sg ausgeblendet:


void pci_unmap_sg(struct pci_dev *pdev, struct scatterlist *list,
                  int nents, int direction);

Beachten Sie, daß nents die Anzahl der ursprünglich an pci_map_sg übergebenen Einträge und nicht die Anzahl der von dieser Funktion zurückgegebenen DMA-Puffer sein muß.

> Scatter-Gather-Abbildungen sind Streaming-DMA-Abbildungen, und es gelten auch die gleichen Zugriffsregeln. Wenn Sie auf eine eingeblendete Scatter-Gather-Liste zugreifen müssen, müssen Sie diese zunächst synchronisieren:


void pci_dma_sync_sg(struct pci_dev *pdev, struct scatterlist *sg,
                     int nents, int direction);

So unterstützen die einzelnen Architekturen PCI-DMA

Wie wir bereits zu Beginn dieses Abschnitts erwähnt haben, ist DMA eine sehr Hardware-spezifische Operation. Die PCI-DMA-Schnittstelle, die wir gerade beschrieben haben, versucht, so viele Hardware-Abhängigkeiten wie möglich zu verstecken. Es scheint aber immer noch einiges hindurch.

M68K, S/390, Super-H

Diese Architekturen unterstützen den PCI-Bus in Version 2.4.0 nicht.

IA-32 (x86), MIPS, PowerPC, ARM

Diese Plattformen unterstützen die PCI-DMA-Schnittstelle, aber dies ist hauptsächlich Fassade. Es gibt keine Einblendungsregister in der Bus-Schnittstelle, weswegen keine Scatter-Listen kombiniert und keine virtuellen Adressen verwendet werden können. Es gibt keine Unterstützung für Bounce-Buffer, weswegen keine Adressen im hohen Speicher eingeblendet werden können. Die Einblendungsfunktionen der ARM-Architektur können schlafen, die der anderen Plattformen nicht.

IA-64

Auch die Itanium-Architektur hat keine Einblendungsregister. Diese 64-Bit-Architektur kann aber einfach Adressen erzeugen, auf die PCI-Peripherie-Geräte nicht zugreifen können. Daher implementiert die PCI-Schnittstelle auf dieser Plattform Bounce-Buffer und erlaubt damit, (scheinbar) jede Adresse für DMA-Operationen zu verwenden.

Alpha, MIPS64, SPARC

Diese Architekturen unterstützen eine I/O-Speicherverwaltungseinheit. In der kernel-Version 2.4.0 verwendet die MIPS64-Portierung diese Fähigkeit nicht, weswegen die PCI-DMA-Implementation hier wie auf IA-32 aussieht. Die Alpha- und SPARC-Portierungen können aber Puffer vollständig ein- und ausblenden und unterstützen Scatter-Gather-Einblendungen vollständig.

Die hier genannten Unterschiede sind für die meisten Treiber-Autoren kein Problem, solange die Schnittstellen-Richtlinien befolgt werden.

Ein einfaches PCI-DMA-Beispiel

Die eigentliche Form der DMA-Operationen auf dem PCI-Bus hängt stark vom jeweils angesteuerten Gerät ab. Daher geht es in diesem Beispiel nicht um ein tatsächlich existierendes Gerät. Es handelt sich vielmehr um einen Bestandteil eines hypothetischen Treibers namens dad (DMA Acquisition Device). Ein Treiber für dieses Gerät könnte folgende Übertragungsfunktion definieren:


int dad_transfer(struct dad_dev *dev, int write, void *buffer,
                 size_t count)
{
    dma_addr_t bus_addr;
    unsigned long flags;

    /* Den Puffer für DMA einblenden */
    dev->dma_dir = (write ? PCI_DMA_TODEVICE : PCI_DMA_FROMDEVICE);
    dev->dma_size = count;
    bus_addr = pci_map_single(dev->pci_dev, buffer, count,
                              dev->dma_dir);
    dev->dma_addr = bus_addr;

    /* Das Geraet einrichten */
    writeb(dev->registers.command, DAD_CMD_DISABLEDMA);
    writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);
    writel(dev->registers.addr, cpu_to_le32(bus_addr));
    writel(dev->registers.len, cpu_to_le32(count));

    /* Die Operation beginnen */
    writeb(dev->registers.command, DAD_CMD_ENABLEDMA);
    return 0;
}

Diese Funktion blendet den zu übertragenden Puffer ein und beginnt die Geräteoperation. Die andere Hälfte der Arbeit muß in der Interrupt-Routine erledigt werden, die etwa so aussehen würde:


void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    struct dad_dev *dev = (struct dad_dev *) dev_id;

    /* Sicherstellen, daß der Interrupt wirklich von unserem Geraet kommt */

    /* Den DMA-Puffer ausblenden */
    pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size,
                         dev->dma_dir);

    /* Erst jetzt ist es sicher, auf den Puffer zuzugreifen, ihn in
       den User-Space zu kopieren usw. */
    ...
}

Natürlich haben wir hier viele Details weggelassen, darunter auch, wie Sie verhindern, daß mehrere simultane DMA-Operationen ausgelöst werden.

Kleiner SBus-Exkurs

> > SPARC-basierte Systeme enthalten traditionell einen von Sun entworfenen Bus namens SBus. Die Behandlung dieses Bus-Systems würde den Rahmen dieses Kapitels sprengen, aber es lohnt sich, ihn kurz zu erwähnen. Es gibt eine Reihe von in <asm/sbus.h> deklarierten Funktionen für DMA-Einblendungen auf dem SBus; diese haben Namen wie sbus_alloc_consistent und sbus_map_sg. Die SBus-DMA-API sieht also mit anderen Worten der PCI-Schnittstelle sehr ähnlich. Bevor Sie anfangen, mit DMA auf dem SBus zu arbeiten, müssen Sie sich die Funktionsdefinitionen genau anschauen, aber die Konzepte entsprechen den hier zum PCI-Bus genannten.

DMA bei ISA-Geräten

Der ISA-Bus kennt zwei Arten von DMA-Übertragungen: native DMA und ISA-Busmaster-DMA. Native DMA verwendet den Standard-DMA-Controller-Schaltkreis auf der Hauptplatine, um die Signalleitungen des ISA-Busses anzusteuern. ISA-Busmaster-DMA wird dagegen vollständig vom Peripherie-Gerät gesteuert. Diese Art von DMA wird selten verwendet und ist es nicht wert, hier besprochen zu werden, weil sie der DMA von PCI-Geräten sehr ähnlich ist, zumindest aus Sicht des Treibers. Ein Beispiel eines ISA-Busmasters ist der 1542-SCSI-Controller, dessen Treiber als drivers/scsi/aha1542.c in den Kernel-Quellen steht.

Bei nativer DMA sind drei Dinge an einer DMA-Übertragung auf dem ISA-Bus beteiligt:

Der 8237-DMA-Controller (DMAC)

Der Controller verwaltet die Informationen über die DMA-Übertragung, also beispielsweise die Richtung, die Speicheradresse und die Größe der übertragenen Daten. Außerdem enthält er einen Zähler, in dem der Zustand der gerade laufenden Übertragung mitgeführt wird. Wenn der Controller ein DMA-Anforderungssignal bekommt, fordert er die Kontrolle über den Bus an und steuert die Signalleitungen, so daß das Gerät seine Daten lesen oder schreiben kann.

Das Peripheriegerät

Das Gerät muß das DMA-Anforderungssignal aktivieren, wenn es bereit ist, Daten zu übertragen. Die eigentliche Übertragung wird vom DMAC gesteuert; das Hardware-Gerät liest oder schreibt Daten sequentiell vom oder auf den Bus, wenn der Controller dem Gerät Bescheid gibt. Es wird üblicherweise einen Interrupt auslösen, wenn die Übertragung beendet ist.

Der Gerätetreiber

Der Treiber hat nur wenig zu tun: Er teilt dem DMA-Controller die Richtung, die Bus-Adresse und die Größe der zu übertragenden Daten mit. Außerdem kommuniziert er mit seinem Peripherie-Gerät, um es für die Übertragung der Daten vorzubereiten, und beantwortet den Interrupt, wenn die DMA-Übertragung beendet ist.

Der ursprünglich im PC verwendete DMA-Controller konnte vier “Kanäle” verwalten. Zu jedem Kanal gehört ein Satz DMA-Register, so daß vier Geräte ihre DMA-Information gleichzeitig im Controller ablegen können. Neuere PCs enthalten das Äquivalent zu zwei DMAC-Geräten:[2] Der zweite Controller (der Master) ist mit dem Systemprozessor verbunden, der erste (der Slave) mit dem Kanal 0 des zweiten Controllers.[3]

Die Kanäle sind von 0 bis 7 durchnumeriert; der Kanal 4 steht ISA-Geräten nicht zur Verfügung, weil er intern verwendet wird, um den Slave-Controller mit dem Master-Controller zu kaskadieren. Damit stehen die 8-Bit-Kanäle 0-3 (auf dem Slave) und die 16-Bit-Kanäle 5-7 auf dem Master zur Verfügung. Die Größe einer DMA-Übertragung wird im Controller als 16-Bit-Wert abgelegt und gibt die Anzahl der Bus-Zyklen an. Die maximale Transfergröße beträgt also 64 KByte beim Slave-Controller und 128 KByte beim Master.

Weil der DMA-Controller eine systemweite Ressource ist, übernimmt der Kernel die Verwaltung. Er verwendet eine DMA-Registratur, um einen Mechanismus zum Anfordern und Freigeben von DMA-Kanälen sowie eine Menge von Funktionen zur Konfiguration der Kanal-Informationen im DMA-Controller bereitstellen zu können.

DMA-Kanäle registrieren

Sie sollten Kernel-Registraturen inzwischen kennen — wir haben sie schon bei I/O-Ports und Interrupt-Leitungen gesehen. Die Registratur für DMA-Kanäle ist den anderen ähnlich. Nach dem Einbinden von <asm/dma.h> stehen die beiden folgenden Funktionen zur Verfügung, mit denen die Zuweisung eines DMA-Kanals angefordert und der Kanal später wieder freigegeben werden kann:



int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);

Das Argument channel ist eine Zahl zwischen 0 und 7, oder genauer gesagt eine positive ganze Zahl, die kleiner als MAX_DMA_CHANNELS ist. Auf dem PC ist MAX_DMA_CHANNELS als 8 definiert, damit es zur Hardware paßt. Das Argument name ist ein String, der das Gerät identifiziert. Dieser Name erscheint in /proc/dma und kann dort von Anwenderprogrammen ausgelesen werden.

Der Rückgabewert von request_dma ist 0 im Erfolgsfall und -EINVAL oder -EBUSY, wenn ein Fehler aufgetreten ist. Der erste Fehlercode bedeutet, daß der angeforderte Kanal außerhalb des zulässigen Bereichs ist, der zweite, daß bereits ein anderes Gerät den Kanal verwendet.

Wir empfehlen Ihnen, bei DMA-Kanälen die gleiche Vorsicht wie bei I/O-Ports und Interrupt-Leitungen walten zu lassen. Es ist sehr viel besser, den Kanal beim open anzufordern, als ihn schon in der Initialisierungsfunktion zu blockieren. Durch das Verzögern der Anforderung ist eine gewisse gemeinsame Nutzung der DMA-Kanäle möglich; so können sich Ihre Soundkarte und Ihre analoge I/O-Schnittstelle einen DMA-Kanal teilen, solange sie ihn nicht zur gleichen Zeit benutzen wollen.

Außerdem sollten Sie den DMA-Kanal anfordern, nachdem Sie die Interrupt-Leitung angefordert haben, und ihn wieder freigeben, bevor Sie die Interrupt-Leitung freigeben. Dies ist die gewöhnliche Reihenfolge für das Anfordern der beiden Ressourcen; wenn Sie dieser Konvention folgen, vermeiden Sie Deadlocks. Beachten Sie, daß jedes Gerät, das DMA verwendet, auch eine IRQ-Leitung benötigt, weil es sonst nicht den Abschluß der Datenübertragung melden könnte.

Typischerweise sieht der Code von open etwa wie der folgende aus, der sich auf ein hypothetisches Modul dad (DMA Acquisition Device) bezieht. Das gezeigte dad-Gerät verwendet einen schnellen Interrupt-Handler, der keine gemeinsam genutzten IRQ-Leitungen unterstützt.



int dad_open (struct inode *inode, struct file *filp)
{
    struct dad_device *my_device;

    /* ... */
    if ( (error = request_irq(my_device.irq, dad_interrupt,
                              SA_INTERRUPT, "dad", NULL)) )
        return error; /* oder blockierendes open implementieren */

    if ( (error = request_dma(my_device.dma, "dad")) ) {
        free_irq(my_device.irq, NULL);
        return error; /* oder blockierendes open implementieren */
    }
    /* ... */
    return 0;
}

Die dazu passende Implementation von close sieht folgendermaßen aus:



void dad_close (struct inode *inode, struct file *filp)
{
    struct dad_device *my_device;

    /* ... */
    free_dma(my_device.dma);
    free_irq(my_device.irq, NULL);
    /* ... */
}

/proc/dma sieht in einem System mit einer installierten Soundkarte so aus:


merlino% cat /proc/dma
 1: Sound Blaster8
 4: cascade

Interessanterweise bekommt der Default-Soundtreiber den DMA-Kanal beim Hochfahren und gibt ihn nie wieder frei. Der Eintrag cascade ist ein Platzhalter und zeigt an, daß der Kanal 4 wie erläutert Treibern nicht zur Verfügung steht.

Mit dem DMA-Controller kommunizieren

Nach der Registrierung ist es die Hauptaufgabe des Treibers, den DMA-Controller entsprechend zu konfigurieren. Diese Aufgabe ist nicht trivial, aber glücklicherweise exportiert der Kernel alle Funktionen, die ein typischer Treiber benötigt.

Der Treiber muß den DMA-Controller konfigurieren, wenn entweder read oder write aufgerufen wird oder wenn asynchrone Übertragungen vorbereitet werden. Das geschieht entweder beim open oder als Antwort auf einen ioctl-Befehl, je nach Treiber und der von ihm implementierten Policy. Der hier gezeigte Code wird typischerweise von read- und write-Gerätemethoden verwendet.

Dieser Abschnitt enthält einen schnellen Überblick über die Interna des DMA-Controllers, damit Sie den hier eingeführten Code verstehen können. Wenn Sie mehr wissen wollen, sollten Sie <asm/dma.h> und Literatur über PC-Hardware lesen. Insbesondere gehen wir hier nicht auf den Unterschied zwischen 8-Bit- und 16-Bit-Übertragungen ein. Wenn Sie einen Gerätetreiber für ISA-Karten schreiben, werden Sie die relevante Information in den Hardware-Handbüchern des Gerätes finden.

Der DMA-Controller ist eine gemeinsam genutzte Ressource, weswegen es zu Verwirrung kommen könnte, wenn mehr als ein Prozessor gleichzeitig versuchen würde, diesen zu programmieren. Aus diesem Grund ist der Controller mit einem Spinlock namens dma_spin_lock geschützt. Treiber sollten diese Sperre aber nicht direkt manipulieren; dafür gibt es zwei Funktionen:

unsigned long claim_dma_lock();

Holt die DMA-Sperre. Diese Funktion sperrt außerdem die Interrupts auf dem lokalen Prozessor, weswegen der Rückgabewert die üblichen “Flags” sind, die zum Wiedereinschalten der Interrupts verwendet werden müssen.

void release_dma_lock(unsigned long flags);

Gibt das DMA-Spinlock zurück und restauriert den vorherigen Interrupt-Status.

Das Spinlock sollte gehalten werden, wenn die im Folgenden beschriebenen Funktionen verwendet werden, aber nicht während der eigentlichen I/O-Operation selbst. Ein Treiber sollte nie schlafen, während er ein Spinlock hält.

Die Information, die in den Controller geladen werden muß, besteht aus drei Teilen: der RAM-Adresse, der Anzahl der atomaren Elemente, die übertragen werden sollen (in Bytes oder Worten), und der Richtung der Übertragung. Dazu werden von <asm/dma.h> die folgenden Funktionen exportiert:

void set_dma_mode(unsigned int channel, char mode);

Gibt an, ob der Kanal vom Gerät lesen (DMA_MODE_READ) oder darauf schreiben (DMA_MODE_WRITE) soll. Es gibt noch einen dritten Modus namens DMA_MODE_CASCADE, der verwendet wird, um die Kontrolle über den Bus aufzugeben. Über Kaskadierung ist der erste Controller mit dem zweiten verbunden; diese Technik kann aber auch von echten ISA-Busmaster-Geräten verwendet werden. Wir werden hier nicht auf das Busmastering eingehen.

void set_dma_addr(unsigned int channel, unsigned int addr);

Weist die Adresse des DMA-Puffers zu. Diese Funktion speichert die 24 niedrigstwertigen Bits von addr im Controller. Das Argument addr muß eine Bus-Adresse sein (siehe “the Section called Bus-Adressen”).

void set_dma_count(unsigned int channel, unsigned int count);

Legt die Anzahl der zu übertragenden Daten fest. Das Argument count bezeichnet auch bei 16-Bit-Kanälen Bytes; in diesem Fall muß der Wert gerade sein.

Außer diesen drei Funktionen gibt es eine Reihe von Verwaltungsfunktionen, die bei der Verwendung von DMA-Geräten eingesetzt werden müssen:

void disable_dma(unsigned int channel);

Ein DMA-Kanal kann im Controller abgeschaltet werden. Das sollte geschehen, bevor der Kanal konfiguriert wird, um einen unzulässigen Betrieb zu verhindern (der Controller wird über 8-Bit-Datenübertragungen programmiert, daher wird keine der obigen Funktionen atomar ausgeführt).

void enable_dma(unsigned int channel);

Diese Funktion teilt dem Controller mit, daß der DMA-Kanal gültige Daten enthält.

int get_dma_residue(unsigned int channel);

Der Treiber muß manchmal wissen, ob eine DMA-Übertragung schon beendet ist. Diese Funktion gibt die Anzahl der Bytes zurück, die noch übertragen werden müssen. Der Rückgabewert nach einer erfolgreichen Übertragung ist 0, und er ist nicht vorhersagbar (aber sicher nicht 0), wenn der Controller noch arbeitet. Diese Nichtvorhersagbarkeit resultiert daraus, daß die verbleibende Anzahl von Bytes ein 16-Bit-Wert ist, der aus zwei 8-Bit-Werten zusammengesetzt wird.

void clear_dma_ff(unsigned int channel)

Diese Funktion setzt das DMA-Flip-Flop zurück. Mit dem Flip-Flop wird der Zugriff auf die 16-Bit-Register gesteuert. Auf die Register wird mit zwei aufeinanderfolgenden 8-Bit-Operationen zugegriffen, und mit dem Flip-Flop wird das niedrigstwertige Byte (bei nicht gesetztem Flip-Flop) beziehungsweise das höchstwertige Byte (bei gesetztem Flip-Flop) ausgewählt. Das Flip-Flop wechselt seinen Zustand automatisch nach einer Übertragung von 8 Bits; der Programmierer muß es nur einmal vor dem Zugriff auf die DMA-Register zurücksetzen.

Mit diesen Funktionen kann ein Treiber die folgende Funktion implementieren, mit der eine DMA-Übertragung vorbereitet wird:



int dad_dma_prepare(int channel, int mode, unsigned int buf,
                    unsigned int count)
{
    unsigned long flags;

    flags = claim_dma_lock();
    disable_dma(channel);
    clear_dma_ff(channel);
    set_dma_mode(channel, mode);
    set_dma_addr(channel, virt_to_bus(buf));
    set_dma_count(channel, count);
    enable_dma(channel);
    release_dma_lock(flags);
    return 0;

Mit der folgenden Funktion kann dann abgefragt werden, ob die DMA-Übertragung erfolgreich beendet wurde:



int dad_dma_isdone(int channel)
{
    int residue;
    unsigned long flags = claim_dma_lock ();
    residue = get_dma_residue(channel);
    release_dma_lock(flags);
    return (residue == 0);
}

Damit muß nur noch die Gerätekarte selbst konfiguriert werden. Diese gerätespezifische Aufgabe wird meistens durch Lesen oder Schreiben einiger weniger I/O-Ports erledigt. Die Geräte unterscheiden sich dabei ziemlich. Beispielsweise erwarten manche Geräte, daß der Programmierer ihnen mitteilt, wie groß der DMA-Puffer ist. Bei anderen wiederum muß der Treiber einen im Gerät fest verdrahteten Wert auslesen. Das Hardware-Handbuch ist beim Konfigurieren des Geräts Ihr einziger Freund.

Fußnoten

[1]

Der Ausdruck Fragmentierung wird normalerweise im Zusammenhang mit Festplatten verwendet und meint dort, daß Dateien nicht in aufeinanderfolgenden Blocks auf dem magnetischen Medium abgelegt werden. Das gleiche Konzept gibt es aber auch im Speicher, wo jeder virtuelle Adreßraum über das gesamte physikalische RAM verteilt werden kann und es schwierig wird, aufeinanderfolgende physikalische Seiten zu bekommen, wenn ein DMA-Puffer angefordert wird.

[2]

Die Schaltkreise sind heute ein Teil des Chipsatzes auf der Hauptplatine, aber noch vor ein paar Jahren waren das zwei separate 8237-Chips.

[3]

Die ursprünglichen PCs hatten nur einen Controller, der zweite wurde in den Rechnern hinzugefügt, die mit einem 286er Prozessor arbeiten. Dieser zweite Controller ist aber als Master verdrahtet, weil er die 16-Bit-Übertragungen erledigt, während der erste nur 8 Bit auf einmal überträgt und vor allem für die Abwärtskompatibilität da ist.