Aus Linux-Magazin 06/2016

Kernel- und Treiberprogrammierung mit dem Linux-Kernel – Folge 86

© psdesign1, Fotolia

Beim DMA-Transfer klemmt sich die CPU vom Bus ab und überlässt einem DMA-Controller die Regie, der Datenblöcke effizienter kopiert als sie selbst. Der Programmierer muss etwas Basiswissen über Speicherverwaltung, Adressvarianten und Cache-Kohärenz vorweisen. Nach dieser Kern-Technik kann er das.

Ein paar Grundlagen in technischer Informatik und etwas Mathematik reichen, um auszurechnen, dass auf der CPU ausgeführter Treibercode die beachtlichen Datenübertragungsraten von SSDs oder Gigabit-Ethernet nicht zustande bringt.

Die meisten Rechnerarten sind deswegen mit einem Controller ausgestattet, der das Kopieren von Daten selbstständig ohne Beteiligung der CPU und schnell genug für SSDs oder modernes Ethernet realisiert. Ob es sich um einen extra Baustein handelt oder ob der Controller zusammen mit der CPU (und anderen Units) auf demselben Chip sitzt, ist funktionell wenig bedeutsam.

Mit einer Quelladresse, einer Zieladresse und der Menge der zu kopierenden Daten gefüttert, kopiert der Controller in Windeseile Daten innerhalb des Hauptspeichers, aber auch zwischen Hauptspeicher und Peripherie beziehungsweise Peripherie und Hauptspeicher. Dazu hijackt er normalerweise den Systembus, greift also ohne Umwege direkt auf den Speicher zu. Daher auch der Name: Direct Memory Access (DMA).

Nützlich nicht nur für schnelle Blocktransfers

Entwickler kopieren aber nicht nur Daten per DMA, sondern realisieren damit Zero-Copy und halten harte Realzeit-Anforderungen ein. Noch pfiffiger: Sie steuern damit Pulsweiten-moduliert Gleichstrommotoren oder andere Hardware an. Wen wundert’s also, dass moderne Prozessoren gleich mehrere DMA-Einheiten mitbringen?!

Der Raspberry Pi beispielsweise bietet 16 DMA-Kanäle, einige davon darf der Programmierer mit selbst geschriebenem Kernelcode und etwas Hintergrundwissen für seine Zwecke einsetzen. Dazu reserviert er einen DMA-Kanal, spezifiziert den Quell- und den Ziel-Adressbereich und startet schließlich den Kopiervorgang. Doch bis ein DMA-Controller die richtigen Daten tatsächlich von A nach B transferiert, gibt es einige Klippen zu umschiffen.

So bietet Linux derzeit kein auf alle unterstützten Plattformen portiertes vollständiges DMA-Subsystem. Es bleibt daher nur, auf plattformspezifische Funktionen auszuweichen. Auf dem Raspberry Pi reserviert die Funktion »bcm_dma_chan_alloc()« im Kernel die DMA-Kanäle (Tabelle 1). Mit den richtigen Parametern aufgerufen, liefert sie die Nummer des reservierten DMA-Kanals. Sind alle Kanäle belegt, quittiert die Funktion den Aufruf mit einem negativen Rückgabewert.

Tabelle 1

DMA-Funktionen auf dem Raspberry Pi

Prototyp Beschreibung
Raspi-spezifisch
int bcm_dma_chan_alloc(unsigned preferred_feature_set, void __iomem  out_dma_base, int *out_dma_irq) reserviert einen DMA-Kanal und gibt die Kanalnummer und die physische Adresse der Controllerregister zurück
int bcm_dma_chan_free(int channel) gibt den reservierten Kanal frei
void bcm_dma_start(void __iomem *dma_chan_base, dma_addr_t control_block) startet den DMA-Vorgang
void bcm_dma_wait_idle(void __iomem *dma_chan_base) wartet aktiv (Busyloop) auf das Ende des gesamten DMA-Transfers
Allgemeine Funktionen
void * dma_alloc_coherent (struct device * dev, size_t size, dma_addr_t * handle, int gfp) reserviert Cache-kohärenten Speicher für den DMA-Transfer
void dma_free_coherent (struct device * dev, size_t size, void * cpu_addr, dma_addr_t handle) gibt den reservierten Speicher frei
dma_addr_t dma_map_single (struct device * dev, void * cpu_addr, size_t size, enum dma_data_direction dir) bereitet einen physisch-kontinuierlichen Speicherbereich für den DMA-Transfer vor
void dma_unmap_single (struct device * dev, dma_addr_t handle, size_t size, enum dma_data_direction dir) macht die Abbildung eines DMA-Speicherbereichs wieder rückgängig

