Grafik in C (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Zeile 295: Zeile 295:
Diese Funktion nennen wir “DrawPixel” und übergeben ihr die Koordinate in x und y.
Diese Funktion nennen wir “DrawPixel” und übergeben ihr die Koordinate in x und y.


<syntaxhighlight lang="c">
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.
void DrawPixel(u32 x, u32 y)
{
    if ((x < SCREEN_X) && (y < SCREEN_Y))
    {
        write32(DrawColor,graphicsAddress+(((SCREEN_X*y)+x)*4));
    }
}
</syntaxhighlight>
 
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. Die 16 Bit Unterstützung werden wir später erstellen.


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.
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.
   
   
<syntaxhighlight lang="asm">
  mov r3,#SCREEN_X    @Load screen width to r3
  sub r3,#1            @Subtract it by one
  cmp r0,r3            @And compare it with transfer
  pophi {pc}          @If it's higher then go back
  mov r3,#SCREEN_Y    @Load screen height to r3
  sub r3,#1            @Subtract it by one
  cmp r1,r3            @And compare it with transfer
  pophi {pc}          @If it's higher then go back
</syntaxhighlight>
Wenn wir es überprüft haben, können wir die richtige Position berechnen. Dies erfolgt nach folgender Formel:
Wenn wir es überprüft haben, können wir die richtige Position berechnen. Dies erfolgt nach folgender Formel:


Zeile 327: Zeile 305:
</syntaxhighlight>
</syntaxhighlight>


Umgesetzt für unseren Code bedeutet es:
<syntaxhighlight>
((SCREEN_X * r1) + r0) * 32/8
</syntaxhighlight>
<syntaxhighlight lang="asm">
  mov r3,#SCREEN_X    @width to r3
  mul r3,r1            @multibly with pos y
  add r3,r0            @add pos x
  mov r4,#4
  mul r3,r4            @multibly with 4 (32/8)
</syntaxhighlight>
Dies entspricht fast 1:1 der zuvor festgelegten Berechnung. Nur bei der letzten Berechnung haben wir 32/8 gekürzt und multiplizieren einfach mit 4, was das gleiche entspricht. Damit haben wir die Position im Screen definiert. Nun müssen wir einfach nur noch die Farbe im Screen ablegen.
Dazu müssen wir die ScreenAdresse noch zu dieser Position hinzuaddieren.
Dazu müssen wir die ScreenAdresse noch zu dieser Position hinzuaddieren.


<syntaxhighlight lang="asm">
<syntaxhighlight lang="c">
  ldr r4,=graphicsAddress  @Load Graphicaddress to r4
void DrawPixel(u32 x, u32 y)
  ldr r4,[r4]
{
  add r4,r3                @add pos of Pixel
    if ((x < SCREEN_X) && (y < SCREEN_Y))
  str r2,[r4]              @Store Color to Graphic
    {
        write32(DrawColor,graphicsAddress+(((SCREEN_X*y)+x)*4));
    }
}
</syntaxhighlight>
</syntaxhighlight>


Damit wird bereits der Pixel angezeigt. Wenn wir jetzt allerdings die Funktion so beibehalten, werden wir irgendwann feststellen, dass das System relativ träge arbeitet. Dies liegt an den Multiplikationen, die wir hier verwendet haben. Multiplikationen bedeuten für den ARM-CPU eine Menge Rechenaufwand. Auf dies sollte, wenn es möglich ist, verzichtet werden.
Damit wird bereits der Pixel angezeigt.  


Schauen wir erstaml die erste Multiplikation an. HIer fällt mir auf die schnelle nichts ein, wie wir diese Multiplikation umgehen können, da wir nicht wissen, was als "y" übergeben wird. Wenn wir aber den nächsten Befehl anschauen, wird hier ein Wert addiert. Der ARM-Prozessor bietet hier einen interessanten Befehl an, der Multipliziert und kann zugleich eine Zahl addieren, genau das, was wir brauchen.
Damit ist unsere vorläufige “DrawPixel” Funktion erstmal fertig und erzeugen eine neue Headerdatei in der wir diese Funktionen beschreiben.
<syntaxhighlight lang="c">
//
// 20.02.2025 www.satyria.de
//
// screen.h
//


<syntaxhighlight lang="asm">
#ifndef _screen_h
  mov r3,#SCREEN_X    @width to r3
#define _screen_h
  mla r3,r1,r3,r0      @multibly with pos y and add pos x
</syntaxhighlight>


Immerhin haben wir hier zumindest mal die addition gespart.
#include "types.h"
r3 müsste nun noch mit 4 multipliziert und zur Bildschirmposition addiert werden. Zahlenwerte, die mit 2, 4, 8, 16, 32, ... multipliziert werden, sind im Binärer Form sehr einfach umzusetzen, wenn wir hier einfach die Bitfolge ein Bit nach links verschieben, haben wir die Zahl mit zwei multipliziert. Wenn man nun zwei Bits nach links verschiebt, wird daraus eine Multiplikation um 4.
mla war bereits eine kombinierter Befehl. Es gibt einige Befehle, die zusätzlich zu ihrem eigentlichen Sinn eine Shift-Operation zulassen. Darunter zählt auch der add-Befehl. Hier erstmal der Sourcecode dafür, wie ich es mir Vorstelle:


<syntaxhighlight lang="asm">
void Init_Screen(void) ;
  ldr r4,=graphicsAddress    @Load Graphicaddress to r4
void DrawPixel(u32 x, u32 y);
  ldr r4,[r4]


  add r4,r3,lsl #2  @multibly r3 with 4 and
#endif
                    @Add the result to the graphic address
  str r2,[r4]      @Store the Color to graphic address
</syntaxhighlight>
</syntaxhighlight>
Wir schauen uns hier den add Befehl an. Hier wird ein lsl #2 angehängt. Dies ist ein Shift-Befehl, der Bits nach links verschiebt. Dies wird in unserem Fall mit dem Register r3 verwendet und anschließend mit r4 addiert. Das Ergebniss steht danach in r4. Obwohl hier r3 augenscheinlich verändert wurde, ist r3 nach dieser Aktion unverändert.
Auf diese Weiße, haben wir hier eine ganze Multiplikation eingesparrt.
Damit ist unsere vorläufige “DrawPixel” Funktion erstmal fertig und erzeugen eine neue Headerdatei (drawing.h) in der wir diese Funktion ablegen.


== Anwendung der DrawPixel-Funktion (5.1) ==
== Anwendung der DrawPixel-Funktion (5.1) ==

Version vom 5. März 2025, 08:23 Uhr

Bisher sind die Ergebnisse, die wir bisher programmiert haben, nicht allzu aufwendig gewesen. Bisher konnte der Raspberry nicht wirklich mit der Ausenwelt kommunizieren. Es war "nur" eine LED, die geleuchtet oder geblinkt hat. Natürlich ist mit diesem Wissen bisher, jeder Zeit möglich, den GPIO entsprechend zu programmieren, so dass viel mehr gemacht werden kann, als diese Beispiele bisher zeigten. Im Internet gibt es viele Beispiele, für welche Dinge genau diese Schnittstelle verwendet werden könnte. Gerne kannst du das tun. Mir selbst hat das nicht gereicht. Ich möchte mehr, als nur das. Immerhin bietet der Raspberry Pi auch einen HDMI Ausgang, der nur darauf wartet, verwendet zu werden. Also machen wir uns dran, diesen zu nutzen. Wir schließen nun einen Monitor an des Raspberry Pi an und lassen uns die Ergebnis direkt vom Raspberry Pi anzeigen. Da vom Grunde her, da wir in Barre Metal schreiben, der Raspberry “Dumm” ist und auch nicht weiß, wie er es machen soll, müssen wir entsprechende Routinen schreiben. Der Raspberry Pi 4 besitzt zwei HDMI Ausgänge, mit dem er zwei Bildschirme ansteuern kann. Beide können hier bis zu UHD Auflösung anzeigen. Auf dem Raspberry Pi ist hierzu ein eigener Prozessor (GPU) zuständig, der die arbeit der CPU abnimmt.

Übrigens: Die GPU ist auch zuständig dafür, dass der Raspberry überhaupt startet. Sie lädt die Firmware in den Speicher und übergibt dann die Arbeit an die CPU. Dies ist während des Startvorgangs, wenn ein Bildschirm angeschlossen ist, gut sichtbar.

Der Raspberry Pi ist ein wahres Wunder, was seine Fähigkeiten der Grafikanzeige betreffen. Der Raspberry Pi 4 kann bis zu 4096 x 2160 Pixel (entspricht eine Auflösung von 4k) bei 60 Hz anzeigen. Wenn beide HDMI-Ports verwendet werden ist die Herzzahl halbiert. Ältere Modelle unterstützen immerhin 1920 x 1200 bei 60Hz.

Da wir die Grafik programmieren wollen, müssen wir erstmal verstehen, wie eine Grafik überhaupt dargestellt wird. Die Anzeige, die visuell aufgenommen wird, besteht aus einzelnen Punkten (Pixel). Jeder Punkt hat eine eigene Farbe. In der Regel wird der Bildschirm mit einem Koordinatensystem (x,y) dargestellt. Der erste Punkt (links oben) hat die Koordinate 0,0. Der nächste, rechts davon erhöht sich x um eins und hat dann die Koordinate 1,0 usw… Nach unten wird y um jeweils eins erhöht, 1,0 2,0 usw.

Auflösungen

Der Raspberry Pi kennt viele Auflösungen, die er unterstützt. Eine große Auswahl kann über https://de.wikipedia.org/wiki/Bildaufl%C3%B6sung#Computer nachgeschlagen werden. Folgende Auflösungen werde ich hierbei verwenden:

Mode Auflösung
VGA 640 x 480
XGA 1024 x 768
HD 1920 x 1080
UHD (only Pi 4) 4096 x 2160

Die Auflösung einer Anzeige ist allerdings nicht alles. Da Computersysteme nur Binäre Zahlen kennen, als nur "1" oder "0", könnte man jedes Feld gerade mal an oder ausschalten. Dies erzeugt damit nicht die vielen Farben, die wir auf den Bildschirmen sehen, sondern erzeugt gerade mal ein Monochromes Bild, ein hartes Schwarz-Weiß Bild. In diesem Fall wird dann nur eine Bitmap verwendet. Aus diesem Grund wurden für Computergrafiken zusätzliche Standards definiert, wie die Farben dargestellt werden können.

Wiederholt man die Anzeige mehrmals aufeinander, so entsteht mehr Tiefe. Wenn nun eine Anzeige zwei Bitmaps verwendet, so stehen für jeden Punkt zwei Bits zur Verfügung. Bei zwei Bits sind damit schon mal vier Zustände möglich, also auch vier Farben. Um so mehr man diese Bitmaps vertieft, um so mehr Farben sind möglich.

Farbtiefe

Auf dem Raspberry Pi werden 4 Modies angeboten, die verwendet werden könnten. Laut Dokumentation werden die Farbtiefen mit 8-Bit, 16-Bit, 24-Bit und 32-Bit angegeben. Unter 8-Bit wird eine Farbpalette verwendet, die die entsprechenden Farben anbietet. Laut Dokumentation wird allerdings von 8-Bit und 24-Bit abgeraten, da diese zu Fehlern bei der Darstellung kommen könnte. Siehe dazu auch https://www.raspberrypi.org/documentation/configuration/config-txt/video.md unter den Punkt framebuffer_depth. Damit wir es einfach haben, beschränken wir uns auf die 32-Bit Darstellungen.

Ich möchte mal mit einen kleinen Beispiel zeigen, welcher Speicheraufwand nötig ist, entsprechende Bilder darzustellen. Als Beispiel habe ich hier ein Bild mit einer Bitmap (Monochrome), mit einer Auflösung von 20 x 20 Pixel. Der Speicherbedarf errechnet sich bei Monochromer Darstellung folgend:

Speicherbedarf (Byte) = (x * y) / 8         (20*20)/8 = 50 Bytes

Die Berechnung für mehr Bitmaps erfolgt mit folgender Formel und als Beispiel ein Bild mit 4 Bitplanes:

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

Aber verwenden wir mal ein reales Verhältnis, HD-Auflösung mit einer RGBA32-Farbtiefe.

((1920 * 1080) * 32) / 8 = 8294400 Bytes ->  ~8,3 MegaBytes

Abbildung der Bitmaps im Speicher

Die Bitmaps werden nicht als Bitmaps im Speicher abgelegt. Die Grafikdaten muss man sich wie in einem dreidimensionaler Raum vorstellen. Als Beispiel habe ich hier ein 20x20 Pixel mit einer Farbtiefe von 16 Bits dargestellt:

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

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

Diese Informationen habe ich in der zu diesem Teil gehörendem Sourcecode zusammen gesetzt und in der base.inc eingefügt.

Zunächst werden die neuen Register, welches dafür verantwortlich sind, in der “base.inc” definiert.

@Mailbox
.equ MAILBOX_BASE, RPI_BASE + 0xB880 
.equ MAIL_WRITE, MAILBOX_BASE + 0x20
.equ MAIL_TAG_WRITE, MAIL_WRITE + 0x8

Wie bereits geschrieben, werden in diesem Beispiel "TAGs" verwendet. Damit verwenden wir die "MAIL_TAG_WRITE" Funktion. 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

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.

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.

Mailbox Länge
Mailbox Puffer
   TAG1 Name
   TAG1 Länge
   TAG1 Puffer
      TAG1 Wert1
      TAG1 Wert2
      ...
   TAG2 Name
   ...
   TAGx NULL (End TAG)

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

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.

u32 pScreen[36] ALIGN(16) = {
...
}

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.

u32 pScreen[36] ALIGN(16) = {
    34*4, //34 Einträge * 4 Bytes
    0,

Damit haben wir nun schon den Kopf geschrieben. Die nächsten Einträge sind nun unsere Daten, was wir denn wollen. 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:

TAG Name (Set_physical_display)
Puffergröße in Bytes
Antwort Puffer
Breite des Bildschirms in Pixel
Höhe des Bildschirms in Pixel

Übertragen in unsere Struktur sieht es wie folgt aus:

    Set_physical_display, //Tag Identifier
    0x8,0x8,              //Buffer size in bytes
    SCREEN_X,             //Width in pixels
    SCREEN_Y,             //Height in pixels

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

TAG Name (Set_virtual_buffer)
Puffergröße in Bytes
Antwort Puffer
Breite des Bildschirms in Pixel
Höhe des Bildschirms in Pixel

Und wieder umgesetz in unsere Struktur:

    Set_virtual_buffer,
    0x8,0x8,
    SCREEN_X,
    SCREEN_Y,

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

TAG Name (Set_depth)
Puffergröße in Bytes
Antwort Puffer
Tiefe der Bitplanes
  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 werden.

Der nächste TAG "Set_virtual_offset" wird verwendet um einen Offset zu erzeugen.

TAG Name (Set_virtual_offset)
Puffergröße in Bytes
Antwort Puffer
Offset x
Offset y

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.

    Set_virtual_offset,
    0x8,0x8,
    0x0,0x0,

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

TAG Name (Set_palette)
Puffergröße in Bytes
Antwort Puffer
erster eingestellter Palettenindex
Anzahl der einzustellenden Paletteneinträge
   RGBA Palettenwerte

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.

    Set_palette,
    0x10,0x10,
    0x0,
    0x2,
    0x00000000,
    0xffffffff,

Mit dem nächsten TAG (Allocate_buffer) beauftragen wir die GPU Speicher zu reservieren und den Inhalt des Speichers anzuzeigen.

TAG Name (Allocate_buffer)
Puffergröße in Bytes
Antwort Puffer
Frame Buffer Basisadresse in Bytes
Bildpuffergröße in Bytes
    Allocate_buffer,
    0x8,0x8,
    0x0,          //Bufferadresse (pScreen[31])
    0x0,

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.

Unsere TAG-Liste ist damit beendet und wir müssen hier noch den ENDE-TAG eintragen.

  0x00000000

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

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.

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.

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.

    uintptr pScreenAddress = (uintptr)pScreen;
    while (TRUE) {
        BcmMailBox_Write(BCM_MAILBOX_PROP_OUT,(u32)pScreenAddress);
        
        if (pScreen[31] != 0)
        {
            break;
        }
    }

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.

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.

#define ADDRESS_MASK 0x3FFFFFFF

    graphicsAddress = pScreen[31] & ADDRESS_MASK;

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.

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.

Diese Funktion nennen wir “DrawPixel” und übergeben ihr die Koordinate in 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.

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.

Wenn wir es überprüft haben, können wir die richtige Position berechnen. Dies erfolgt nach folgender Formel:

((Gesammtscreenbreite * Position y) + Position x) * Screentiefe in Byte

Dazu müssen wir die ScreenAdresse noch zu dieser Position hinzuaddieren.

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.

Damit ist unsere vorläufige “DrawPixel” Funktion erstmal fertig und erzeugen eine neue Headerdatei in der wir diese 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 (5.1)

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.

Als wir die TAG-Struktur erstellten, haben wir einige Variablen benutzt, die wir erstmal festlegen müssen.

.equ SCREEN_X,       1920
.equ SCREEN_Y,       1080
.equ BITS_PER_PIXEL, 32

Damit definieren wir die Breite, die Höhe und die Tiefe unserer Bildschirmes.

Die erste Funktion, die wir aufrufen müssen, ist die Erstellung des Bildschirms. HIer wird nichts übergeben und wir bekommen nichts zurück.

   bl FB_Init     @Open Screen

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.

   mov r5,#0               @Set r5 to 0 for a loop
   loop:
      mov r0,r5            @Copy r5 to r0 (x)
      mov r1,r5            @Copy r5 to r1 (y)
      mov r2,#0xffffffff   @Color white according to r2
      bl DrawPixel         @And draw the pixel
      add r5,#1            @Add r5 with one
      cmp r5,#10           @As long as 10 is not reached
      bne loop             @Continue the loop

Der Sourcecode, für dieses Beispiel ist hier zum laden bereit: 5.1.zip

Neue Befehle (1)

pophi

POPHI ist ein kombinierter Befehl aus pop und einem Bedingungscode. Durch das Suffix "hi" wird der Befehl nur ausgeführt, wenn zuvor das Ergebnis größer war.

mul

MUL multiplizier zwei Register.

mla

MLA multipliziert zwei Register und addiert das dritte Register.

Beispiel:

mla r0,r1,r4,r0

Wenn r0 = 20, r1 = 33, r4 = 22 sind, ergibt sich folgende Rechnung:

(33 * 22) + 20 = 746

In r0 wird nach der Berechnung 746 stehen.

Modifizierung der DrawPixel-Funktion (5.2)

Unsere bisherige DrawPixel-Funktion kann zur Zeit nur eine Farbtiefe von 32 Bit verarbeiten. Da wir aber auch die Farbtiefe von 16 Bit unterstützen wollen, müssen wir dieser Funktion noch mit einer 16-Bit Variante erweitern, damit wir uns hier nicht mehr weiter darum kümmern müssen und uns ganz auf alles andere Konzentrieren können.

Auch die Farbe selbst ist abhängig von der Farbtiefe, deswegen werden wir hierfür eine neue Funktion erstellen, die die Farbe für jede Farbtiefe erzeugt und aus der DrawPixel-Funktion herausnehmen.

Dies bedeutet, dass wir zunächst die Farbe setzen und erst dann die entsprechende Zeichenfunktion verwenden werden.

Neue “SetColor” Funktion

Fangen wir erstmal mit der Farbe an. Bisher haben wir die Farbe direkt als ein 32-Bit-Wert definiert und der Funktion DrawPixel übergeben. Da wir jetzt auch 16-Bit unterstützen wollen, müssen wir hier etwas tun und die Farbe, die später gesetzt wird, erstmal berechnen. Unsere Funktion wird die drei Farbwerte für Rot, Grün und Blau in jeweils r0, r1 und r2 als 8-Bit-Wert übergeben bekommen.

SetColor:
/* void SetColor (int red (r0), int green (r1), int blue (r2))
*/
   push {lr}

Die Farbwerte für die unterschiedlichen Farbtiefen, werden auch unterschiedlich berechnet. Alleine, wenn man die Zahlenwerte für jede Farbe sich anschaut, sieht man, das in 16 Bit keine 3 Farbwerte mit je 8 Bit untergebracht werden können. Auch bei 32 Bit ergibt sich das Problem, dass die jeweiligen 8 Bits der Farben maximal 24 Bit ergeben. Allerdings ist es bei 32 Bit einfacher, als bei 16 Bit.

Zunächst überprüfen wir, welche Farbtiefe vorgegeben ist und verzweigen entsprechend.

   mov r3,#BITS_PER_PIXEL     @First get the color depth
   cmp r3,#16                 @Color depth = 16?
   beq Bitmap16               @Then create color for 16 bits
   cmp r3,#32                 @Color depth = 32?
   beq Bitmap32               @Then create color for 32 bits
   pop {pc}                   @Otherwise do nothing

In der Variablen "BITS_PER_PIXEL" haben wir unsere Farbtiefe abgelegt. Diese verwenden wir, um auf die entsprechende Routine zu verzweigen. Sollte hier nichts stimmen, so wird keine Farbe gesetzt.

Zusätzlich erzeugen wir in der .section .data die Variable "Color". In diese werden wir den Farbwert nach der berechnung ablegen.

.section .data
Color:
   .int 0

Zunächst erstellen wir die Bitmap32 Funktion, da diese die einfachere der beiden ist.

Bei 32-Bit Farbtiefe muss der 32-Bit Wert folgendem Schema entsprechen:

AlphaBlending [Byte]
Red [Byte]
Green [Byte]
Blue [Byte]

AlphaBlending ist ein Wert, der die Abdeckung der Farbe selbst definiert. Wir werden diesen nicht verwenden, da dieser Wert keine Bedeutung bei unserer Art, der Anzeige von Farben hat. Damit es eventuell zu keinen Störungen kommt, setzen wir diesen Wert immer auf volle Deckung.

Dies ist auch der erste Wert, den wir setzen.

   mov r3,#0xff      @AlphaBlending

Damit wir den nächsten Wert hineinschreiben können, verschieben wir den Wert um 8 Positionen nach links und verwenden orr, um den Rotanteil einzutragen.

   lsl r3,#8         @Shift left 8 bits
   orr r3,r0         @Write the red part into the register

Das gleiche machen wir für den Grünanteil und für den Blauanteil.

   lsl r3,#8         @Shift left 8 bits
   orr r3,r1         @Write the green part into the register
   lsl r3,#8         @Shift left 8 bits
   orr r3,r2         @Write the blue part into the register

Damit andere Funktionen die Farbe zur verfügung haben, speichern wir diesen Wert in die Variable Color. Damit ist diese Funktion fertig.

   ldr r4,=Color
   str r3,[r4]
   pop {pc}

Nun erzeugen wir eine Funktion, um einen 16-Bit-Farbwert zu erstellen.

Unter 16 Bit können wir keine 3x8 Bits ablegen, da wir hierfür mindestens 24 Bits benötigen würden. Bei 16 Bit wurde folgendes Format festgelegt:

Red [5 Bits]
Green [6 Bits]
Blue [5 Bits]

Also versuchen wir, unsere Übergabewerte entsprechend so zu ändern, dass sie hier rein passen. Wenn wir uns die Farbwerte als Binärzahl anschauen, so sieht man, dass die höherwertigen Bits mehr einfluß auf die Farbe haben, als niedrigere Bits. So können wir einfach die unteren Bits entfernen. Wir schieben einfach die Bits nach rechts und lassen sie einfach herunterfallen. Für die Farbe Rot bedeutet es, dass wir den Wert um 3-Bits schieben müssen, damit dann noch 5 Bits übrig bleiben.

Bitmap16:
   lsr r0,#3                  @shift bits right 3

Als nächstes "reinigen" wir diesen Wert, damit uns bei den nächsten Aktionen hier nichts stört. Dafür benutzen wir ein logisches AND. Der Wert von r0 wird mit der Folge “00000000000000000000000000011111” verglichen. Alle Bits, die gesetzt sind, werden ohne Änderung übernommen, Bits, die Null sind, werden gelöscht. Anschließend schieben wir die Bits um 6 nach links, um Platz für den nächsten Wert, der 6-Bit breit ist, zu machen.

   and r0,#0b00000000000000000000000000011111
   lsl r0,#6

Der zweite Wert, der Grünanteil folgt dem gleichen Prinzip. Allerdings ist dieser Wert 6-Bit groß, so dass wir nur um 2 nach rechts schieben müssen und ein Bit mehr als gut bewerten. Mit “orr” addieren wir den Wert dann nach r0. Dann machen wir wieder Platz für den nächsten Wert, der in diesem Fall dann 5-Bit breit ist.

   lsr r1,#2
   and r1,#0b00000000000000000000000000111111
   orr r0,r1
   lsl r0,#5

Der Blau-Anteil ist wie der Rot-Anteil nur 5 Bit breit, so dass wir hier das vom Rot-Anteil übernehmen können.

   lsr r2,#3
   and r2,#0b00000000000000000000000000011111
   orr r0,r2

Damit steht unser Farbwert in r0. Diesen sichern wir im Speicher ab.

   ldr r4,=Color
   str r0,[r4]
   pop {pc}

Nachdem wir nun eine Funktion für Farben erzeugt haben, passt diese Art, wie wir nun die Farben definieren nicht mehr. Unsere DrawPixel Funktion müssen wir nun dafür anpassen. Zusätzlich unterstützen wir zusätzlich den 16-Bit Modus, der die Position des Pixel etwas anders berechnet.

Bei 16-Bit, dies entspricht 2 Bytes, wird der Wert mit 2 multibliziert, bei 32-Bit, dies entspricht 4 Bytes, wird der Wert mit 4 multipliziert. Dies sieht dann wie folgt aus:

_Draw16:
   add r4,r3,lsl #1  @multibly r3 with 2 and
                     @add the result to the graphic address
   ldr r2,=Color
   ldr r2,[r2]
   strh r2,[r4]       @Store the Color to graphic address
   pop {pc}
 
_Draw32:
   add r4,r3,lsl #2  @multibly r3 with 4 and
                     @add the result to the graphic address
   ldr r2,=Color
   ldr r2,[r2]
   str r2,[r4]       @Store the Color to graphic address
   pop {pc}

Bei 16 Bit haben wir keinen 32 Bit Wert für die Farbe. Wenn wir den vollen Wert in den Speicher des Bildschirmes schreiben, würde dies den Nachbar-Pixel entsprechend verändern. Damit dies nicht passiert, verwenden wir strh. Dieser Befehl kopiert nur ein Halbword in den Speicher und lässt damit die Nachbarbereiche unberührt.


Neue Funktionen verwenden

Da wir nun die Zuordnung der Farben und auch die DrawPixel-Funktion verändert haben, müssen wir auch unser Hauptprogramm entsprechend ändern. Den gesammten Source bekommst du hier: 5.2.zip

Zunächst setzen wir die Farbe, die wir für unsere Zeichenfunktion verwenden wollen.

   mov r0,#0x5f
   mov r1,#0xd0
   mov r2,#0x2f
   bl SetColor

Mit dieser Art der Zuweisung, werden nun alle Funktionen mit der gleichen Farbe verwendet. Auch ist es in diesem Fall egal, welche Farbtiefe unser Bildschirm hat, da genau diese SetColor-Funktion dies für uns übernimmt.

Zum zeichnen eines Punktes verwenden wir weiterhin die DrawPixel Funktion. Allerdings erwartet diese hier nur noch die Koordinaten des Punktes. Umgesetzt, auf die vorhergehende Version, sieht dies nun wie folgt aus:

   mov r5,#0               
   loop:
      mov r0,r5            @only x
      mov r1,r5            @and y
      bl DrawPixel         
      add r5,#1            
      cmp r5,#10           
      bne loop

Wir übergeben nur noch "x" in r0 und "y" in r1. Die Farbe holt die Funktion aus der Variablen, die zuvor mit SetColor berechnet wurde.


< Zurück (Fehlerbehandlung in C (PI4)) < Hauptseite > Weiter (Chars in C (PI4)) >