Grafik in C (PI4): Unterschied zwischen den Versionen
KKeine Bearbeitungszusammenfassung |
KKeine Bearbeitungszusammenfassung |
||
| (4 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt) | |||
| Zeile 1: | Zeile 1: | ||
Die bisherigen Programme auf dem Raspberry Pi waren vergleichsweise einfach und ermöglichten lediglich die Steuerung einer LED. Der Raspberry Pi kann jedoch weitaus mehr, insbesondere im Bereich der Grafik, da er über einen HDMI-Ausgang verfügt, der für fortgeschrittene Anwendungen genutzt werden kann. In diesem Abschnitt wollen wir den HDMI-Ausgang des Raspberry Pi verwenden, um grafische Ergebnisse direkt anzuzeigen. | |||
Zu Beginn schließen wir einen Monitor an den Raspberry Pi an, um die Ergebnisse direkt auf dem Bildschirm sehen zu können. Da wir Bare Metal programmieren, müssen wir sämtliche Routinen selbst schreiben, da der Raspberry Pi keine vorinstallierten Weisungen hat, wie er Bilder auf dem Monitor anzeigt. | |||
Der Raspberry Pi | Der Raspberry Pi 4 besitzt zwei HDMI-Ausgänge, die jeweils Bildschirme mit bis zu UHD-Auflösung (4096 x 2160 Pixel) bei 60 Hz unterstützen. Bei Verwendung beider HDMI-Ports beträgt die Bildwiederholrate maximal 30 Hz. Ältere Raspberry Pi-Modelle unterstützen Bildschirme bis zu einer Auflösung von 1920 x 1200 Pixel bei 60 Hz. | ||
Interessanterweise ist es die GPU des Raspberry Pi, die den Startvorgang initiiert. Sie lädt die Firmware in den Speicher und übergibt danach die Kontrolle an die CPU. Dies ist während des Startvorgangs deutlich sichtbar, wenn ein Bildschirm angeschlossen ist. | |||
Um mit der Grafikprogrammierung zu beginnen, müssen wir zunächst verstehen, wie ein Bild auf dem Bildschirm dargestellt wird. Ein Bildschirm besteht aus einzelnen Punkten, den Pixeln, die jeweils eine eigene Farbe haben. Normalerweise wird der Bildschirm in einem Koordinatensystem (x, y) dargestellt. Der erste Pixel oben links hat die Koordinaten (0,0). Der nachfolgende Pixel rechts davon hat die Koordinate (1,0) und so weiter. Nach unten wird die y-Koordinate jeweils um eins erhöht: (0,1), (0,2) usw. | |||
[[Datei:Koordinaten1.png|rand]] | [[Datei:Koordinaten1.png|rand]] | ||
| Zeile 13: | Zeile 13: | ||
== Auflösungen == | == Auflösungen == | ||
Der Raspberry Pi | Der Raspberry Pi unterstützt viele verschiedene Auflösungen. Eine Übersicht über die gängigsten Auflösungen findest du beispielsweise unter https://de.wikipedia.org/wiki/Bildaufl%C3%B6sung#Computer. Hier ein paar Auflösungen, die verwendet werden könnten: | ||
{| class="wikitable" | {| class="wikitable" | ||
| Zeile 26: | Zeile 26: | ||
| HD || 1920 x 1080 | | HD || 1920 x 1080 | ||
|- | |- | ||
| UHD (only Pi 4) || 4096 x 2160 | | UHD (only Pi 4 or Pi 5) || 4096 x 2160 | ||
|} | |} | ||
Die Auflösung | Die Auflösung allein reicht jedoch nicht aus, um komplexe, farbige Bilder darzustellen. Da Computersysteme nur binäre Zahlen kennen, also nur "1" oder "0", könnte man jedes Pixel lediglich ein- oder ausschalten, was nur ein Schwarz-Weiß-Bild erzeugen würde. Um die Vielzahl der Farben, die wir auf Bildschirmen sehen, darzustellen, werden zusätzliche Standards definiert. | ||
Wird die Anzeige mehrmals hintereinander vertieft, entsteht eine größere Farbtiefe. Wenn ein Bild beispielsweise zwei Bitmaps verwendet, stehen für jeden Punkt zwei Bits zur Verfügung. Das ergibt vier Zustände bzw. vier Farben. Je mehr Bitmaps verwendet werden, desto mehr Farben können angezeigt werden. | |||
== Farbtiefe == | == Farbtiefe == | ||
Der Raspberry Pi unterstützt vier verschiedene Farbtiefen: 8-Bit, 16-Bit, 24-Bit und 32-Bit. In der Praxis wird jedoch von der Verwendung der 8-Bit und 24-Bit Farbtiefe abgeraten, da sie zu Darstellungsfehlern führen können. Siehe hierzu auch die Dokumentation unter https://www.raspberrypi.org/documentation/configuration/config-txt/video.md im Abschnitt framebuffer_depth. Um es einfach zu halten, konzentrieren wir uns auf die 32-Bit Farbtiefe. | |||
Beispiel: Speicheraufwand für Bilder | |||
Um den Speicheraufwand zu veranschaulichen, betrachten wir ein Beispiel. Ein monochromes Bild mit einer Auflösung von 20 x 20 Pixel benötigt folgenden Speicherbedarf: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 45: | Zeile 46: | ||
[[Datei:Koordinaten2.png|rand|400x400px]] | [[Datei:Koordinaten2.png|rand|400x400px]] | ||
Für ein Bild mit 4 Bitmaps beträgt der Speicherbedarf: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 53: | Zeile 54: | ||
[[Datei:Koordinaten3.png|rand|500x500px]] | [[Datei:Koordinaten3.png|rand|500x500px]] | ||
Nun ein Beispiel mit einer HD-Auflösung und RGBA32-Farbtiefe: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
((1920 * 1080) * 32) / 8 = 8294400 Bytes | Speicherbedarf (Bytes) = ((1920 * 1080) * 32) / 8 = 8294400 Bytes | ||
Das entspricht etwa 8,3 Megabytes. | |||
</syntaxhighlight> | </syntaxhighlight> | ||
== Abbildung der Bitmaps im Speicher == | == Abbildung der Bitmaps im Speicher == | ||
Bitmaps werden nicht als eine Reihe von Einzelbildern (Bitmaps) im Speicher abgelegt, sondern die Grafikdaten sind wie in einem dreidimensionalen Raum organisiert. Betrachten wir ein Beispiel mit einer Auflösung von 20 x 20 Pixel und einer Farbtiefe von 16 Bit: | |||
Jeder Bildschirmpunkt wird bei einer Farbtiefe von 16 Bit durch 16 aufeinanderfolgende Bits dargestellt. Der erste Punkt verwendet die Bits 0-15, der nächste die Bits 16-31, und so weiter. Das müssen wir bei den Berechnungen für jeden einzelnen Punkt berücksichtigen. | |||
[[Datei:Koordinaten4.png|rand|600x600px]] | [[Datei:Koordinaten4.png|rand|600x600px]] | ||
== Der Postbote und TAGs == | == Der Postbote und TAGs == | ||
Wie bereits beschrieben, wird die Grafik nicht von der CPU, sondern von der GPU (Graphics Processing Unit) dargestellt. Damit die GPU weiß, was sie tun soll, benötigt sie bestimmte Informationen zur Darstellung, etwa wie der Bildschirm aussehen soll. Eine direkte Verbindung zwischen CPU und GPU existiert jedoch nicht, weshalb die Kommunikation über einen Vermittler erfolgt, der hier als "Postbote" bezeichnet wird. Die CPU sendet eine Nachricht an den Postboten, und dieser leitet die Nachricht an die GPU weiter. Ebenso kann die GPU Nachrichten an die CPU schicken. | |||
Der Postbote hat die Basisadresse 0xB880 und verwendet "TAGs" für die Informationsübermittlung. Die Dokumentation zu diesen TAGs ist leider spärlich, doch es gibt eine Gemeinschaft, die Informationen gesammelt und bereitgestellt hat. Weitere Informationen findest du hier: | |||
Der Postbote hat | |||
https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface | https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface | ||
Diese Informationen habe ich in | Diese Informationen habe ich in dem zugehörigen Sourcecode zusammengefasst und in der base.h Datei eingefügt. | ||
Zunächst | Zunächst definieren wir in der base.h die neuen Register, die dafür verantwortlich sind: | ||
<syntaxhighlight lang=" | <syntaxhighlight lang="C"> | ||
// | |||
// Mailbox | |||
// | |||
#define MAILBOX_BASE RPI_BASE + 0xB880 | |||
#define MAILBOX0_READ MAILBOX_BASE + 0x00 | |||
#define MAILBOX0_STATUS MAILBOX_BASE + 0x18 | |||
#define MAILBOX_STATUS_EMPTY 0x40000000 | |||
#define MAILBOX1_WRITE MAILBOX_BASE + 0x20 | |||
#define MAILBOX1_STATUS MAILBOX_BASE + 0x38 | |||
#define MAILBOX_STATUS_FULL 0x80000000 | |||
#define MAIL_WRITE MAILBOX_BASE + 0x20 | |||
#define MAIL_TAG_WRITE MAIL_WRITE + 0x8 | |||
#define MAILBOX_CHANNEL_PM 0 // power management | |||
#define MAILBOX_CHANNEL_FB 1 // frame buffer | |||
#define BCM_MAILBOX_PROP_OUT 8 // property tags (ARM to VC) | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Für die Kommunikation verwenden wir "TAGs". Diese TAGs sind in einer bestimmten Struktur organisiert, die von der GPU verstanden wird. | |||
=== Aufbau der TAG-Struktur === | === Aufbau der TAG-Struktur === | ||
TAG-Listen | TAG-Listen haben eine festgelegte Struktur, die die GPU versteht. Eine TAG-Struktur beginnt immer mit einer 32-Bit Zahl, die die Länge der gesamten Struktur angibt. Es folgt ein 32-Bit Puffer, in den eine Antwort geschrieben werden kann. Danach kommen die einzelnen TAGs, die wiederum eine Grundstruktur aufweisen: Der Name des TAGs, die Puffergröße in Bytes, ein Antwortpuffer und die Werte des TAGs. Die Struktur endet mit einem NULL-TAG. | ||
Eine TAG-Struktur | |||
Die | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 112: | Zeile 119: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Jeder TAG muss auf 32-Bit ausgerichtet sein. | |||
== Aufbau unserer Struktur == | |||
Wir erstellen unsere TAG-Struktur und richten sie auf 16 Byte aus, da nur die oberen 28 Bits der Adresse an die Mailbox übergeben werden. | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 122: | Zeile 130: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Die Struktur beginnt mit der Angabe der Länge und einem Puffer für mögliche Antworten: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 130: | Zeile 138: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Jetzt fügen wir die Daten hinzu, beginnend mit der Auflösung des Bildschirms. Der TAG Set_physical_display hat folgende Struktur: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 141: | Zeile 148: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 150: | Zeile 157: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Die Variablen SCREEN_X und SCREEN_Y definieren wir in der config.h Datei, hier für eine Auflösung von 1920 x 1080 Pixel. | |||
Als nächstes definieren wir den Set_virtual_buffer TAG: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 162: | Zeile 169: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 171: | Zeile 178: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Der | Der Set_depth TAG definiert die Farbtiefe: | ||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 179: | Zeile 186: | ||
Tiefe der Bitplanes | Tiefe der Bitplanes | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="asm"> | <syntaxhighlight lang="asm"> | ||
| Zeile 187: | Zeile 196: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Auch hier verwenden wir eine Variable | Auch hier verwenden wir eine Variable BITS_PER_PIXEL, die wir in der config.h Datei auf 32 Bit definieren. | ||
Der | Der Set_virtual_offset TAG erzeugt Offset-Werte: | ||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 199: | Zeile 208: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 207: | Zeile 216: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Der Set_palette TAG definiert die Farbpalette: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 218: | Zeile 227: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 229: | Zeile 238: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Da wir hauptsächlich 16-Bit oder 32-Bit Farbtiefen verwenden, ist der Eintrag Set_palette weniger wichtig, muss aber dennoch definiert werden. | |||
Der Allocate_buffer TAG sorgt dafür, dass die GPU Speicher reserviert und diesen anzeigt: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
| Zeile 238: | Zeile 249: | ||
Bildpuffergröße in Bytes | Bildpuffergröße in Bytes | ||
</syntaxhighlight> | </syntaxhighlight> | ||
In unsere Struktur übertragen sieht es so aus: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 246: | Zeile 259: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Der TAG erzeugt einen Puffer, | Der TAG erzeugt einen Puffer, dessen Adresse in pScreen[31] abgelegt wird, sobald die GPU dies durchgeführt hat. Dies ist die Adresse, auf die wir später zugreifen können. | ||
Unsere TAG-Liste | Unsere TAG-Liste endet mit einem NULL-TAG: | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
| Zeile 254: | Zeile 267: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Diese Struktur beschreibt, was wir darstellen möchten. Die GPU interpretiert diese Werte und versucht, sie darzustellen. Wenn die GPU die Angaben nicht exakt umsetzen kann, erzeugt sie einen ähnlichen Zustand. Wir sollten die von der GPU zurückgemeldeten Werte überprüfen, um sicherzustellen, dass sie unseren Erwartungen entsprechen. | |||
=== Der Postbote === | === Der Postbote === | ||
Nun | Nun bringt der Postbote unsere TAG-Liste zur GPU, um den Bildschirm zu erzeugen. Wir verwenden zunächst eine einfache Methode und erweitern später die Funktionen. | ||
Zuerst geben wir dem Postboten den Auftrag, die Informationen an die GPU zu übermitteln. Da die CPU und die GPU nicht synchron laufen, wissen wir zunächst nicht, ob die Anzeige tatsächlich erstellt wurde. Die GPU benötigt Zeit, um unseren Wunsch umzusetzen. | |||
Um | Um sicherzustellen, dass die Anzeige erfolgreich erzeugt wurde, fragen wir kontinuierlich ab, ob eine Adresse für den Bildschirm existiert. Diese Adresse wird in unserer Struktur unter pScreen[31] abgelegt. Wenn die GPU erfolgreich war, schreibt sie die Adresse in dieses Feld. Solange dies nicht der Fall ist, wiederholen wir die Abfrage. | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
uintptr pScreenAddress = (uintptr)pScreen; | |||
while (TRUE) { | |||
BcmMailBox_Write(BCM_MAILBOX_PROP_OUT, (u32)pScreenAddress); | |||
if (pScreen[31] != 0) { | |||
break; | |||
} | } | ||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Falls die GPU nie eine Adresse zurückgibt, würden wir in einer Endlosschleife festhängen. Später werden wir weitere Funktionen einführen, um solche Probleme zu vermeiden. | |||
Die Adresse, die wir | Die Adresse, die wir zurückbekommen, ist ein Byte-Wert, mit dem wir so nicht direkt arbeiten können. Wir wandeln diesen Wert um und speichern ihn für spätere Zwecke. | ||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
#define ADDRESS_MASK 0x3FFFFFFF | #define ADDRESS_MASK 0x3FFFFFFF | ||
graphicsAddress = pScreen[31] & ADDRESS_MASK; | |||
</syntaxhighlight> | </syntaxhighlight> | ||
== Auf den Bildschirm zeichnen == | == Auf den Bildschirm zeichnen == | ||
Nun haben wir einen Screen | Nun haben wir einen Screen, der allerdings leer ist. Wir benötigen eine Methode, um etwas auf den Bildschirm zu zeichnen. | ||
Wir entwickeln eine Funktion, die einen einzelnen Bildpunkt (Pixel) anzeigt. Diese grundlegende Funktion bildet die Basis für weitere grafische Funktionen. | |||
Wir nennen die Funktion DrawPixel und übergeben ihr die Koordinaten x und y. | |||
Für verschiedene Farbtiefen | Für verschiedene Farbtiefen sind unterschiedliche Berechnungen notwendig. Zunächst halten wir es einfach und gehen von einer Farbtiefe von 32 Bit aus. | ||
Zuerst überprüfen wir, ob der Pixel im sichtbaren Bereich liegt, um unerwünschte Nebeneffekte zu vermeiden. Danach berechnen wir die Position nach folgender Formel: | |||
<syntaxhighlight> | <syntaxhighlight> | ||
(( | ((Gesamtbreite des Screens * Position y) + Position x) * Farbtiefe in Bytes | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Die Screen-Adresse wird zu dieser Position hinzuaddiert: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
void DrawPixel(u32 x, u32 y) | void DrawPixel(u32 x, u32 y) { | ||
{ | if ((x < SCREEN_X) && (y < SCREEN_Y)) { | ||
if ((x < SCREEN_X) && (y < SCREEN_Y)) | write32(DrawColor, graphicsAddress + (((SCREEN_X * y) + x) * 4)); | ||
write32(DrawColor,graphicsAddress+(((SCREEN_X*y)+x)*4)); | |||
} | } | ||
} | } | ||
| Zeile 319: | Zeile 325: | ||
Damit wird bereits der Pixel angezeigt. | Damit wird bereits der Pixel angezeigt. | ||
== Header-Datei für DrawPixel == | |||
Wir erstellen eine Header-Datei, in der wir unsere Funktionen beschreiben: | |||
<syntaxhighlight lang="c"> | <syntaxhighlight lang="c"> | ||
// | // | ||
| Zeile 332: | Zeile 340: | ||
#include "types.h" | #include "types.h" | ||
void Init_Screen(void) ; | void Init_Screen(void); | ||
void DrawPixel(u32 x, u32 y); | void DrawPixel(u32 x, u32 y); | ||
| Zeile 340: | Zeile 348: | ||
== Anwendung der DrawPixel-Funktion == | == Anwendung der DrawPixel-Funktion == | ||
Wir haben | Wir haben nun eine Funktion, die es ermöglicht, etwas auf dem Bildschirm zu sehen. Nun verwenden wir die neuen Funktionen im Hauptprogramm, um ein Ergebnis anzuzeigen. | ||
Zuerst definieren wir die Variablen, die wir beim Erstellen der TAG-Struktur verwendet haben, in der config.h Datei: | |||
<syntaxhighlight lang="C"> | <syntaxhighlight lang="C"> | ||
| Zeile 349: | Zeile 357: | ||
#define BITS_PER_PIXEL 32 | #define BITS_PER_PIXEL 32 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Diese Werte definieren die Breite, Höhe und Tiefe des Bildschirms. | |||
Die erste Funktion, die wir aufrufen, ist die Bildschirminitialisierung: | |||
Die erste Funktion, die wir aufrufen | |||
<syntaxhighlight lang="C"> | <syntaxhighlight lang="C"> | ||
Init_Screen(); | Init_Screen(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Ein einzelner Punkt auf dem Bildschirm ist schwer zu sehen. Daher setzen wir mehrere Punkte nebeneinander und verwenden eine Schleife, die die Koordinaten verändert und jeden Punkt zeichnet: | |||
<syntaxhighlight lang="C"> | <syntaxhighlight lang="C"> | ||
| Zeile 368: | Zeile 374: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Der | Der Quellcode für dieses Beispiel ist [https://www.satyria.de/arm/sources/RPI4/C/4.zip hier] verfügbar. | ||
----- | ----- | ||
Aktuelle Version vom 6. März 2025, 13:18 Uhr
Die bisherigen Programme auf dem Raspberry Pi waren vergleichsweise einfach und ermöglichten lediglich die Steuerung einer LED. Der Raspberry Pi kann jedoch weitaus mehr, insbesondere im Bereich der Grafik, da er über einen HDMI-Ausgang verfügt, der für fortgeschrittene Anwendungen genutzt werden kann. In diesem Abschnitt wollen wir den HDMI-Ausgang des Raspberry Pi verwenden, um grafische Ergebnisse direkt anzuzeigen.
Zu Beginn schließen wir einen Monitor an den Raspberry Pi an, um die Ergebnisse direkt auf dem Bildschirm sehen zu können. Da wir Bare Metal programmieren, müssen wir sämtliche Routinen selbst schreiben, da der Raspberry Pi keine vorinstallierten Weisungen hat, wie er Bilder auf dem Monitor anzeigt.
Der Raspberry Pi 4 besitzt zwei HDMI-Ausgänge, die jeweils Bildschirme mit bis zu UHD-Auflösung (4096 x 2160 Pixel) bei 60 Hz unterstützen. Bei Verwendung beider HDMI-Ports beträgt die Bildwiederholrate maximal 30 Hz. Ältere Raspberry Pi-Modelle unterstützen Bildschirme bis zu einer Auflösung von 1920 x 1200 Pixel bei 60 Hz.
Interessanterweise ist es die GPU des Raspberry Pi, die den Startvorgang initiiert. Sie lädt die Firmware in den Speicher und übergibt danach die Kontrolle an die CPU. Dies ist während des Startvorgangs deutlich sichtbar, wenn ein Bildschirm angeschlossen ist.
Um mit der Grafikprogrammierung zu beginnen, müssen wir zunächst verstehen, wie ein Bild auf dem Bildschirm dargestellt wird. Ein Bildschirm besteht aus einzelnen Punkten, den Pixeln, die jeweils eine eigene Farbe haben. Normalerweise wird der Bildschirm in einem Koordinatensystem (x, y) dargestellt. Der erste Pixel oben links hat die Koordinaten (0,0). Der nachfolgende Pixel rechts davon hat die Koordinate (1,0) und so weiter. Nach unten wird die y-Koordinate jeweils um eins erhöht: (0,1), (0,2) usw.
Auflösungen
Der Raspberry Pi unterstützt viele verschiedene Auflösungen. Eine Übersicht über die gängigsten Auflösungen findest du beispielsweise unter https://de.wikipedia.org/wiki/Bildaufl%C3%B6sung#Computer. Hier ein paar Auflösungen, die verwendet werden könnten:
| Mode | Auflösung |
|---|---|
| VGA | 640 x 480 |
| XGA | 1024 x 768 |
| HD | 1920 x 1080 |
| UHD (only Pi 4 or Pi 5) | 4096 x 2160 |
Die Auflösung allein reicht jedoch nicht aus, um komplexe, farbige Bilder darzustellen. Da Computersysteme nur binäre Zahlen kennen, also nur "1" oder "0", könnte man jedes Pixel lediglich ein- oder ausschalten, was nur ein Schwarz-Weiß-Bild erzeugen würde. Um die Vielzahl der Farben, die wir auf Bildschirmen sehen, darzustellen, werden zusätzliche Standards definiert.
Wird die Anzeige mehrmals hintereinander vertieft, entsteht eine größere Farbtiefe. Wenn ein Bild beispielsweise zwei Bitmaps verwendet, stehen für jeden Punkt zwei Bits zur Verfügung. Das ergibt vier Zustände bzw. vier Farben. Je mehr Bitmaps verwendet werden, desto mehr Farben können angezeigt werden.
Farbtiefe
Der Raspberry Pi unterstützt vier verschiedene Farbtiefen: 8-Bit, 16-Bit, 24-Bit und 32-Bit. In der Praxis wird jedoch von der Verwendung der 8-Bit und 24-Bit Farbtiefe abgeraten, da sie zu Darstellungsfehlern führen können. Siehe hierzu auch die Dokumentation unter https://www.raspberrypi.org/documentation/configuration/config-txt/video.md im Abschnitt framebuffer_depth. Um es einfach zu halten, konzentrieren wir uns auf die 32-Bit Farbtiefe.
Beispiel: Speicheraufwand für Bilder Um den Speicheraufwand zu veranschaulichen, betrachten wir ein Beispiel. Ein monochromes Bild mit einer Auflösung von 20 x 20 Pixel benötigt folgenden Speicherbedarf:
Speicherbedarf (Byte) = (x * y) / 8 (20*20)/8 = 50 BytesFür ein Bild mit 4 Bitmaps beträgt der Speicherbedarf:
Speicherbedarf (Byte) = ((x * y) * tiefe) / 8 ((20*20)*4)/8 = 200 BytesNun ein Beispiel mit einer HD-Auflösung und RGBA32-Farbtiefe:
Speicherbedarf (Bytes) = ((1920 * 1080) * 32) / 8 = 8294400 Bytes
Das entspricht etwa 8,3 Megabytes.Abbildung der Bitmaps im Speicher
Bitmaps werden nicht als eine Reihe von Einzelbildern (Bitmaps) im Speicher abgelegt, sondern die Grafikdaten sind wie in einem dreidimensionalen Raum organisiert. Betrachten wir ein Beispiel mit einer Auflösung von 20 x 20 Pixel und einer Farbtiefe von 16 Bit:
Jeder Bildschirmpunkt wird bei einer Farbtiefe von 16 Bit durch 16 aufeinanderfolgende Bits dargestellt. Der erste Punkt verwendet die Bits 0-15, der nächste die Bits 16-31, und so weiter. Das müssen wir bei den Berechnungen für jeden einzelnen Punkt berücksichtigen.
Der Postbote und TAGs
Wie bereits beschrieben, wird die Grafik nicht von der CPU, sondern von der GPU (Graphics Processing Unit) dargestellt. Damit die GPU weiß, was sie tun soll, benötigt sie bestimmte Informationen zur Darstellung, etwa wie der Bildschirm aussehen soll. Eine direkte Verbindung zwischen CPU und GPU existiert jedoch nicht, weshalb die Kommunikation über einen Vermittler erfolgt, der hier als "Postbote" bezeichnet wird. Die CPU sendet eine Nachricht an den Postboten, und dieser leitet die Nachricht an die GPU weiter. Ebenso kann die GPU Nachrichten an die CPU schicken.
Der Postbote hat die Basisadresse 0xB880 und verwendet "TAGs" für die Informationsübermittlung. Die Dokumentation zu diesen TAGs ist leider spärlich, doch es gibt eine Gemeinschaft, die Informationen gesammelt und bereitgestellt hat. Weitere Informationen findest du hier:
https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface
Diese Informationen habe ich in dem zugehörigen Sourcecode zusammengefasst und in der base.h Datei eingefügt.
Zunächst definieren wir in der base.h die neuen Register, die dafür verantwortlich sind:
//
// Mailbox
//
#define MAILBOX_BASE RPI_BASE + 0xB880
#define MAILBOX0_READ MAILBOX_BASE + 0x00
#define MAILBOX0_STATUS MAILBOX_BASE + 0x18
#define MAILBOX_STATUS_EMPTY 0x40000000
#define MAILBOX1_WRITE MAILBOX_BASE + 0x20
#define MAILBOX1_STATUS MAILBOX_BASE + 0x38
#define MAILBOX_STATUS_FULL 0x80000000
#define MAIL_WRITE MAILBOX_BASE + 0x20
#define MAIL_TAG_WRITE MAIL_WRITE + 0x8
#define MAILBOX_CHANNEL_PM 0 // power management
#define MAILBOX_CHANNEL_FB 1 // frame buffer
#define BCM_MAILBOX_PROP_OUT 8 // property tags (ARM to VC)
Für die Kommunikation verwenden wir "TAGs". Diese TAGs sind in einer bestimmten Struktur organisiert, die von der GPU verstanden wird.
Aufbau der TAG-Struktur
TAG-Listen haben eine festgelegte Struktur, die die GPU versteht. Eine TAG-Struktur beginnt immer mit einer 32-Bit Zahl, die die Länge der gesamten Struktur angibt. Es folgt ein 32-Bit Puffer, in den eine Antwort geschrieben werden kann. Danach kommen die einzelnen TAGs, die wiederum eine Grundstruktur aufweisen: Der Name des TAGs, die Puffergröße in Bytes, ein Antwortpuffer und die Werte des TAGs. Die Struktur endet mit einem NULL-TAG.
Mailbox Länge
Mailbox Puffer
TAG1 Name
TAG1 Länge
TAG1 Puffer
TAG1 Wert1
TAG1 Wert2
...
TAG2 Name
...
TAGx NULL (End TAG)Jeder TAG muss auf 32-Bit ausgerichtet sein.
Aufbau unserer Struktur
Wir erstellen unsere TAG-Struktur und richten sie auf 16 Byte aus, da nur die oberen 28 Bits der Adresse an die Mailbox übergeben werden.
u32 pScreen[36] ALIGN(16) = {
...
}
Die Struktur beginnt mit der Angabe der Länge und einem Puffer für mögliche Antworten:
u32 pScreen[36] ALIGN(16) = {
34*4, //34 Einträge * 4 Bytes
0,
Jetzt fügen wir die Daten hinzu, beginnend mit der Auflösung des Bildschirms. Der TAG Set_physical_display hat folgende Struktur:
TAG Name (Set_physical_display)
Puffergröße in Bytes
Antwort Puffer
Breite des Bildschirms in Pixel
Höhe des Bildschirms in PixelIn unsere Struktur übertragen sieht es so aus:
Set_physical_display, //Tag Identifier
0x8,0x8, //Buffer size in bytes
SCREEN_X, //Width in pixels
SCREEN_Y, //Height in pixels
Die Variablen SCREEN_X und SCREEN_Y definieren wir in der config.h Datei, hier für eine Auflösung von 1920 x 1080 Pixel.
Als nächstes definieren wir den Set_virtual_buffer TAG:
TAG Name (Set_virtual_buffer)
Puffergröße in Bytes
Antwort Puffer
Breite des Bildschirms in Pixel
Höhe des Bildschirms in PixelIn unsere Struktur übertragen sieht es so aus:
Set_virtual_buffer,
0x8,0x8,
SCREEN_X,
SCREEN_Y,
Der Set_depth TAG definiert die Farbtiefe:
TAG Name (Set_depth)
Puffergröße in Bytes
Antwort Puffer
Tiefe der BitplanesIn unsere Struktur übertragen sieht es so aus:
Set_depth, //Tag Identifier
0x4, //Buffer size in bytes
0x4, //Response buffer
BITS_PER_PIXEL, //depth
Auch hier verwenden wir eine Variable BITS_PER_PIXEL, die wir in der config.h Datei auf 32 Bit definieren.
Der Set_virtual_offset TAG erzeugt Offset-Werte:
TAG Name (Set_virtual_offset)
Puffergröße in Bytes
Antwort Puffer
Offset x
Offset yIn unsere Struktur übertragen sieht es so aus:
Set_virtual_offset,
0x8,0x8,
0x0,0x0,
Der Set_palette TAG definiert die Farbpalette:
TAG Name (Set_palette)
Puffergröße in Bytes
Antwort Puffer
erster eingestellter Palettenindex
Anzahl der einzustellenden Paletteneinträge
RGBA PalettenwerteIn unsere Struktur übertragen sieht es so aus:
Set_palette,
0x10,0x10,
0x0,
0x2,
0x00000000,
0xffffffff,
Da wir hauptsächlich 16-Bit oder 32-Bit Farbtiefen verwenden, ist der Eintrag Set_palette weniger wichtig, muss aber dennoch definiert werden.
Der Allocate_buffer TAG sorgt dafür, dass die GPU Speicher reserviert und diesen anzeigt:
TAG Name (Allocate_buffer)
Puffergröße in Bytes
Antwort Puffer
Frame Buffer Basisadresse in Bytes
Bildpuffergröße in BytesIn unsere Struktur übertragen sieht es so aus:
Allocate_buffer,
0x8,0x8,
0x0, //Bufferadresse (pScreen[31])
0x0,
Der TAG erzeugt einen Puffer, dessen Adresse in pScreen[31] abgelegt wird, sobald die GPU dies durchgeführt hat. Dies ist die Adresse, auf die wir später zugreifen können.
Unsere TAG-Liste endet mit einem NULL-TAG:
0x00000000
Diese Struktur beschreibt, was wir darstellen möchten. Die GPU interpretiert diese Werte und versucht, sie darzustellen. Wenn die GPU die Angaben nicht exakt umsetzen kann, erzeugt sie einen ähnlichen Zustand. Wir sollten die von der GPU zurückgemeldeten Werte überprüfen, um sicherzustellen, dass sie unseren Erwartungen entsprechen.
Der Postbote
Nun bringt der Postbote unsere TAG-Liste zur GPU, um den Bildschirm zu erzeugen. Wir verwenden zunächst eine einfache Methode und erweitern später die Funktionen.
Zuerst geben wir dem Postboten den Auftrag, die Informationen an die GPU zu übermitteln. Da die CPU und die GPU nicht synchron laufen, wissen wir zunächst nicht, ob die Anzeige tatsächlich erstellt wurde. Die GPU benötigt Zeit, um unseren Wunsch umzusetzen.
Um sicherzustellen, dass die Anzeige erfolgreich erzeugt wurde, fragen wir kontinuierlich ab, ob eine Adresse für den Bildschirm existiert. Diese Adresse wird in unserer Struktur unter pScreen[31] abgelegt. Wenn die GPU erfolgreich war, schreibt sie die Adresse in dieses Feld. Solange dies nicht der Fall ist, wiederholen wir die Abfrage.
uintptr pScreenAddress = (uintptr)pScreen;
while (TRUE) {
BcmMailBox_Write(BCM_MAILBOX_PROP_OUT, (u32)pScreenAddress);
if (pScreen[31] != 0) {
break;
}
}
Falls die GPU nie eine Adresse zurückgibt, würden wir in einer Endlosschleife festhängen. Später werden wir weitere Funktionen einführen, um solche Probleme zu vermeiden.
Die Adresse, die wir zurückbekommen, ist ein Byte-Wert, mit dem wir so nicht direkt arbeiten können. Wir wandeln diesen Wert um und speichern ihn für spätere Zwecke.
#define ADDRESS_MASK 0x3FFFFFFF
graphicsAddress = pScreen[31] & ADDRESS_MASK;
Auf den Bildschirm zeichnen
Nun haben wir einen Screen, der allerdings leer ist. Wir benötigen eine Methode, um etwas auf den Bildschirm zu zeichnen.
Wir entwickeln eine Funktion, die einen einzelnen Bildpunkt (Pixel) anzeigt. Diese grundlegende Funktion bildet die Basis für weitere grafische Funktionen.
Wir nennen die Funktion DrawPixel und übergeben ihr die Koordinaten x und y.
Für verschiedene Farbtiefen sind unterschiedliche Berechnungen notwendig. Zunächst halten wir es einfach und gehen von einer Farbtiefe von 32 Bit aus.
Zuerst überprüfen wir, ob der Pixel im sichtbaren Bereich liegt, um unerwünschte Nebeneffekte zu vermeiden. Danach berechnen wir die Position nach folgender Formel:
((Gesamtbreite des Screens * Position y) + Position x) * Farbtiefe in BytesDie Screen-Adresse wird zu dieser Position hinzuaddiert:
void DrawPixel(u32 x, u32 y) {
if ((x < SCREEN_X) && (y < SCREEN_Y)) {
write32(DrawColor, graphicsAddress + (((SCREEN_X * y) + x) * 4));
}
}
Damit wird bereits der Pixel angezeigt.
Header-Datei für DrawPixel
Wir erstellen eine Header-Datei, in der wir unsere Funktionen beschreiben:
//
// 20.02.2025 www.satyria.de
//
// screen.h
//
#ifndef _screen_h
#define _screen_h
#include "types.h"
void Init_Screen(void);
void DrawPixel(u32 x, u32 y);
#endif
Anwendung der DrawPixel-Funktion
Wir haben nun eine Funktion, die es ermöglicht, etwas auf dem Bildschirm zu sehen. Nun verwenden wir die neuen Funktionen im Hauptprogramm, um ein Ergebnis anzuzeigen.
Zuerst definieren wir die Variablen, die wir beim Erstellen der TAG-Struktur verwendet haben, in der config.h Datei:
#define SCREEN_X 1920
#define SCREEN_Y 1080
#define BITS_PER_PIXEL 32
Diese Werte definieren die Breite, Höhe und Tiefe des Bildschirms.
Die erste Funktion, die wir aufrufen, ist die Bildschirminitialisierung:
Init_Screen();
Ein einzelner Punkt auf dem Bildschirm ist schwer zu sehen. Daher setzen wir mehrere Punkte nebeneinander und verwenden eine Schleife, die die Koordinaten verändert und jeden Punkt zeichnet:
u32 i;
for (i=100;i<=500;i++)
{
DrawPixel(i,i);
}
Der Quellcode für dieses Beispiel ist hier verfügbar.
| < Zurück (Fehlerbehandlung in C (PI4)) | < Hauptseite > | Weiter (Chars in C (PI4)) > |
