Grafik in C (PI4): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Die Seite wurde neu angelegt: „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…“
 
KKeine Bearbeitungszusammenfassung
 
(10 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
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.
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.
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.
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 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.
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.


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


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


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. Wir beschränken uns damit nur auf 16-Bit und 32-Bit Darstellungen.  
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.


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


Die Berechnung für mehr Bitmaps erfolgt mit folgender Formel und als Beispiel ein Bild mit 4 Bitplanes:
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]]


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


<syntaxhighlight>
<syntaxhighlight>
((1920 * 1080) * 32) / 8 = 8294400 Bytes ->  ~8,3 MegaBytes
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 ==


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


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 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 und TAGs ==
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:


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.
https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface


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:
Diese Informationen habe ich in dem zugehörigen Sourcecode zusammengefasst und in der base.h Datei eingefügt.


https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface
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 112: 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 übergegeben werden. Dazu verwenden wir “.align 16”.
== 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="asm">
<syntaxhighlight lang="c">
.section .data
u32 pScreen[36] ALIGN(16) = {
.align 16
...
}
</syntaxhighlight>
</syntaxhighlight>


Zunächst geben wir unseren TAG-Struktur einen Namen. Wir nennen sie "FB_STRUCT". Als erster Eintrag wir zunächst die Länge der gesammten 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="asm">
<syntaxhighlight lang="c">
FB_STRUCT:
u32 pScreen[36] ALIGN(16) = {
  .int FB_STRUCT_END - FB_STRUCT @Length of the structure
    34*4, //34 Einträge * 4 Bytes
  .int 0x00000000                @Buffer request / response code
    0,
</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 140: Zeile 148:
</syntaxhighlight>
</syntaxhighlight>


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


<syntaxhighlight lang="asm">
<syntaxhighlight lang="c">
  .int Set_physical_display @Tag Identifier
    Set_physical_display, //Tag Identifier
  .int 0x00000008          @Buffer size in bytes
    0x8,0x8,              //Buffer size in bytes
  .int 0x00000008          @Response buffer
    SCREEN_X,             //Width in pixels
  .int SCREEN_X            @Width in pixels
    SCREEN_Y,             //Height in pixels
  .int SCREEN_Y            @Height in pixels
</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 wir im Hauptprogramm 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 162: Zeile 169:
</syntaxhighlight>
</syntaxhighlight>


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


<syntaxhighlight lang="asm">
<syntaxhighlight lang="c">
  .int Set_virtual_buffer   @Tag Identifier
    Set_virtual_buffer,
  .int 0x00000008          @Buffer size in bytes
    0x8,0x8,
  .int 0x00000008          @Response buffer
    SCREEN_X,
  .int SCREEN_X             @Width in pixels
    SCREEN_Y,
  .int SCREEN_Y             @Height in pixels
</syntaxhighlight>
</syntaxhighlight>


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


<syntaxhighlight>
<syntaxhighlight>
Zeile 180: Zeile 186:
Tiefe der Bitplanes
Tiefe der Bitplanes
</syntaxhighlight>
</syntaxhighlight>
In unsere Struktur übertragen sieht es so aus:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="asm">
   .int Set_depth            @Tag Identifier
   Set_depth,           //Tag Identifier
   .int 0x00000004          @Buffer size in bytes
   0x4,                  //Buffer size in bytes
   .int 0x00000004          @Response buffer
   0x4,                  //Response buffer
   .int BITS_PER_PIXEL      @depth
   BITS_PER_PIXEL,       //depth
</syntaxhighlight>
</syntaxhighlight>


Auch hier verwenden wir eine Variable (BITS_PER_PIXEL), die wir auch im Hauptprogramm später 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 200: 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="asm">
<syntaxhighlight lang="c">
  .int Set_virtual_offset   @Tag Identifier
    Set_virtual_offset,
  .int 0x00000008          @Buffer size in bytes
    0x8,0x8,
  .int 0x00000008          @Response buffer
    0x0,0x0,
  .int 0                    @Offset x
  .int 0                    @Offset y
</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 221: 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="asm">
<syntaxhighlight lang="c">
  .int Set_palette           @Tag Identifier
    Set_palette,
  .int 0x00000010            @Buffer size in bytes
    0x10,0x10,
  .int 0x00000010            @Response buffer
    0x0,
  .int 0                    @first set pallet index
    0x2,
  .int 2                    @Number of pallet entries to be set
    0x00000000,
  .int 0x00000000,0xFFFFFFFF @RGBA pallet values
    0xffffffff,
</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 250:
</syntaxhighlight>
</syntaxhighlight>


<syntaxhighlight lang="asm">
In unsere Struktur übertragen sieht es so aus:
  .int Allocate_buffer       @Tag Identifier
 
  .int 0x00000008            @Buffer size in bytes
<syntaxhighlight lang="c">
  .int 0x00000008            @Response buffer
    Allocate_buffer,
FB_POINTER:
    0x8,0x8,
  .int 0                    @Frame buffer base address in bytes
    0x0,          //Bufferadresse (pScreen[31])
  .int 0                    @Image buffer size in bytes
    0x0,
</syntaxhighlight>
</syntaxhighlight>


Der TAG erzeugt einen Puffer, der die Adresse des Speichers für den Screen zurück gibt. Dieser Wert, wird im Label "FB_POINTER" abgelegt, sobalt die GPU dies erfolgreicht 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="asm">
<syntaxhighlight lang="c">
   .int 0x00000000   @0x0 (End Tag)
   0x00000000
FB_STRUCT_END:
</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 jedesmal ü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.
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.


<syntaxhighlight lang="asm">
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.
FB_Init:
  push {lr}
  ldr r0,=MAIL_TAG_WRITE    @Function Mail-Tag-Write
  ldr r1,=FB_STRUCT + 0x8    @Submit framebuffer structure
  str r1,[r0]               
</syntaxhighlight>


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


Um nun sicher zu sein, dass es geklappt hat, fragen wir einfach nach, ob es eine Adresse gibt, wo den unser Bildschirm abgelegt wurde. Dazu habe ich in der TAG-Liste das Label "FB_POINTER" erzeugt. 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.
<syntaxhighlight lang="c">
 
uintptr pScreenAddress = (uintptr)pScreen;
<syntaxhighlight lang="asm">
while (TRUE) {
  ldr r0,=FB_POINTER        @Get the result from the pointers
    BcmMailBox_Write(BCM_MAILBOX_PROP_OUT, (u32)pScreenAddress);
  ldr r0,[r0]        
   
  teq r0,#0                  @And check if we have anything
    if (pScreen[31] != 0) {
  beq FB_Init                @If not, just do it again
        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.
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="asm">
<syntaxhighlight lang="c">
  and r0,#0x3FFFFFFF
#define ADDRESS_MASK 0x3FFFFFFF
  ldr r1,=graphicsAddress
graphicsAddress = pScreen[31] & ADDRESS_MASK;
  str r0,[r1]
  pop {pc}
</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.
 
Diese Funktion nennen wir “DrawPixel” und übergeben ihr die Koordinate in x (r0), y (r1) und die Farbe (r2).
 
<syntaxhighlight lang="asm">
DrawPixel:
/* void DrawPixel (int x (r0), int y (r1), int color (r2))
*/
  push {lr}
</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.
<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:
 
<syntaxhighlight>
((Gesammtscreenbreite * Position y) + Position x) * Screentiefe in Byte
</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.
 
<syntaxhighlight lang="asm">
  ldr r4,=graphicsAddress  @Load Graphicaddress to r4
  ldr r4,[r4]
  add r4,r3                @add pos of Pixel
  str r2,[r4]              @Store Color to Graphic
</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.
 
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.
 
<syntaxhighlight lang="asm">
  mov r3,#SCREEN_X    @width to r3
  mla r3,r1,r3,r0      @multibly with pos y and add pos x
</syntaxhighlight>
 
Immerhin haben wir hier zumindest mal die addition gespart.
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">
  ldr r4,=graphicsAddress    @Load Graphicaddress to r4
  ldr r4,[r4]
 
  add r4,r3,lsl #2  @multibly r3 with 4 and
                    @Add the result to the graphic address
  str r2,[r4]      @Store the Color to graphic address
</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) ==
 
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.
 
<syntaxhighlight lang="asm">
.equ SCREEN_X,      1920
.equ SCREEN_Y,      1080
.equ BITS_PER_PIXEL, 32
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
  bl FB_Init    @Open Screen
</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.
 
<syntaxhighlight lang="asm">
  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
</syntaxhighlight>
 
Der Sourcecode, für dieses Beispiel ist hier zum laden bereit: [http://assem.satyria.org/source/kurs/5.1.zip 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.
Wir entwickeln eine Funktion, die einen einzelnen Bildpunkt (Pixel) anzeigt. Diese grundlegende Funktion bildet die Basis für weitere grafische Funktionen.


=== mul ===
Wir nennen die Funktion DrawPixel und übergeben ihr die Koordinaten x und y.


MUL multiplizier zwei Register.
Für verschiedene Farbtiefen sind unterschiedliche Berechnungen notwendig. Zunächst halten wir es einfach und gehen von einer Farbtiefe von 32 Bit aus.


=== mla ===
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:
 
MLA multipliziert zwei Register und addiert das dritte Register.
 
Beispiel:
 
<syntaxhighlight lang="asm">
mla r0,r1,r4,r0
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
SetColor:
/* void SetColor (int red (r0), int green (r1), int blue (r2))
*/
  push {lr}
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
  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
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
.section .data
Color:
  .int 0
</syntaxhighlight>
 
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:


<syntaxhighlight>
<syntaxhighlight>
AlphaBlending [Byte]
((Gesamtbreite des Screens * Position y) + Position x) * Farbtiefe in Bytes
Red [Byte]
Green [Byte]
Blue [Byte]
</syntaxhighlight>
</syntaxhighlight>


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.
Die Screen-Adresse wird zu dieser Position hinzuaddiert:


Dies ist auch der erste Wert, den wir setzen.
<syntaxhighlight lang="c">
 
void DrawPixel(u32 x, u32 y) {
<syntaxhighlight lang="asm">
    if ((x < SCREEN_X) && (y < SCREEN_Y)) {
  mov r3,#0xff      @AlphaBlending
        write32(DrawColor, graphicsAddress + (((SCREEN_X * y) + x) * 4));
    }
}
</syntaxhighlight>
</syntaxhighlight>


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.
Damit wird bereits der Pixel angezeigt.  


<syntaxhighlight lang="asm">
== Header-Datei für DrawPixel ==
  lsl r3,#8        @Shift left 8 bits
  orr r3,r0        @Write the red part into the register
</syntaxhighlight>


Das gleiche machen wir für den Grünanteil und für den Blauanteil.
Wir erstellen eine Header-Datei, in der wir unsere Funktionen beschreiben:
<syntaxhighlight lang="c">
//
// 20.02.2025 www.satyria.de
//
// screen.h
//


<syntaxhighlight lang="asm">
#ifndef _screen_h
  lsl r3,#8        @Shift left 8 bits
#define _screen_h
  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
</syntaxhighlight>
 
Damit andere Funktionen die Farbe zur verfügung haben, speichern wir diesen Wert in die Variable Color. Damit ist diese Funktion fertig.
 
<syntaxhighlight lang="asm">
  ldr r4,=Color
  str r3,[r4]
  pop {pc}
</syntaxhighlight>
 
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:
 
<syntaxhighlight>
Red [5 Bits]
Green [6 Bits]
Blue [5 Bits]
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
Bitmap16:
  lsr r0,#3                  @shift bits right 3
</syntaxhighlight>
 
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.
 
<syntaxhighlight lang="asm">
  and r0,#0b00000000000000000000000000011111
  lsl r0,#6
</syntaxhighlight>


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.
#include "types.h"
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.


<syntaxhighlight lang="asm">
void Init_Screen(void);
  lsr r1,#2
void DrawPixel(u32 x, u32 y);
  and r1,#0b00000000000000000000000000111111
  orr r0,r1
  lsl r0,#5
</syntaxhighlight>


Der Blau-Anteil ist wie der Rot-Anteil nur 5 Bit breit, so dass wir hier das vom Rot-Anteil übernehmen können.
#endif
 
<syntaxhighlight lang="asm">
  lsr r2,#3
  and r2,#0b00000000000000000000000000011111
  orr r0,r2
</syntaxhighlight>
</syntaxhighlight>


Damit steht unser Farbwert in r0. Diesen sichern wir im Speicher ab.
== Anwendung der DrawPixel-Funktion ==


<syntaxhighlight lang="asm">
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.
  ldr r4,=Color
  str r0,[r4]
  pop {pc}
</syntaxhighlight>
 
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:
Zuerst definieren wir die Variablen, die wir beim Erstellen der TAG-Struktur verwendet haben, in der config.h Datei:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
_Draw16:
#define SCREEN_X       1920
  add r4,r3,lsl #1  @multibly r3 with 2 and
#define SCREEN_Y       1080
                    @add the result to the graphic address
#define BITS_PER_PIXEL 32
  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}
</syntaxhighlight>
</syntaxhighlight>
Diese Werte definieren die Breite, Höhe und Tiefe des Bildschirms.


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.
Die erste Funktion, die wir aufrufen, ist die Bildschirminitialisierung:
 
<syntaxhighlight lang="C">
<!--== Neue Befehle ==
   Init_Screen();
 
=== lsr ===
=== strh ===
-->
 
== 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: [http://assem.satyria.org/source/kurs/5.2.zip 5.2.zip]
 
Zunächst setzen wir die Farbe, die wir für unsere Zeichenfunktion verwenden wollen.
 
<syntaxhighlight lang="asm">
   mov r0,#0x5f
  mov r1,#0xd0
  mov r2,#0x2f
  bl SetColor
</syntaxhighlight>
</syntaxhighlight>


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


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
  mov r5,#0             
    u32 i;
  loop:
    for (i=100;i<=500;i++)
      mov r0,r5            @only x
    {
      mov r1,r5            @and y
        DrawPixel(i,i);
      bl DrawPixel        
    }
      add r5,#1           
      cmp r5,#10         
      bne loop           
</syntaxhighlight>
</syntaxhighlight>


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