Was ist DMA2D?
Mit der Weiterentwicklung der eingebetteten Grafik übernehmen Mikrocontroller immer komplexere Grafikberechnungen und Anzeigeaufgaben. Die Rechenleistung der CPU reicht jedoch möglicherweise nicht aus, um hochauflösende Grafiken mit lebendigen Farben zu verarbeiten. Glücklicherweise hat ST ab dem STM32F429 eine externe Peripherieeinheit ähnlich einer GPU in die STM32-Mikrocontroller integriert, die als Chrom-ART Accelerator oder DMA2D bekannt ist. DMA2D bietet Beschleunigung in vielen 2D-Grafik-Szenarien und integriert effektiv Funktionen, die denen einer „GPU” in modernen Grafikkarten ähneln.
Obwohl DMA2D nur 2D-Beschleunigung bietet und seine Fähigkeiten im Vergleich zu GPUs in PCs relativ einfach sind, kann es die meisten Anforderungen an die Grafikbeschleunigung in der Embedded-Entwicklung erfüllen. Durch die effektive Nutzung von DMA2D können wir flüssige und beeindruckende UI-Effekte auf Mikrocontrollern erzielen.
DMA2D-Funktionen
- Farbfüllung (rechteckige Bereiche)
- Kopieren von Bildern (Speicher)
- Farbformatkonvertierung (z. B. YCbCr zu RGB oder RGB888 zu RGB565)
- Transparenzüberblendung (Alpha-Blend)
Die ersten beiden sind speicherbasierte Operationen, während die beiden letzteren eine Beschleunigung der Rechenleistung beinhalten. Transparenzüberblendung und Farbformatkonvertierung können mit dem Kopieren von Bildern kombiniert werden, was eine erhebliche Flexibilität bietet.
In der praktischen Entwicklung ähnelt die Verwendung von DMA2D der Verwendung herkömmlicher DMA-Controller. In bestimmten nicht-grafischen Szenarien kann DMA2D sogar herkömmliche DMA für bestimmte Aufgaben ersetzen.
Es ist wichtig zu beachten, dass DMA2D-Beschleuniger in verschiedenen ST-Produktlinien geringfügige Unterschiede aufweisen können. Beispielsweise fehlt dem DMA2D in der MCU der STM32F4-Serie die Fähigkeit, zwischen den Farbformaten ARGB und AGBR zu konvertieren. Wenn Sie eine bestimmte Funktionalität benötigen, sollten Sie daher im Programmierhandbuch nachsehen, ob diese unterstützt wird.
DMA2D-Betriebsmodi
Ähnlich wie beim herkömmlichen DMA, das über die Modi „Peripherie zu Peripherie“, „Peripherie zu Speicher“ und „Speicher zu Peripherie“ verfügt, gibt es auch bei DMA2D als DMA-Komponente vier Betriebsmodi:
- Register zu Speicher
- Speicher zu Speicher
- Speicher zu Speicher mit Pixel-Farbformatkonvertierung
- Speicher zu Speicher mit Pixel-Farbformatkonvertierung und Transparenzüberblendung
Die ersten beiden Modi umfassen einfache Speicheroperationen, während die beiden letzteren Modi das Kopieren von Speicherinhalten durchführen und gleichzeitig je nach Bedarf die Konvertierung des Farbformats und/oder die Transparenzüberblendung übernehmen.
DMA2D und HAL-Bibliothek
In vielen Fällen vereinfacht die Verwendung der HAL-Bibliothek das Schreiben von Code und verbessert die Portabilität. Bei der Verwendung von DMA2D gibt es jedoch eine Ausnahme. Das Hauptproblem bei HAL liegt in den übermäßigen Verschachtelungen und Sicherheitsprüfungen, die die Effizienz verringern. Während der Effizienzverlust bei der Verwendung anderer Peripheriegeräte möglicherweise nicht erheblich ist, kann die Verwendung der HAL-Bibliothek bei DMA2D – einem Beschleuniger, der auf Berechnung und Geschwindigkeit ausgerichtet ist – die Beschleunigungseffizienz erheblich verringern.
Daher vermeiden wir häufig die Verwendung relevanter HAL-Funktionen für DMA2D-Operationen. Aus Gründen der Effizienz wird eine direkte Registermanipulation eingesetzt, um maximale Beschleunigungsvorteile zu gewährleisten.
Da die meisten DMA2D-Anwendungsfälle häufige Änderungen der Betriebsmodi beinhalten, verliert die grafische Konfiguration von DMA2D in CubeMX ihre Praktikabilität.
Anwendung von DMA2D in der Entwicklung eingebetteter Grafiksysteme
Benötigte Werkzeuge
- STM32-Entwicklungsboard mit DMA2D-Peripherie x1
- Farb-TFT-Bildschirm x1
In diesem Beispiel verwenden wir das ART-Pi-Entwicklungsboard von RT-Thread mit einem STM32H750XB mit einer Taktfrequenz von bis zu 480 MHz und 32 MB SDRAM. Es enthält auch einen Debugger (ST-Link V2.1). Außerdem verwenden wir einen 3,5-Zoll-TFT-LCD-Bildschirm mit einer RGB666-Schnittstelle und einer Auflösung von 320 x 240 (QVGA).

