Grafik in C (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
KKeine Bearbeitungszusammenfassung
 
(2 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 68: Zeile 68:


[[Datei:Koordinaten4.png|rand|600x600px]]
[[Datei:Koordinaten4.png|rand|600x600px]]
Wie hier zu sehen ist, wird für den ersten Bildschirmpunkt die Bits 0-15 verwendet. Der nächste Bildschirmpunkt die Bits 16-31 usw… Dies müssen wir bei Berechnungen jeden einzelnen Punktes später berücksichtigen.


== 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.


Wie ich bereits vorher beschrieben habe, wird die Grafik nicht durch die CPU dargestellt, sondern über einen eigenen Prozessor (GPU). Zunächst muss der GPU mitgeteilt werden, was den gemacht werden soll. Dazu benötigt er Informationen, was und wie er es Darstellen soll. Also wie der Bildschirm aussehen soll. Eine direkte Verbindung zwischen der CPU und GPU exsitiert nicht und damit ist die bereits erlernte Art, wie der Prozessor mit der Pereferie kommuniziert so nicht mehr möglich. Allerdings gibt es einen Vermitler, der das ganze übernimmt. Er wird hier als Postbote genant. Wie im Leben, schreibt die CPU einen Brief, den er an den Postboten übergibt und er übergibt diesen Brief der GPU. Umgekehrt ist dies auch möglich. Also damit können Nachrichten von GPU an die CPU übermittelt werden.
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 für den CPU die Basisadresse 0xB880 und wir verwenden "TAGs" für Informationen. Leider ist die Dokumentations dazu sehr spärlich. Allerdings gibt es eine Gruppe, die Informationen gesammelt hat und diese der Gemeinschaft weiter gegeben haben. Schaut einfach mal dort vorbei:
https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface


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:


Diese Informationen habe ich in der zu diesem Teil gehörendem Sourcecode zusammen gesetzt und in der base.inc eingefügt.
<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


Zunächst werden die neuen Register, welches dafür verantwortlich sind, in der “base.inc” definiert.
#define MAIL_WRITE MAILBOX_BASE + 0x20
#define MAIL_TAG_WRITE MAIL_WRITE + 0x8


<syntaxhighlight lang="asm">
#define MAILBOX_CHANNEL_PM  0      // power management
@Mailbox
#define MAILBOX_CHANNEL_FB  1      // frame buffer
.equ MAILBOX_BASE, RPI_BASE + 0xB880
#define BCM_MAILBOX_PROP_OUT    8      // property tags (ARM to VC)
.equ MAIL_WRITE, MAILBOX_BASE + 0x20
.equ MAIL_TAG_WRITE, MAIL_WRITE + 0x8
</syntaxhighlight>
</syntaxhighlight>


Wie bereits geschrieben, werden in diesem Beispiel "TAGs" verwendet. Damit verwenden wir die "MAIL_TAG_WRITE" Funktion.
Für die Kommunikation verwenden wir "TAGs". Diese TAGs sind in einer bestimmten Struktur organisiert, die von der GPU verstanden wird.
Da beide Prozessoren nicht unbedingt die gleiche Sprache sprechen, wurde eine Art Universalsprache zwischen beiden Prozessoren vereinbart, die über TAG-Listen gesteuert wird.


=== Aufbau der TAG-Struktur ===
=== Aufbau der TAG-Struktur ===
TAG-Listen, oder auch Struktur genannt, sind Listen, die eine bestimmte und festgelegte Struktur aufweisen. Diese Struktur muss der gegenüberliegende Partner, in diesem Fall die GPU verstehen.
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.
 
Für unsere GPU, die wir verwenden, erwartet der Prozessor folgende Struktur:
 
Eine TAG-Struktur fängt immer mit einer 32-Bit Zahl an, die die Länge der gesamten Struktur angibt. Als nächstes Benötigt die Struktur einen 32-Bit Puffer, um eventuell eine Antwort abzulegen, der bei einem Aufruf übermittelt wird. Im Anschluß erfolgen die einzelnen TAGs.
Jeder TAG folgt wieder einer Grundstruktur. Zunächst wird der Name des TAGs übermittelt, Anschließend die Anzahl der Werte in Bytes (32Bit, entsprechend 4) und wieder ein Puffer, für die Antwort. Im Anschluss erfolgen die Werte, für den jeweiligen einzelnen TAG.
Die gesamte Struktur wird mit einem NULL-TAG abgeschlossen.


<syntaxhighlight>
<syntaxhighlight>
Zeile 116: Zeile 119:
</syntaxhighlight>
</syntaxhighlight>


Wichtig ist hierbei noch zu wissen, dass jeder einzelne TAG auf 32-Bit ausgerichtet sein muss.
Jeder TAG muss auf 32-Bit ausgerichtet sein.


Kommen wir nun zu unserer Struktur, die wir wie folgt aufsetzen. Zunächst müssen wir die Struktur auf 16 Byte ausrichten, da nur die oberen 28 Bits der Adresse an die Mailbox übergeben werden.
== 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 126: Zeile 130:
</syntaxhighlight>
</syntaxhighlight>


Zunächst geben wir unseren TAG-Struktur einen Namen. Wir nennen sie "pScreen". Als erster Eintrag wir zunächst die Länge der gesamten Struktur angegeben und noch ein Puffer, für mögliche Antworten erzeugt.
Die Struktur beginnt mit der Angabe der Länge und einem Puffer für mögliche Antworten:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 134: Zeile 138:
</syntaxhighlight>
</syntaxhighlight>


Damit haben wir nun schon den Kopf geschrieben. Die nächsten Einträge sind nun unsere Daten, was wir denn wollen.
Jetzt fügen wir die Daten hinzu, beginnend mit der Auflösung des Bildschirms. Der TAG Set_physical_display hat folgende Struktur:
Den ersten Eintrag, den wir verwenden, ist die Auflösung unserer Anzeige, die wir verwenden wollen. Der TAG, den wir hierfür benötigen heißt "Set_physical_display" und hat folgendes Aussehen:


<syntaxhighlight>
<syntaxhighlight>
Zeile 145: Zeile 148:
</syntaxhighlight>
</syntaxhighlight>


Übertragen in unsere Struktur sieht es wie folgt aus:
In unsere Struktur übertragen sieht es so aus:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 154: Zeile 157:
</syntaxhighlight>
</syntaxhighlight>


Für die Breite und Höhe habe ich Variablen eingesetzt, damit die Struktur zumindest für die Programmierung variabel bleibt. Die SCREEN_X und SCREEN_Y werden in der "config.h" Datei definieren und hier im ersten Beispiel eine Auflösung von 1920 x 1080 Pixel verwenden.  
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.


Die GPU möchte auch Angaben des Virtuellen Puffer haben. Dazu wird der TAG "Set_virtual_buffer" verwendet. Seine Struktur ist ähnlich die, die wir zuvor erstellt haben.
Als nächstes definieren wir den Set_virtual_buffer TAG:


<syntaxhighlight>
<syntaxhighlight>
Zeile 166: Zeile 169:
</syntaxhighlight>
</syntaxhighlight>


Und wieder umgesetz in unsere Struktur:
In unsere Struktur übertragen sieht es so aus:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 175: Zeile 178:
</syntaxhighlight>
</syntaxhighlight>


Der nächste TAG "Set_depth" definiert die Tiefe des Bildschirms.
Der Set_depth TAG definiert die Farbtiefe:


<syntaxhighlight>
<syntaxhighlight>
Zeile 183: 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 191: Zeile 196:
</syntaxhighlight>
</syntaxhighlight>


Auch hier verwenden wir eine Variable (BITS_PER_PIXEL), die wir in der "config.h" Datei auf 32 Bit definieren werden.
Auch hier verwenden wir eine Variable BITS_PER_PIXEL, die wir in der config.h Datei auf 32 Bit definieren.


Der nächste TAG "Set_virtual_offset" wird verwendet um einen Offset zu erzeugen.
Der Set_virtual_offset TAG erzeugt Offset-Werte:


<syntaxhighlight>
<syntaxhighlight>
Zeile 203: Zeile 208:
</syntaxhighlight>
</syntaxhighlight>


Es ist durchaus möglich, dass wir für die physikalische Anzeige oder dem virtuellen Puffer unterschiedliche Dimensionen übergeben haben. Damit ist es möglich, dass auf dem Bildschirm nur ein Ausschnitt der Anzeige angezeigt wird. Mit diesem Offset wird genau dieser Ausschnitt festgelegt. In unserem Fall verwenden wir keinen Offset und haben bereits zuvor die Anzeige und den Puffer gleich dimensioniert. Also wir setzen die Position des Offsets auf NULL.
In unsere Struktur übertragen sieht es so aus:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 211: Zeile 216:
</syntaxhighlight>
</syntaxhighlight>


Dann kommen wir zu unseren Farben. Hierfür ist augenscheinlich der TAG "Set_palette" zuständig und wird in der TAG-Liste benötigt.
Der Set_palette TAG definiert die Farbpalette:


<syntaxhighlight>
<syntaxhighlight>
Zeile 222: Zeile 227:
</syntaxhighlight>
</syntaxhighlight>


Nun haben wir gelernt, dass Anzeigen mit einer Farbtiefe von 8 Bit, Paletten für die Anzeige verwenden, wir aber eigentlich darauf verzichten sollen. Wir werden für unsere Anzeigen nur 16-Bit oder 32-Bit verwenden. Damit ist dieser Eintrag nicht wirklich wichtig. Denoch müssen wir diesen Teil erstellen.
In unsere Struktur übertragen sieht es so aus:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 233: Zeile 238:
</syntaxhighlight>
</syntaxhighlight>


Mit dem nächsten TAG (Allocate_buffer) beauftragen wir die GPU Speicher zu reservieren und den Inhalt des Speichers anzuzeigen.
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 242: 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 250: Zeile 259:
</syntaxhighlight>
</syntaxhighlight>


Der TAG erzeugt einen Puffer, der die Adresse des Speichers für den Screen zurück gibt. Dieser Wert, wird im Label "pScreen[31]" abgelegt, sobald die GPU dies erfolgreich durchgeführt hat. Dies ist dann auch die Adresse, auf die wir später zurückgreifen können.
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 ist damit beendet und wir müssen hier noch den ENDE-TAG eintragen.
Unsere TAG-Liste endet mit einem NULL-TAG:


<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
Zeile 258: Zeile 267:
</syntaxhighlight>
</syntaxhighlight>


Soweit so gut. Diese Struktur beschreibt nun das, was wir gerne hätten. Die GPU interpretiert diese Werte und versucht dies auch darzustellen. Wenn die GPU dies nicht umsetzen kann, versucht die GPU einen annährend Zustand zu erzeugen. Dies bedeutet, dass nicht unbedingt das Angezeigt wird, was wir vorgegeben haben.
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.
Dies kann zu Problemen kommen, da manche Funktionen, auch die, die wir später erstellen, von zum Beispiel der Dimension und/oder der Farbtiefe abhängig sind. Unsere erstellte TAG-Liste ist allerdings keine Einbahnstraße. Auch die GPU verwendet diese, um uns die Werte anzugeben, wie den tatsächlich das Ergebnis aussieht. Diese Werte, die dann wichtig sind, sollten dann jedes mal überprüft werden, ob diese so passen.


=== Der Postbote ===
=== Der Postbote ===


Nun kommt der Postbote ins Spiel. Wir geben ihm unsere TAG-Liste und erzeugen damit unseren Bildschirm. Wir werden zunächst eine einfache Methode verwenden und später die Funktionen erweitern.
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.


Damit haben wir dem Postboten zumindest den Auftrag gegeben, dass er diese Informationen an den GPU zu übergeben. Leider laufen die beiden Prozessoren nicht synchron und wir wissen zu diesem Zeitpunkt nicht, ob nun wirklich die Anzeige erzeugt wurde. Für die GPU ist unser Wunsch auch recht aufwendig, welche Zeit benötigt.
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 nun sicher zu sein, dass es geklappt hat, fragen wir einfach nach, ob es eine Adresse gibt, wo den unser Bildschirm abgelegt wurde. Innerhalb unserer Struktur wird unter pScreen[31] dieser Wert abgelegt. Genau dort schreibt die GPU die Adresse hinein, wenn sie erfolgreich war. Solange diese Adresse nicht gesetzt ist, wissen wir, dass die GPU noch nicht fertig ist. Wir wiederholen einfach diese Aufforderung, bis hier ein Wert steht.
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;
uintptr pScreenAddress = (uintptr)pScreen;
    while (TRUE) {
while (TRUE) {
        BcmMailBox_Write(BCM_MAILBOX_PROP_OUT,(u32)pScreenAddress);
    BcmMailBox_Write(BCM_MAILBOX_PROP_OUT, (u32)pScreenAddress);
       
   
        if (pScreen[31] != 0)
    if (pScreen[31] != 0) {
         {
         break;
            break;
        }
     }
     }
}
</syntaxhighlight>  
</syntaxhighlight>  


Problematisch wird es allerdings, wenn die GPU nie irgendwas zurück gibt, dann sind wir hier in einer Dauerschleife. Aber wollen wir nicht so schwarz sehen und hoffen, dass es hier zu keinem Fehler kommt. Hier verwenden wir später andere Funktionen, damit wir hier nicht in Problemen bei anderen Abfragen kommen.
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ück bekommen ist ein "byte" Wert, mit dem wir so nicht arbeiten können. Wir müssen diesen zuerst für unsere Zwecke umwandeln, was wir mit einem einfachen "and" durchführen. Anschließend speichern wir den Wert für spätere Zwecke.
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;
    graphicsAddress = pScreen[31] & ADDRESS_MASK;
</syntaxhighlight>
</syntaxhighlight>


== Auf den Bildschirm zeichnen ==
== Auf den Bildschirm zeichnen ==


Nun haben wir einen Screen. Naja, dieser Screen ist leer und man sieht eigentlich nichts. Also müssen wir einen Weg finden, dass wir in irgend einer Weise etwas auf den Bildschirm bekommen.
Nun haben wir einen Screen, der allerdings leer ist. Wir benötigen eine Methode, um etwas auf den Bildschirm zu zeichnen.


Dazu werden wir eine Funktion entwickeln, die einen einzelnen Bildpunkt definiert und anzeigt. Diese grundlegende Funktion wird die Ausgangsbasis für alle grafischen Funktionen sein.
Wir entwickeln eine Funktion, die einen einzelnen Bildpunkt (Pixel) anzeigt. Diese grundlegende Funktion bildet die Basis für weitere grafische Funktionen.


Diese Funktion nennen wir “DrawPixel” und übergeben ihr die Koordinate in x und y.
Wir nennen die Funktion DrawPixel und übergeben ihr die Koordinaten x und y.


Für verschiedene Farbtiefen, sind verschiedene Berechnungen notwendig, um die richtige Farbe darzustellen. Genau so ist diese Farbtiefe auch verantwortlich, wo den die einzelnen Bildpunkte im Speicher zu finden sind. Zunächst werden wir diese Funktion relativ einfach halten und einfach davon ausgehen, dass wir eine Farbtiefe von 32 Bit haben.
Für verschiedene Farbtiefen sind unterschiedliche Berechnungen notwendig. Zunächst halten wir es einfach und gehen von einer Farbtiefe von 32 Bit aus.


Zunächst überprüfen wir, ob der Pixel überhaupt im sichtbaren Bereichs liegt. Es würde sonst zu unerwünschten Nebeneffekten kommen, denn wir wissen nicht, welche Funktion die entsprechende Speicherstelle im System hat.
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:
Wenn wir es überprüft haben, können wir die richtige Position berechnen. Dies erfolgt nach folgender Formel:


<syntaxhighlight>
<syntaxhighlight>
((Gesammtscreenbreite * Position y) + Position x) * Screentiefe in Byte
((Gesamtbreite des Screens * Position y) + Position x) * Farbtiefe in Bytes
</syntaxhighlight>
</syntaxhighlight>


Dazu müssen wir die ScreenAdresse noch zu dieser Position hinzuaddieren.
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 323: Zeile 325:
Damit wird bereits der Pixel angezeigt.  
Damit wird bereits der Pixel angezeigt.  


Damit ist unsere vorläufige “DrawPixel” Funktion erstmal fertig und erzeugen eine neue Headerdatei in der wir diese Funktionen beschreiben.
== Header-Datei für DrawPixel ==
 
Wir erstellen eine Header-Datei, in der wir unsere Funktionen beschreiben:
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
//
//
Zeile 336: 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 344: Zeile 348:
== Anwendung der DrawPixel-Funktion ==
== Anwendung der DrawPixel-Funktion ==


Wir haben jetzt eine Funktion geschrieben, die es ermöglicht, etwas auf dem Bildschirm zu sehen. Jetzt müssen wir nur noch unsere neuen Funktionen im Hauptprogramm verwenden und wir dürften ein Ergebniss sehen.
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.


Als wir die TAG-Struktur erstellten, haben wir einige Variablen benutzt, die wir erstmal festlegen müssen. Dazu verwenden wir unsere "config.h" Datei und ergänzen folgende Zeilen:
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 353: 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.


Damit definieren wir die Breite, die Höhe und die Tiefe unserer Bildschirmes.
Die erste Funktion, die wir aufrufen, ist die Bildschirminitialisierung:
 
Die erste Funktion, die wir aufrufen müssen, ist die Erstellung des Bildschirms. HIer wird nichts übergeben und wir bekommen nichts zurück.
 
<syntaxhighlight lang="C">
<syntaxhighlight lang="C">
   Init_Screen();
   Init_Screen();
</syntaxhighlight>
</syntaxhighlight>


Einen einzelnen Punkt auf einem Bildschirm zu sehen, ist manchmal recht schwierig. Dazu werden wir mehrere Punkte nebeneinander setzen und verwenden eine Schleife, die die Koordinaten entsprechend verändert und lassen uns jedesmal den Punkt zeichnen.
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 372: Zeile 374:
</syntaxhighlight>
</syntaxhighlight>


Der Sourcecode, für dieses Beispiel ist hier zum laden bereit: [https://www.satyria.de/arm/sources/RPI4/C/4.zip]
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 Bytes

Für ein Bild mit 4 Bitmaps beträgt der Speicherbedarf:

Speicherbedarf (Byte) = ((x * y) * tiefe) / 8      ((20*20)*4)/8 = 200 Bytes

Nun 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 Pixel

In 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 Pixel

In 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 Bitplanes

In 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 y

In 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 Palettenwerte

In 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 Bytes

In 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 Bytes

Die 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)) >