Geschlossene Veranstaltung

Quell- und Zielbereich gibt der Programmierer in einem DMA-Kontrollblock (Control Block, CB) an. Der ist ebenfalls plattformspezifisch. Der Raspberry Pi hält dafür die Datenstruktur »struct bcm2708_dma_cb« vor, die Variablen für die Quelle, für das Ziel, für die Anzahl der zu transferierenden Bytes und einige weitere enthält. Besonders bedeutsam ist »next« , in der entweder »NULL« oder die Adresse eines nachfolgenden Kontrollblocks liegt (Abbildung 1).

Abbildung 1: Der DMA-Controller arbeitet im Endlosbetrieb, wenn die Kontrollblöcke eine geschlossene Liste bilden.

Abbildung 1: Der DMA-Controller arbeitet im Endlosbetrieb, wenn die Kontrollblöcke eine geschlossene Liste bilden.

Hat der DMA-Controller den im Control Block spezifizierten Auftrag abgearbeitet, stoppt er nicht zwingend den Transfer, sondern arbeitet den in »next« referenzierten Control Block ab. Die Kontrollblöcke lassen sich somit in einer Liste hintereinanderhängen. Zeigt der Next-Zeiger des letzten Listenelements wieder auf das erste Element (geschlossene Liste), setzt das sogar einen Endlosbetrieb in Marsch. Erst wenn der Next-Zeiger »NULL« ist, stoppt der DMA-Controller.

Im DMA-Kontrollblock ist zudem noch zu spezifizieren, ob der Controller die Daten innerhalb des normalen Hauptspeichers transferieren soll oder ob Quelle oder Ziel Peripheriebausteine sind, etwa eine serielle Schnittstelle. Genauer gesagt gibt der Programmierer an, ob jede Einzelübertragung zugleich Quell- und Zieladresse inkrementiert oder eben nicht (Abbildung 2). Da die meiste Peripherie über ein einzelnes Register angesprochen wird, ist Inkrementieren oft kontraproduktiv.

Das Element »stride« ermöglicht ein Umsortieren der Daten während des Kopierens. Handelt es sich bei einem zu transferierenden Datenblock beispielsweise um eine Matrix, so lässt sich diese per DMA transponieren, also drehen (Abbildung 3). Das ist nicht nur für Grafikanwendungen ein nützliches Gimmick [1].

Abbildung 2: Per DMA lassen sich Speicherbereiche innerhalb des Hauptspeichers kopieren, aber auch in Kombination mit Peripherie.

Abbildung 2: Per DMA lassen sich Speicherbereiche innerhalb des Hauptspeichers kopieren, aber auch in Kombination mit Peripherie.

Abbildung 3: Das »stride«-Element kann veranlassen, dass der DMA-Controller eine Matrize umsortiert.

Abbildung 3: Das »stride«-Element kann veranlassen, dass der DMA-Controller eine Matrize umsortiert.

Adressen-Wirrwarr

Beim Ausfüllen des Kontrollblocks lauert Gefahr: Durch jedes Linux-System geistern drei Arten von Adressen: die logische, die physische und die Peripherie-Adresse ([2], Abbildung 4). Die logische (oder virtuelle)Adresse ist diejenige, die der Entwickler in seinem Programm notiert.

Die daraus resultierende physische und ebenso die Peripherie-Adresse ergeben sich erst anhand der Verschaltung der Hardware. Dabei sind CPU, Hauptspeicher und Peripherie oft über unterschiedliche Systembusse und über zweierlei MMUs (Memory Management Units) miteinander verbunden.

Die Speicher-MMU setzt die logische Adresse auf die physische Hauptspeicheradresse um, die IO-MMU setzt die Adresse auf die Busadresse um. Da der DMA-Controller des Raspberry Pi am IO-Bus angebunden ist, benötigt er alle Adressenangaben, also die der Quelle, die des Ziels und die des Kontrollblocks selbst beziehungsweise des nächsten Kontrollblocks als Peripherie-Adresse.