Entwicklungsumgebung
Der in diesem Artikel vorgestellte Inhalt und Code kann in verschiedenen Entwicklungsumgebungen wie RT-Thread Studio, MDK, IAR usw. verwendet werden.
Bevor Sie mit den Experimenten in diesem Artikel beginnen, benötigen Sie ein Basisprojekt, das das LCD-Display mithilfe der Framebuffer-Technologie ansteuert. Vor der Ausführung des bereitgestellten Codes muss DMA2D aktiviert werden.
DMA2D kann mit diesem Makro aktiviert werden:
__HAL_RCC_DMA2D_CLK_ENABLE();
Anwendungsprojekt: Rechteckfüllung
Eingebettete Grafiken umfassen verschiedene Arten von Operationen, darunter das Füllen von Rechtecken, das Kopieren von Speicherinhalten, das Mischen von Transparenzen usw. Wir verwenden das Füllen von Rechtecken als Beispiel. Das Ziel besteht darin, mit DMA2D zum Füllen von Rechtecken ein einfaches Balkendiagramm zu erstellen:

Zunächst müssen wir den Bildschirm mit einer weißen Farbe füllen, die als Hintergrund für das Muster dient. Dieser Schritt ist entscheidend, da das vorhandene Muster auf dem Bildschirm unser beabsichtigtes Design beeinträchtigen könnte. Anschließend wird das Balkendiagramm aus vier blauen rechteckigen Blöcken und einem Liniensegment erstellt, das als spezieller rechteckiger Block mit einer Höhe von 1 betrachtet werden kann. Das Zeichnen dieser Grafik umfasst daher eine Reihe von „Rechteckfüllungs“-Operationen:
- Füllen Sie ein Rechteck mit weißer Farbe, das den gesamten Bildschirm bedeckt.
- Vier Datenbalken mit blauer Farbe füllen.
- Füllen Sie ein Liniensegment mit schwarzer Farbe und einer Höhe von 1.
Im Wesentlichen umfasst das Zeichnen eines Rechtecks beliebiger Größe an einer beliebigen Position auf der Leinwand das Setzen der Pixeldaten an der entsprechenden Speicherstelle auf die gewünschte Farbe. Aufgrund der linearen Speicherung des Framebuffers im Speicher haben die scheinbar zusammenhängenden rechteckigen Bereiche jedoch nicht zusammenhängende Speicheradressen, es sei denn, die Breite des Rechtecks stimmt genau mit der Breite des Bildschirms überein.
Das folgende Diagramm zeigt ein typisches Speicherlayout, wobei die Zahlen die Speicheradresse jedes Pixels im Framebuffer angeben (Offset relativ zur Basisadresse, ohne Berücksichtigung von Multibyte-Pixeln). Der blaue Bereich stellt das zu füllende Rechteck dar. Es ist offensichtlich, dass die Speicheradressen innerhalb des Rechtecks nicht zusammenhängend sind.

Diese Eigenschaft des Framebuffers verhindert, dass wir effiziente Operationen wie memset zum Füllen rechteckiger Bereiche verwenden können. Normalerweise würden wir einen verschachtelten Schleifenansatz wie den unten stehenden verwenden, um ein beliebiges Rechteck zu füllen. Hier sind xs und ys die Koordinaten der oberen linken Ecke des Rechtecks auf dem Bildschirm, width und height definieren die Abmessungen des Rechtecks und color gibt die Füllfarbe an:
for (int y = ys; y < ys + height; y++) {
for (int x = xs; x < xs + width; x++) {
framebuffer[y][x] = color;
}
}
Der Code mag zwar einfach erscheinen, doch während der Ausführung wird ein erheblicher Teil der CPU-Zyklen für Operationen wie Bedingungsprüfungen, Adressberechnungen und Inkrementierungen verschwendet, während nur ein minimaler Teil für das eigentliche Schreiben in den Speicher verwendet wird. Diese Situation führt zu einer verminderten Effizienz.
In solchen Fällen kommt der Register-zu-Speicher-Arbeitsmodus des DMA2D zum Einsatz. DMA2D kann einen rechteckigen Speicherbereich schnell füllen, selbst wenn dieser Bereich im Speicher nicht zusammenhängend ist.
Anhand des in der Abbildung oben dargestellten Beispiels wollen wir uns nun genauer ansehen, wie dies erreicht wird:

