Grafik (PI5): Unterschied zwischen den Versionen
| Zeile 219: | Zeile 219: | ||
*'''999: b 999b''': Eine Endlosschleife, um das Programm am Laufen zu halten, nachdem die Pixel gezeichnet wurden und der Fehlercode angezeigt wurde. | *'''999: b 999b''': Eine Endlosschleife, um das Programm am Laufen zu halten, nachdem die Pixel gezeichnet wurden und der Fehlercode angezeigt wurde. | ||
----- | |||
| style="width: | |||
{| style="width: 100%; | |||
| style="width: 33%;" | [[Fehlerbehandlung|< Zurück (Fehlerbehandlung)]] | |||
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]] | |||
| style="width: 33%; text-align:right;" | [[Chars (PI5)|Weiter (Chars (PI5)) >]] | |||
|} | |} | ||
Aktuelle Version vom 23. August 2024, 10:44 Uhr
Vorab: Auf dem Raspberry Pi 5 hat sich in Sachen Grafik einiges geändert, was es für BareMetal-Entwickler schwieriger macht, die Grafik richtig zu programmieren. Dazu gab es bereits eine Diskussion auf GitHub: Raspberry Pi Firmware Issue #1904.
Diese Entwicklung oder Entscheidung gefällt mir nicht!
Dennoch habe ich es geschafft, eine Anzeige auf dem Raspberry Pi 5 zu implementieren, die funktional ist. Im Gegensatz zu den Vorgängermodellen wird der Bildschirm in der config.txt Datei festgelegt und ist bereits vorhanden, wenn der erste Bootvorgang durchgeführt wird.
Wir verwenden den Eintrag framebuffer_depth=32 in der config.txt Datei. Dieser Eintrag erzeugt einen Framebuffer mit einer Tiefe von 32-Bit und einer Auflösung von 1920x1080 Pixeln.
Im Raspberry Pi sind mehrere Prozessoren für verschiedene Aufgaben zuständig. So gibt es auch einen Prozessor (GPU), der die Grafik verwaltet. Interessant ist hierbei, dass genau dieser Prozessor auch für den ersten Start des Raspberry Pi zuständig ist. Nach der ersten Initialisierung übergibt er die Kontrolle an den ARM-Prozessor.
Da jeder Prozessor für sich arbeitet, müssen wir einen Weg finden, damit diese Prozessoren miteinander kommunizieren können. Dazu wurde eine Art Postfach eingerichtet, in das wir per "Brief" mitteilen, was wir vom Grafikprozessor möchten. Bei einer Antwort verwendet der Grafikprozessor genau dieses Postfach wieder. Natürlich, wie in der Informatik üblich, ist der Aufbau des "Briefes" definiert. An diese Richtlinien müssen wir uns halten.
Aufbau einer Mailbox-Nachricht
Eine TAG-Struktur beginnt immer mit einer 32-Bit Zahl, die die Länge der gesamten Struktur angibt. Danach folgt ein 32-Bit Puffer, um eventuell eine Antwort abzulegen, die bei einem Aufruf übermittelt wird. Anschließend kommen die einzelnen TAGs. Jeder TAG hat wieder eine Grundstruktur: zuerst wird der Name des TAGs übermittelt, gefolgt von der Anzahl der Werte in Bytes (32 Bit, entsprechend 4) und wieder ein Puffer für die Antwort. Danach folgen die Werte für den jeweiligen TAG. Die gesamte Struktur endet mit einem NULL-TAG.
- Mailbox Länge
- Mailbox AnfrageCode
- TAG1 Name
- TAG1 Länge
- TAG1 Puffer
- TAG1 Wert1
- TAG1 Wert2
- ...
- TAG2 Name
- ...
- TAGx NULL (End TAG)
Weitere Infos kannst du auf GitHub nachlesen. Wichtig ist, dass jeder einzelne TAG auf 32-Bit ausgerichtet sein muss.
Im Gegensatz zu den Vorgängermodellen ist es beim Raspberry Pi 5 einfacher, die Speicheradresse der Grafik zu erhalten. Leider ist es jedoch nicht mehr möglich, eine gewünschte Anzeige zu konfigurieren, wie zum Beispiel die Bildschirmauflösung. Wir müssen das nutzen, was möglich ist und die Adresse des Grafikspeichers mit PROPTAG_ALLOCATE_BUFFER anfordern.
Unsere Struktur für den Mailbox-Aufruf sieht nun wie folgt aus:
//**********************************************************
// Data
//**********************************************************
.section .data
.align 16
pScreen:
.int pScreen_end - pScreen
.int CODE_REQUEST
.int PROPTAG_ALLOCATE_BUFFER
.int 8
.int 4
m_nBufferPtr:
.int 0
m_nBufferSize:
.int 0
.int PROPTAG_END
pScreen_end:
Wir werden der MailBox-Funktion, die wir noch schreiben, die Adresse dieser Struktur in x1 übermitteln und in w0 werden wir den MailBox-Kanal schreiben. Für die Kommunikation (ARM to VC) wird der Kanal 8 verwendet, den wir zuvor mit BCM_MAILBOX_PROP_OUT definiert haben.
// boolean Init_Screen (void)
.globl Init_Screen
Init_Screen:
stp x29, x30, [sp, -16]!
mov x29, sp
1:
ldr w0,=BCM_MAILBOX_PROP_OUT // Kanal
ldr x1,=pScreen // ScreenStruktur
bl BcmMailBox_Write // Rufe die Mailbox auf
ldr x0,=m_nBufferPtr // Lade die Speicheradresse
ldr w0,[x0]
cmp w0,#0
beq 1b
and w0,w0,#0x3FFFFFFF // Passe die Adresse an
ldr x1,=graphicsAddress // Und sichere sie
str w0,[x1]
ldp x29, x30, [sp], 16
ret
In unserer Tag-Liste erhalten wir unter PROPTAG_ALLOCATE_BUFFER eine Rückantwort des Grafikchips, an welcher Adresse der Grafikspeicher liegt. Diese Position im Tag haben wir dem Label m_nBufferPtr gegeben, sodass wir direkt darauf zugreifen können.
Wenn die Speicheradresse weiterhin einen Null-Wert hat, wissen wir, dass die Kommunikation entweder noch nicht erfolgt ist oder es einen Fehler gab. Wir rufen die Mailbox so lange auf, bis wir eine Antwort haben.
Die Adresse, die wir zurückbekommen, ist die Adresse, wie sie der Grafikchip sieht. Da wir jedoch auf dem ARM-Prozessor arbeiten, müssen wir die Adresse an unseren Adressraum anpassen. Dies geschieht mit and w0,w0,#0x3FFFFFFF. Danach legen wir diese Adresse unter graphicsAddress ab, damit sie nicht verloren geht.
Die Mailbox
Aktuell gestalten wir es noch sehr einfach und überlassen es dem aufrufenden Programm, das Ergebnis auszuwerten.
.globl BcmMailBox_Write
// Eingaben: x0 = nChannel, x1 = nData
BcmMailBox_Write:
stp x29, x30, [sp, -16]!
mov x29, sp
ldr x2, =MAILBOX1_WRITE
orr w1, w1, w0
str w1, [x2]
ldp x29, x30, [sp], 16
ret
Wir speichern die Adresse der Funktion MAILBOX1_WRITE in x2, verknüpfen die übergebenen Werte w0 und x1 mit dem orr-Befehl und schreiben das Ergebnis an die Adresse.
Hinweis
Dies ist eine sehr einfache Umsetzung der Mailbox-Funktion. Später werden wir diese erweitern, da wir zunächst prüfen sollten, ob die Mailbox bereit ist, um Daten zu verarbeiten. Dies ist nicht immer möglich, und wir müssten warten, bis die Daten in der Mailbox verarbeitet sind.
DrawPixel-Funktion
Nun haben wir die Grafikadresse, aber bisher können wir noch nichts anzeigen. Dazu erstellen wir eine DrawPixel-Funktion.
Der Funktion werden die Koordinaten in w0 (x) und w1 (y) übergeben. Zunächst prüfen wir, ob der Pixel überhaupt auf den Bildschirm passt. Wenn nicht, überspringen wir diese Funktion.
Wenn der Pixel hineinpasst, müssen wir die Position im Speicher berechnen: In jeder Zeile passen 1920 Bildpunkte. Wir multiplizieren y mit der Anzahl der Bildpunkte pro Zeile und addieren x dazu. Da wir pro Bildpunkt 32 Bit (4 Bytes) haben, multiplizieren wir das Ergebnis mit 4. Damit hätten wir den Speicherbereich, den unser Bildschirmpunkt hätte, wenn die Grafikadresse null wäre. Wir addieren einfach die Grafikadresse dazu.
Nun haben wir die Position berechnet, an der wir unseren Punkt speichern können. Da unser System eine Tiefe von 32 Bit hat, was die Farbe darstellt, müssen wir hier einfach unseren Farbwert speichern.
Die Farbe
Die Farbe, die wir hier verwenden, wird als ARGB bezeichnet. Hier steht jeder Buchstabe für ein bestimmtes Verhalten:
- A: Farbabdeckung
- R: Rotanteil
- G: Grünanteil
- B: Blauanteil
Jeder Wert kann einen Bereich von 0 bis 255 annehmen. Je höher der Wert, desto intensiver ist der Farbanteil oder die Deckung. Der Einfachheit halber werden wir immer eine Farbabdeckung von 100 % wählen und das Hexadezimalsystem verwenden:
Beispiele für Farben:
- 0xffff0000: Rot
- 0xff00ff00: Grün
- 0xffffff00: Gelb
Bevor wir die DrawPixel-Funktion verwenden, legen wir den Farbwert als Variable DrawColor im Speicher ab. Diesen Wert lesen wir dort heraus und kopieren ihn an die zuvor berechnete Position.
Hier ist der Code für die DrawPixel-Funktion:
// void DrawPixel (x=w0, y=w1)
.globl DrawPixel
DrawPixel:
stp x29, x30, [sp, -16]!
stp x3, x4, [sp, -16]!
mov x29, sp
cmp w0, #SCREEN_X // Überprüfe, ob der Pixel in den Screen passt
bge 1f
cmp w1, #SCREEN_Y
bge 1f
mov w3, SCREEN_X
mul w3, w3, w1
add w3, w3, w0
lsl w3, w3, #2
ldr x4, =graphicsAddress
ldr w4, [x4]
add w3, w3, w4
ldr x2, =DrawColor
ldr w2, [x2]
str w2, [x3]
1:
ldp x3, x4, [sp], 16
ldp x29, x30, [sp], 16
ret
.section .data
DrawColor:
.int 0xffffffff // für Weiß
DrawPixel-Funktion in Aktion
Nun verwenden wir unsere neue Funktion in unserem Kernel:
//
// kernel.S
//
.section .text
.globl main
main:
bl LED_off
bl Init_Screen
mov w10, 100
2:
cmp w10, #500
bge 1f
mov w0, w10
mov w1, w10
bl DrawPixel
add w10, w10, #1
b 2b
1:
mov w0, #2
bl LED_Error // Um das Ende anzuzeigen.
999:
b 999b
Wir erzeugen eine Schleife, die von 100 bis 500 zählt. Diesen Wert übergeben wir der DrawPixel-Funktion als x und y. Damit erzeugen wir eine Linie, die schräg von links oben nach rechts unten verläuft. Wenn dies erfolgreich durchgeführt wurde, gehen wir absichtlich in den Fehlercode 2, um zu sehen, dass der Raspberry alles abgearbeitet hat.
Kernel aufgeschlüsselt
- bl LED_off: Schaltet die LED aus, um sicherzustellen, dass wir einen definierten Ausgangszustand haben.
- bl Init_Screen: Initialisiert den Bildschirm, um sicherzustellen, dass wir die Grafikadresse korrekt erhalten haben.
Schleife zum Zeichnen von Pixeln:
- mov w10, 100: Setzt den Startwert für die Schleife.
- cmp w10, #500: Vergleicht den aktuellen Wert von w10 mit 500.
- bge 1f: Springt zu Label 1, wenn w10 größer oder gleich 500 ist, um die Schleife zu beenden.
- mov w0, w10 und mov w1, w10: Setzt die x- und y-Koordinaten für den Pixel, der gezeichnet werden soll.
- bl DrawPixel: Ruft die DrawPixel-Funktion auf, um den Pixel zu zeichnen.
- add w10, w10, #1: Erhöht w10 um 1, um den nächsten Pixel zu zeichnen.
- b 2b: Springt zurück zum Anfang der Schleife, um den nächsten Pixel zu zeichnen.
Fehlercode-Anzeige:
- mov w0, #2 und bl LED_Error: Setzt den Fehlercode auf 2 und ruft die LED_Error-Funktion auf, um das Ende der Ausführung anzuzeigen.
Endlosschleife:
- 999: b 999b: Eine Endlosschleife, um das Programm am Laufen zu halten, nachdem die Pixel gezeichnet wurden und der Fehlercode angezeigt wurde.
| < Zurück (Fehlerbehandlung) | < Hauptseite > | Weiter (Chars (PI5)) > |