Um also dieselbe Speicherzelle zu adressieren, verwendet der DMA-Controller eine andere Adresse als das eigentliche Programm. Glücklicherweise gibt es mit »dma_alloc_coherent()« eine Funktion zur Speicherreservierung, die sowohl die logische als auch zugleich die Peripherie-Adresse zurückgibt. Bei der DMA-Programmierung sollte der Entwickler auch tatsächlich diese Funktionen verwenden, um damit zugleich die nächste Klippe zu umschiffen.

Der DMA-Controller transferiert die Daten nämlich unabhängig vom Hauptprozessor, die CPU bekommt tatsächlich vom Transfer nichts mit. Das ist wegen der CPU-internen Caches problematisch: Die CPU arbeitet aus Performance-Gründen nach Möglichkeit mit einer im Cache liegenden Kopie der eigentlichen Daten. Wenn jetzt ein DMA Daten im Hauptspeicher modifiziert, die zugleich im Prozessorcache liegen, hantiert die CPU mit veralteten Daten. Der Fachmann sagt, es liegt keine Cache-Kohärenz vor. Um dies zu verhindern, muss jemand die MMU für die beteiligten Speicherbereiche auf den DMA-Transfer vorbereiten. Das stellt die Speicherreservierungsfunktion »dma_alloc_coherent()« sicher.

Abbildung 4: Hardware-bedingt gibt es logische Hauptspeicheradressen sowie physische und Peripherie-Adressen.

Abbildung 4: Hardware-bedingt gibt es logische Hauptspeicheradressen sowie physische und Peripherie-Adressen.

Ein praktisches Beispiel …

Ist ein DMA-Kanal reserviert und mit einer Liste von Kontrollblöcken konfiguriert, kann es losgehen. Der Kernel des Raspberry Pi stellt hierfür die Funktion »bcm_dma_start()« bereit. Über das Ende des DMA-Transfers informiert entweder ein Interrupt oder die Funktion »bcm_dma_wait_idle()« lässt den Code bis zum Abschluss des DMA-Transfers in einer Busyloop aktiv warten.

Listing 1 zeigt ein Kernelmodul, das eine Speicherseite (4096 Byte) per DMA kopiert. Den Quell- und den Zielspeicher fordert »dma_alloc_coherent()« beim Betriebssystem an, ebenso den Speicher für den DMA-Kontrollblock. Die zu jedem Bereich gehörenden zwei Adressen nehmen zwei Variablen auf.

Die Variablen »source_mem« , »dest_mem« und »cb_mem« speichern die logische Adresse, über die das Programm selbst auf den Speicher zugreifen kann. Die Variablen »source_io« , »dest_io« und »cb_io« enthalten die Peripherie-Adresse, die der DMA-Controller verwendet. Erkennbar ist diese Zuordnung auch am Datentyp der Variablen: Adressen für DMA sind vom Typ »dma_addr_t« , der Programmierer kann solche Adressen nicht dereferenzieren.

Um zu sehen, dass das Beispielprogramm tatsächlich Daten transportiert, kopiert Zeile 44 der String »Hello World« in den Quellbuffer. Im Zielbuffer legt die Zeile 45 den String »not yet copied« ab, den der dann ablaufende DMA mit “Hello World” überschreiben wird.

Listing 1

Kernelmodul dmatest.c mit DMA-Nutzung