Da es sich ausschließlich um das Füllen von Speicher und nicht um das Kopieren handelt, muss DMA2D im Register-zu-Speicher-Modus betrieben werden. Dies wird erreicht, indem die Bits [17:16] des CR-Registers von DMA2D auf „11” gesetzt werden, wie im folgenden Codeausschnitt gezeigt:
DMA2D->CR = 0x00030000UL;
Als Nächstes teilen wir DMA2D die Eigenschaften des zu füllenden Rechtecks mit, wie z. B. die Startadresse des Bereichs, seine Breite in Pixeln und seine Höhe.
Die Startadresse des Bereichs ist die Speicheradresse des Pixels oben links im Rechteck (Adresse des roten Pixels in der Abbildung), die vom OMAR-Register von DMA2D verwaltet wird. Die Breite und Höhe des Rechtecks werden beide in Pixeln angegeben und vom oberen 16-Bit-Bereich (Breite) und unteren 16-Bit-Bereich (Höhe) des NLR-Registers verwaltet. Der Code zum Festlegen dieser Werte lautet wie folgt:
DMA2D->OMAR = (uint32_t)(&framebuffer[y][x]); // Set the starting pixel memory address for filling
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // Set the width and height of the rectangle
Da die Speicheradressen des Rechtecks nicht zusammenhängend sind, müssen wir DMA2D anweisen, nach dem Füllen einer Datenzeile (d. h. der Länge des gelben Bereichs in der Abbildung) eine bestimmte Anzahl von Pixeln zu überspringen. Dieser Wert wird vom OOR-Register verwaltet. Die Berechnung der Anzahl der zu überspringenden Pixel erfolgt auf einfache Weise: Subtrahieren Sie die Breite des Rechtecks von der Breite des Anzeigebereichs. Der Code zur Umsetzung lautet:
DMA2D->OOR = screenWidthPx - width; // Set the row offset, i.e., skip pixels
Zuletzt müssen wir DMA2D die Farbe für die Füllung und das Farbformat mitteilen. Diese werden jeweils über die Register OCOLR und OPFCCR verwaltet. Das Farbformat wird durch die Makros LTDC_PIXEL_FORMAT_XXX definiert. Der Code lautet wie folgt:
DMA2D->OCOLR = color; // Set the color for filling
DMA2D->OPFCCR = pixelFormat; // Set the color format, e.g., use the macro LTDC_PIXEL_FORMAT_RGB565 for RGB565
Nachdem alle Einstellungen vorgenommen wurden, hat DMA2D alle erforderlichen Informationen zum Füllen des Rechtecks erfasst. Um die Übertragung zu starten, setzen wir Bit 0 des CR-Registers von DMA2D auf „1“:
DMA2D->CR |= DMA2D_CR_START; // Start DMA2D data transfer, where DMA2D_CR_START is a macro with the value 0x01
Sobald die DMA2D-Übertragung beginnt, warten wir einfach auf deren Abschluss. Nachdem DMA2D die Übertragung beendet hat, setzt es automatisch Bit 0 des CR-Registers auf „0” zurück, sodass wir mit dem folgenden Code auf den Abschluss warten können:
while (DMA2D->CR & DMA2D_CR_START) {} // Wait for DMA2D transfer completion
Tipp: Wenn Sie ein Betriebssystem verwenden, können Sie den DMA2D-Übertragungsabschluss-Interrupt aktivieren. Anschließend können Sie einen Semaphor erstellen, nach dem Start der Übertragung darauf warten und ihn in der DMA2D-Übertragungsabschluss-Interrupt-Serviceroutine freigeben.
Aus Gründen der Funktionsgeneralität werden die Startadresse der Übertragung und der Zeilenversatz außerhalb der Funktion berechnet und übergeben. Hier ist der vollständige Funktionscode:
static inline void DMA2D_Fill(void * pDst, uint32_t width, uint32_t height, uint32_t lineOff, uint32_t pixelFormat, uint32_t color) {
/* Configure DMA2D */
DMA2D->CR = 0x00030000UL; // Configure for register-to-memory mode
DMA2D->OCOLR = color; // Set the color for filling (format should match the configured color format)
DMA2D->OMAR = (uint32_t)pDst; // Starting memory address of the fill region
DMA2D->OOR = lineOff; // Row offset, i.e., skip pixels (in pixel units)
DMA2D->OPFCCR = pixelFormat; // Set the color format
DMA2D->NLR = (uint32_t)(width << 16) | (uint16_t)height; // Set the width and height of the fill region (in pixel units)
/* Start transfer */
DMA2D->CR |= DMA2D_CR_START;
/* Wait for DMA2D transfer completion */
while (DMA2D->CR & DMA2D_CR_START) {}
}
Der Einfachheit halber fassen wir dies in einer Rechteck-Füllfunktion zusammen, die auf dem Koordinatensystem Ihres Bildschirms basiert:
void FillRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
void* pDist = &(((uint16_t*)framebuffer)[y*320 + x]);
DMA2D_Fill(pDist, w, h, 320 - w, LTDC_PIXEL_FORMAT_RGB565, color);
}
Verwenden wir nun den Code, um das zu Beginn dieses Abschnitts vorgestellte Diagramm zu zeichnen:
// Fill background color
FillRect(0, 0, 320, 240, 0xFFFF);
// Draw data bars
FillRect(80, 80, 20, 120, 0x001F);
FillRect(120, 100, 20, 100, 0x001F);
FillRect(160, 40, 20, 160, 0x001F);
FillRect(200, 60, 20, 140, 0x001F);
// Draw X-axis
FillRect(40, 200, 240, 1, 0x0000);
Die Wirkung der Code-Operation ist wie folgt:





