Grafik in C (PI5)

Aus C und Assembler mit Raspberry

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:

u32 pScreen[8] ALIGN(16) = {
    8*4, //8 Einträge * 4 Bytes
    CODE_REQUEST,
    PROPTAG_ALLOCATE_BUFFER,
    8,
    4,
    0,  //m_nBufferPtr
    0,
    PROPTAG_END
};

Wir werden diese Struktur der MailBox-Funktion, die wir noch schreiben, und zusätzlich werden wir den MailBox-Kanal übergeben. Für die Kommunikation (ARM to VC) wird der Kanal 8 verwendet, den wir zuvor mit BCM_MAILBOX_PROP_OUT definiert haben.

u32 graphicsAddress = 0;
u32 DrawColor = 0xFFFFFFFF;

#define ADDRESS_MASK 0x3FFFFFFF

void Init_Screen(void) 
{

    uintptr pScreenAddress = (uintptr)pScreen;
    while (TRUE) {
        BcmMailBox_Write(BCM_MAILBOX_PROP_OUT,(u32)pScreenAddress);
        if (pScreen[5] != 0)
        {
            break;
        }
    }
    // Passt die Adresse an und speichert sie
    graphicsAddress = pScreen[5] & ADDRESS_MASK;
}

In unserer Tag-Liste erhalten wir unter PROPTAG_ALLOCATE_BUFFER eine Rückantwort des Grafikchips, an welcher Adresse der Grafikspeicher liegt. Die Position im Tag können wir mit pScreen[5] abfragen.

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 und speichern uns die Adresse in graphicsAddress ab. Dies geschieht mit graphicsAddress = pScreen[5] & ADDRESS_MASK.

Die Mailbox

Aktuell gestalten wir es noch sehr einfach und überlassen es dem aufrufenden Programm, das Ergebnis auszuwerten.

void BcmMailBox_Write(u32 nChannel, u32 nData)
{
    write32(MAILBOX1_WRITE, nChannel | nData);
}

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 jeweils als Ganzzahl ü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 (SCREEN_X). 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(u32 x, u32 y)
{
    if ((x < SCREEN_X) && (y < SCREEN_Y))
    {
        write32(graphicsAddress+(((SCREEN_X*y)+x)*4),DrawColor);
    }
}

DrawPixel-Funktion in Aktion

Nun verwenden wir unsere neue Funktion in unserem Kernel:

int main (void)
{
    LED_off();
    Init_Screen();
    u32 i;
    for (i=100;i<=500;i++)
    {
        DrawPixel(i,i);
    }
    LED_Error(2);
}

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.

Du kannst den Source-Code als ZIP-Datei mit folgenden Link downloaden: https://www.satyria.de/arm/sources/C/graphics.zip


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