01 #include <linux/module.h>
02 #include <linux/dma-mapping.h>
03 #include "dma.h"
04
05 #define BUFLEN (PAGE_SIZE)
06
07 static void *source_log, *dest_log, *cb_log;
08 static dma_addr_t source_io, dest_io, cb_io;
09 static struct bcm2708_dma_cb *cb;
10 static int channel;
11 static dma_addr_t dma_reg;
12
13 static int __init mod_init( void )
14 {
15     unsigned int irqs;
16
17     /************************/
18     /* Speicher reservieren */
19     /************************/
20     source_log=dma_alloc_coherent(NULL,BUFLEN,&source_io,
21         GFP_KERNEL);
22     if (!source_log) {
23         printk(KERN_ERR "dma_alloc_coherent error source\n");
24         return -EIO;
25     }
26     dest_log=dma_alloc_coherent(NULL,BUFLEN,&dest_io,GFP_KERNEL);
27     if (!dest_log) {
28         printk(KERN_ERR "dma_alloc_coherent error dest\n");
29         goto free_source;
30     }
31     cb_log=dma_alloc_coherent(NULL,sizeof(struct bcm2708_dma_cb),
32         &cb_io, GFP_KERNEL);
33     if (!cb_log) {
34         printk(KERN_ERR "dma_alloc_coherent error cb\n");
35         goto free_dest;
36     }
37     printk("SOURCE: log-addr: %p bus-addr: %p phys_addr: %p\n",
38         source_log, (char *)source_io,
39         (char *)virt_to_phys(source_log));
40     printk("DEST:   log-addr: %p bus-addr: %p phys_addr: %p\n",
41         dest_log,(char *)dest_io,(char *)virt_to_phys(dest_log));
42     printk("CB:     log-addr: %p bus-addr: %p phys_addr: %p\n",
43         cb_log, (char *)cb_io, (char *)virt_to_phys(cb_log));
44     memcpy( source_log, "Hello World", strlen("Hello World")+1);
45     memcpy( dest_log,"not yet copied",strlen("not yet copied")+1);
46
47     /***********************/
48     /* DMA-Kanal aufsetzen */
49     /***********************/
50     channel = bcm_dma_chan_alloc( BCM_DMA_FEATURE_NORMAL,
51         (void *)&dma_reg, &irqs );
52     printk("channel: %d dma_reg: %p irqs: %x\n",channel,
53         (void*)dma_reg,irqs);
54
55     cb = (struct bcm2708_dma_cb *) cb_log;
56
57     cb->info= BCM2708_DMA_S_INC | BCM2708_DMA_D_INC;
58     cb->src = (unsigned long) source_io;
59     cb->dst = (unsigned long) dest_io;
60     cb->length = BUFLEN;
61     cb->stride = 0;
62     cb->next = 0;
63
64     /****************/
65     /* DMA-Transfer */
66     /****************/
67     printk("ZIEL VORHER  %p: %s\n", dest_log, (char *)dest_log);
68     bcm_dma_start( (void*)dma_reg, cb_io );
69     bcm_dma_wait_idle( (void*)dma_reg );
70     printk("ZIEL NACHHER %p: %s\n", dest_log, (char *)dest_log);
71     return 0;
72 free_dest:
73     dma_free_coherent(NULL, BUFLEN, dest_log, dest_io );
74 free_source:
75     dma_free_coherent(NULL, BUFLEN, source_log, source_io);
76     if (channel) bcm_dma_chan_free(channel);
77     return -EIO;
78 }
79
80 static void __exit mod_exit( void )
81 {
82     printk("freeing dma-ressources\n");
83     dma_free_coherent(NULL, BUFLEN, dest_log, dest_io );
84     dma_free_coherent(NULL, BUFLEN, source_log, source_io);
85     dma_free_coherent(NULL, sizeof(struct bcm2708_dma_cb),
86         cb_log, cb_io);
87     if (channel) bcm_dma_chan_free(channel);
88     return;
89 }
90
91 module_init( mod_init );
92 module_exit( mod_exit );
93 MODULE_LICENSE("GPL");

… das auf einem Vanilla-Kernel nicht läuft

Für das Kompilieren des Codes aus Listing 1 sind ein Makefile, die Headerdatei »dma.h« und ein zum laufenden Modul-Code passender und passend konfigurierter Kernel nötig. Ein Vanilla-Kernel läuft bis heute nicht problemlos auf dem Raspi, daher sind die Kernelquellen der Raspberry Pi Foundation unumgänglich. Himbeer-Kernel gibt es in vielen verschiedenen Versionen, die Autoren setzen zurzeit den in Version 4.1.0 ein, auf dem das Modul kompiliert und läuft.

Mit der Anleitung aus [3] generiert und installiert der Pi-Besitzer seinen Kernel und hat danach den benötigten, passend konfigurierten Quellcode. Die Headerdatei »dma.h« stammt aus eben diesem und ist in »arch/arm/mach-bcm2708/include/mach/dma.h« zu finden.

Die Ausgabe erscheint im Syslog und ist in Abbildung 5 zu sehen. Sie führt die unterschiedlichen Adressen der reservierten Speicherbereiche auf. Während die CPU auf den Speicher über 0xbac18000 zugreift, verwendet der DMA-Controller die Adresse 0xfac18000, und physisch liegt der Speicher bei 0x3ac18000.

Außerdem wird das Kopieren per DMA anhand des Überschreibens des Strings gezeigt. Beim Entladen des Moduls werden die Speicherbereiche per »dma_free_coherent()« wieder freigegeben, ebenso wie der DMA-Kanal selbst.

Abbildung 5: Beim Laden des Kernelmoduls überschreiben die per DMA kopierten Daten den String »not yet copied«.

Abbildung 5: Beim Laden des Kernelmoduls überschreiben die per DMA kopierten Daten den String »not yet copied«.

Während des DMA ist die CPU taub und stumm

Während der Zeit, in der der DMA-Controller den Systembus übernommen hat, vermag die CPU weder auf Hauptspeicher noch auf Peripherie zuzugreifen. Es gibt den DMA in zwei Erscheinungsformen: kurz und schmerzvoll oder länger und nebenbei. Arbeitet der DMA-Controller im Burst-Modus, legt er die CPU für die Zeit des Transfers lahm. Auf dem Raspberry Pi arbeitet der DMA-Controller aber – falls nicht anders konfiguriert – so: Er stibitzt sich ab und zu einen Buszyklus, darum heißt diese transparente Betriebsart Cycle Stealing. Die CPU bemerkt fast nichts vom DMA, da sie während der gestohlenen Zyklen ausreichend Daten in den prozessoreigenen Caches hat.

Beim Raspberry Pi kann der Programmierer die Intensität des Zyklen-Stehlens konfigurieren. Mehr noch, er darf die einzelnen DMA-Übertragungen zeitlich genau takten oder mit Peripheriesignalen steuern. Informationen hierzu liefern das Datenblatt zum Raspberry Pi Controller [4] und die Headerdatei »dma.h« .

Drehzahl am Puls der Zeit

Mit Hilfe der Taktung realisieren dann auch pfiffige Entwickler auf dem Raspberry Pi eine Pulsweitenmodulation. PWM eignet sich beispielsweise prima, um einen Gleichstrommotor mit einer bestimmten Drehgeschwindigkeit laufen zu lassen. Anstatt die Motoren ständig mit Strom zu versorgen und so mit maximaler Umdrehungszahl zu fahren, bekommen die Motoren den Strom zeitlich getaktet. PWM-Signale zum Ansteuern von Peripherie brauchen Hardware-Entwickler alle Nase lang, sodass die beiden PWM-Einheiten am Raspberry Pi schnell ausgelastet sind.

Wer PWM-Signale über DMA erzeugen will, lässt den DMA-Controller eine Folge von Nullen und Einsen aus dem Speicher auf ein GPIO transferieren. Die Anzahl der Nullen im Verhältnis zu den Einsen bestimmt dann die Umdrehungszahl. Zur Taktung verwenden die Entwickler eine der beiden vorhandenen Hardware-PWMs. Mit dieser Technik hängt die Anzahl der verfügbaren unabhängigen PWM-Signale nur noch von der Anzahl der freien DMA-Kanäle ab.

Zwar findet sich im Internet hierfür Quellcode, der ist aber für eine Userland-Applikation gedacht. Für ein Bastlerobjekt ist das akzeptabel, im professionellen Einsatz aber nicht, weil die Applikation nicht verhindern kann, dass ein anderes Programm oder der Kernel denselben DMA-Kanal benutzen wie sie.

Effiziente Blockabfertigung

Unterm Strich gibt es jetzt nur noch wenige Gründe, beim Kopieren auf DMA zu verzichten. Der Geschwindigkeitsvorteil ist immens und DMA hält den Code im Hauptprogramm übersichtlich. Und der Raspberry Pi ist eine ausgezeichnete Plattform, um DMA zu testen.

Infos

  1. Katz, Gentile, “Using Direct Memory Access effectively in media-based embedded applications – Part 1”: http://www.embedded.com/design/mcus-processors-and-socs/4006782/Using-Direct-Memory-Access-effectively-in-media-based-embedded-applications–Part-1
  2. Miller, Henderson, Jelinek, “Dynamic DMA mapping guide”: https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt
  3. Kernel Building: https://www.raspberrypi.org/documentation/linux/kernel/building.md
  4. Broadcom, “BCM2835 ARM Peripherals”:https://www.raspberrypi.org/wp-content/uploads/2012/02/BCM2835-ARM-Peripherals.pdf

Der Autor

Eva-Katharina Kunst ist seit den Anfängen von Linux Fan von Open Source. Jürgen Quade, Professor an der Hochschule Niederrhein, hat mit “Embedded Linux lernen mit dem Raspberry Pi” letztes Jahr sein drittes Linux-Buch veröffentlicht. Das gemeinsame Buch “Linux-Treiber entwickeln” ist jüngst in vierter Auflage erschienen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben