Lass die LED leuchten in C (PI5): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Die Seite wurde neu angelegt: „Unser jetziges Ziel ist es, die fest eingebaute LED des Raspberry Pi zum Blinken zu bringen. Ich werde jeden Schritt erklären, warum er in diesem Projekt so implementiert wurde. === Vorbereitung des Verzeichnisses === Erstelle zunächst ein neues Verzeichnis, z.B. LED, und platziere darin das Makefile und die Datei linker.ld. Erzeuge auch das Verzeichnis include innerhalb von LED, um unsere Header-Dateien zu organisieren. === Programmierung des Startv…“
 
 
(10 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 7: Zeile 7:
=== Programmierung des Startverhaltens des Raspberry Pi ===
=== Programmierung des Startverhaltens des Raspberry Pi ===


Leider kommen wir nicht ganz an Assembler vorbei. Der erste Code, der Ausgeführt wird, sollte in Assembler geschrieben sein. In der REgel werden dort einige Eigenschaften der CPU definiert, die "nur" in Assembler möglich ist. Bei diesem Beispiel ist das noch nicht nötig, aber wir erstellen diese Datei, damit wir bei späteren Projekten schon vorbereitet sind.
Leider kommen wir nicht ganz an Assembler vorbei. Der erste Code, der Ausgeführt wird, sollte in Assembler geschrieben sein. In der Regel werden dort einige Eigenschaften der CPU definiert, die "nur" in Assembler möglich ist. Bei diesem Beispiel ist das noch nicht nötig, aber wir erstellen diese Datei, damit wir bei späteren Projekten schon vorbereitet sind.
Hierzu erstellen wir die Datei "boot.S":
Hierzu erstellen wir die Datei "boot.S":


Zeile 49: Zeile 49:
Unsere "'''config.h'''" ist nun wie folgt beschrieben:
Unsere "'''config.h'''" ist nun wie folgt beschrieben:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
//
//
// config.h
// config.h
Zeile 70: Zeile 70:
'''Erklärung:'''
'''Erklärung:'''
* '''#ifndef _config_h ... #endif''': Verhindert, dass die Datei mehrfach inkludiert wird.
* '''#ifndef _config_h ... #endif''': Verhindert, dass die Datei mehrfach inkludiert wird.
* '''.equ''': Weist Symbolen konstante Werte zu.
* '''#define''': Weist Symbolen konstante Werte zu.
* '''MEM_KERNEL_STACK''': Berechnet den Start des Kernel-Stacks basierend auf der Startadresse des Kernels und seiner maximalen Größe.
* '''MEM_KERNEL_STACK''': Berechnet den Start des Kernel-Stacks basierend auf der Startadresse des Kernels und seiner maximalen Größe.


Zeile 102: Zeile 102:
Erstellen wir nun das Hauptprogramm:
Erstellen wir nun das Hauptprogramm:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
//
//
// kernel.S
// kernel.c
//
//


.section .text
#include "led.h"
.globl main
#include "time.h"
main:
 
     bl LED_off
int main (void)
     mov w0, #0x3F0000
{
    bl wait
     while(1)
    bl LED_on
     {
    mov w0, #0x3F0000
      LED_off();
     bl wait
      wait(0x3F0000);
    b main
      LED_on();
      wait(0x3F0000);
     }
}
</syntaxhighlight>
</syntaxhighlight>
'''Erklärung:'''
'''Erklärung:'''
* '''.section .text''': Definiert den ausführbaren Codeabschnitt.
* '''while(1)''': Erzeugt eine Endlosschleife, da 1 immer wahr ist.
* '''.globl main''': Deklariert main als global.
* '''LED_off()''': Schaltet die LED aus.
* '''bl LED_off''': Schaltet die LED aus.
* '''wait(0x3F0000)''': Ruft die Wartefunktion auf.
* '''mov w0, #0x3F0000''': Setzt den Wert für die Wartezeit.
* '''LED_on()''': Schaltet die LED ein.
* '''bl wait''': Ruft die Wartefunktion auf.
* '''bl LED_on''': Schaltet die LED ein.
* '''b main''': Wiederholt die Schleife.
 
Auch diesen Programmteil legen wir in die .text-Sektion. Wir machen das Programm global bekannt und definieren das Label "'''main'''". Damit hat die '''sysinit'''-Funktion die Einsprungadresse.


Der erste Befehl „bl“ (branch with link) bedeutet, dass an die Funktion '''LED_off''' gesprungen wird und dabei die Rücksprungadresse in das Register '''x30''' gelegt wird. Die Funktion '''LED_off''', die wir später schreiben werden, schaltet die fest eingebaute grüne LED des Raspberry Pi aus. Wenn die Funktion '''LED_off''' korrekt funktioniert, wird nach ihrer Ausführung der nächste Befehl ausgeführt.
Mit int main (void) erstellen wir das Label, welches von sysinit aufgerufen wird.


Als nächstes wird die Funktion '''wait''' aufgerufen. Diese Funktion erwartet einen Wert im Register '''w0''', der angibt, wie lange gewartet werden soll, bevor der nächste Befehl ausgeführt wird.
Zunächst definieren wir eine Endlosschleife mit while. Alles was nach der geschweiften Klammer steht wird endlos wiederholt, da 1 immer wahr ist.


=== Parameterübergabe an Funktionen ===
Der erste Befehl '''LED_off()''' Ruft die entsprechende Funktion auf, die wir später noch schreiben werden. Diese Funktion soll dann die fest eingebaute grüne LED am Raspberry Pi 5 ausschalten.
Viele Funktionen benötigen Parameter, die innerhalb der Funktion verwendet werden. Damit dies einheitlich und ohne Verwirrung geschieht, wurde ein Standard festgelegt. In der Regel werden die Daten in den Anfangsregistern abgelegt. Zum Beispiel wird der erste Parameter in '''w0/x0''' übergeben, der zweite in '''w1/x1''' und so weiter. Da die Anzahl der Register begrenzt ist, sollte man sparsam damit umgehen. In der Regel sollten maximal vier Parameter über Register an Funktionen übergeben werden. Wenn mehr Parameter benötigt werden, werden die Daten in der Regel über den Stack oder als Struktur (dann als Zeiger) übergeben.


Wir werden uns jedoch nicht strikt an diese Regel halten und die Register '''w0/x0''' bis '''w7/x7''' als Parameter verwenden.
Als nächstes wird die Funktion '''wait''' aufgerufen. Diese Funktion erwartet einen Wert, der angibt, wie lange gewartet werden soll, bevor der nächste Befehl ausgeführt wird.


=== Zurück zu unserem Programm ===
Nachdem '''wait''' aufgerufen wurde, wird die Funktion '''LED_on()''' aufgerufen, die die grüne LED wieder einschaltet. Danach wird erneut gewartet und das ganze Programm wird unendlich wiederholt.
Nachdem '''wait''' aufgerufen wurde, wird die Funktion '''LED_on''' aufgerufen, die die grüne LED wieder einschaltet. Danach wird erneut gewartet und mit '''b main''' das ganze Programm unendlich wiederholt.


=== LED-Steuerungsfunktionen ===
=== LED-Steuerungsfunktionen ===
Erstellen wir die Datei led.S für die LED-Steuerung:
Erstellen wir die Datei led.c für die LED-Steuerung:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
//
//
// led.S
// led.c
//
//


#include "base.h"
#include "base.h"
#include "util.h"
#include "types.h"


// void LED_off(void) // Schaltet die LED aus
void LED_off(void)
.section .text
{
.globl LED_off
     u32 reg = read32(ARM_GPIO2_DATA0);
LED_off:
     reg &= ~0x200; // Bit 9 auf 0 setzen
     ldr x1, =ARM_GPIO2_DATA0
     write32 (ARM_GPIO2_DATA0,reg);
     ldr w0, [x1]
}
    bic w0, w0, #0x200
     str w0, [x1]
    ret


// void LED_on(void) // Schaltet die LED ein
void LED_on(void)
.section .text
{
.globl LED_on
     u32 reg = read32 (ARM_GPIO2_DATA0);
LED_on:
     reg |= 0x200; // Bit 9 auf 1 setzen
     ldr x1, =ARM_GPIO2_DATA0
     write32 (ARM_GPIO2_DATA0,reg);
     ldr w0, [x1]
}
    orr w0, w0, #0x200
     str w0, [x1]
    ret
</syntaxhighlight>
</syntaxhighlight>


'''Erklärung:'''
'''Erklärung:'''
* '''ldr x1, =ARM_GPIO2_DATA0''': Lädt die Adresse der LED.
* '''u32 reg = read32(ARM_GPIO2_DATA0)''': Der Inhalt des Registers ARM_GPIO2_DATA0 wird gelesen und in reg abgelegt.
* '''bic w0, w0, #0x200''': Löscht das Bit, um die LED auszuschalten.
* '''reg &= ~0x200''': Löscht das 9 Bit, um die LED auszuschalten.
* '''orr w0, w0, #0x200''': Setzt das Bit, um die LED einzuschalten.
* '''write32 (ARM_GPIO2_DATA0,reg)''': schreibt den Inhalt zurück in das Register
* '''reg |= 0x200''': Setzt das 9 Bit, um die LED einzuschalten.


Der erste Befehl in diesem Sourcecode ist das '''#include "base.h"'''. In der '''base.h'''-Headerdatei sind einige Parameter abgelegt, wie zum Beispiel die Adresse der LED, die wir ansteuern wollen.
Der erste Befehl in diesem Sourcecode ist das '''#include "base.h"'''. In der '''base.h'''-Headerdatei sind einige Parameter abgelegt, wie zum Beispiel die Adresse der LED, die wir ansteuern wollen.


Folgendes wird in diesem Code durchgeführt:
Im Raspberry Pi können viele Geräte (Peripherie) über bestimmte Adressen erreicht und dort direkt manipuliert werden. Die grüne LED des Raspberry Pi 5 ist über das GPIO2-Register erreichbar, genauer gesagt über das Register '''ARM_GPIO2_DATA0'''. Dort belegt die LED das Bit 9 des Registers. Da die anderen Bits in diesem Register andere Funktionen haben, müssen wir zunächst den gesamten Inhalt des Registers laden.
 
Der Befehl '''reg &= ~0x200''' führt eine bitweise Operation durch, um ein bestimmtes Bit in der Variable reg zu löschen (auf 0 zu setzen). Hier ist die genaue Funktionsweise:
 
* Hexadezimale Zahl: 0x200 ist die hexadezimale Darstellung der Zahl 512. In Binärdarstellung ist das 0000 0010 0000 0000.
* Bitweise NOT-Operation: Der Operator ~ führt eine bitweise Negation durch, was bedeutet, dass alle Bits invertiert werden. Für 0x200 (binär 0000 0010 0000 0000) ergibt das:
** ~0x200 = 1111 1101 1111 1111
* Bitweises AND: Der Operator &= kombiniert die bestehende Variable reg mit dem Ergebnis der bitweisen Negation von 0x200 unter Verwendung des bitweisen AND-Operators (&). Dies bedeutet, dass nur die Bits in reg erhalten bleiben, die auch im invertierten Wert von 0x200 gesetzt sind.


# '''ldr x1, =ARM_GPIO2_DATA0''': Lädt die Adresse von ARM_GPIO2_DATA0 in das Register x1.
Der Befehl reg &= ~0x200 setzt also das neunte Bit (wenn man von null zählt) von reg auf 0 und belässt alle anderen Bits unverändert.
# '''ldr w0, [x1]''': Lädt den aktuellen Wert von ARM_GPIO2_DATA0 in das Register w0.
# '''bic w0, w0, #0x200''': Setzt das Bit, das die LED steuert, auf 0 (schaltet die LED aus).
# '''str w0, [x1]''': Speichert den neuen Wert zurück in ARM_GPIO2_DATA0.
# '''ret''': Beendet die Funktion und kehrt zum Aufrufer zurück.


Im Raspberry Pi können viele Geräte (Peripherie) über bestimmte Adressen erreicht und dort direkt manipuliert werden. Die grüne LED des Raspberry Pi 5 ist über das GPIO2-Register erreichbar, genauer gesagt über das Register '''ARM_GPIO2_DATA0'''. Dort belegt die LED das Bit 10 des Registers. Da die anderen Bits in diesem Register andere Funktionen haben, müssen wir zunächst den gesamten Inhalt des Registers laden. Mit '''bic''' können bestimmte Bits (hier Bit 10) gelöscht werden, während die anderen Bits unverändert bleiben. Danach wird der Wert in das Register zurückgeschrieben und die LED ist aus.
Danach wird der Wert in das Register zurückgeschrieben und die LED ist aus.


In der LED_on-Funktion wird das gleiche durchgeführt, allerdings manipulieren wir den Wert in die andere Richtung:
In der LED_on-Funktion wird das gleiche durchgeführt, allerdings manipulieren wir den Wert in die andere Richtung:


'''orr w0, w0, #0x200''': Setzt das Bit 10 auf 1 und schaltet damit die LED wieder an.
Der Befehl reg |= 0x200 führt eine bitweise Operation durch, um ein bestimmtes Bit in der Variable reg zu setzen (auf 1 zu setzen). Hier ist die genaue Funktionsweise:
 
* Hexadezimale Zahl: 0x200 ist die hexadezimale Darstellung der Zahl 512. In Binärdarstellung ist das 0000 0010 0000 0000.
Wie hier zu sehen ist, verwende ich über der Funktion eine Art C-Code-Kommentar, der beschreibt, was die Funktion erwartet und was sie zurückgibt. Zusätzlich schreibe ich in Kurzform, was die Funktion eigentlich macht:
* Bitweises OR: Der Operator |= kombiniert die bestehende Variable reg mit 0x200 unter Verwendung des bitweisen OR-Operators (|). Dies bedeutet, dass jedes Bit in reg auf 1 gesetzt wird, wenn das entsprechende Bit in 0x200 ebenfalls 1 ist.


<syntaxhighlight lang="asm">
Der Befehl reg |= 0x200 setzt also das neunte Bit (wenn man von null zählt) von reg auf 1 und belässt alle anderen Bits unverändert.
// void LED_off(void) // Turns off the LED
</syntaxhighlight>
In diesem Fall steht void dafür, dass hier nichts übergeben und nichts zurückgegeben wird.


=== Basiskonfigurationsdatei '''base.h''' ===
=== Basiskonfigurationsdatei '''base.h''' ===
Die Datei '''base.h''' enthält Basisadressen für den Raspberry Pi:
Die Datei '''base.h''' enthält Basisadressen für den Raspberry Pi:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="C">
//
//
// base.h
// base.h
Zeile 211: Zeile 204:
#define _base_h
#define _base_h


.equ RPI_BASE, 0x107C000000UL
#define RPI_BASE 0x107C000000UL


// GPIO
// GPIO
.equ ARM_GPIO2_BASE, RPI_BASE + 0x1517C00
#define ARM_GPIO2_BASE RPI_BASE + 0x1517C00
.equ ARM_GPIO2_DATA0, ARM_GPIO2_BASE + 0x04
#define ARM_GPIO2_DATA0 ARM_GPIO2_BASE + 0x04


#endif
#endif
Zeile 233: Zeile 226:


=== Wartefunktion '''wait''' ===
=== Wartefunktion '''wait''' ===
Zuletzt erstellen wir die Datei '''time.S''' für die Wartefunktion:
Zuletzt erstellen wir die Datei '''time.c''' für die Wartefunktion:


<syntaxhighlight lang="asm">
<syntaxhighlight lang="c">
//
//
// time.S
// time.c
//
//


// void wait(int loop -> w0)
#include "types.h"
.section .text
 
.globl wait
void wait(u32 zyklen)  
wait:
{
     str x30, [sp, -16]!
     volatile u32 i;
     mov w1, 0
     for (i = 0; i < zyklen; i++)
1:
     {
     cmp w1, w0
        // Leere Schleife zur Verzögerung
    bgt 2f
     }
    add w1, w1, 1
}
    b 1b
2:
     ldr x30, [sp], 16
    ret
</syntaxhighlight>
</syntaxhighlight>
'''Erklärung''':
'''Erklärung''':
* '''str x30, [sp, -16]!''': Sichert die Rücksprungadresse auf dem Stack.
* '''volatile u32 i''': Erzeugt die Variable "i". Durch volatile wird bei einer Optimierung des Codes die Schleife nicht optimiert!
* '''mov w1, 0''': Initialisiert das Zählregister.
* '''for (i = 0; i < zyklen; i++)''': Dies erzeugt eine Schleife und zählt "i" hoch, solange i kleiner als zyklen ist.
* '''cmp w1, w0''': Vergleicht das Zählregister mit dem übergebenen Wert.
* '''{...]''': Hier wird eine leere Schleife generiert.  
* '''bgt 2f''': Springt zu Label 2, wenn w1 größer als w0 ist.
 
* '''add w1, w1, 1''': Erhöht das Zählregister.
=== write32 und read32 ===
* '''b 1b''': Wiederholt die Schleife.
Um ein Systemregister zu lesen oder auch dort hineinzuschreiben, benötigen wir noch zwei Funktionen, die wir '''write32''' und '''read32''' nennen. Da es um eine direkte Adressierung im Speicher geht, verwenden wir einfach einen Assemblercode:
* '''ldr x30, [sp], 16''': Stellt die Rücksprungadresse wieder her.
<syntaxhighlight lang="asm">
* '''ret''': Kehrt zur aufrufenden Funktion zurück.
//util.s
 
.globl write32
write32:
stp x29, x30, [sp, -16]!
mov x29, sp
str w1,[x0]
ldp x29, x30, [sp], 16
ret
 
 
.globl read32
read32:
stp x29, x30, [sp, -16]!
mov x29, sp
    ldr w0,[x0]
ldp x29, x30, [sp], 16
ret
</syntaxhighlight>
 
=== Weitere Header-Dateien ===
In C werden Funktionen häufig in Header-Dateien (mit der Endung .h) deklariert, bevor sie in den eigentlichen Quellcodedateien (mit der Endung .c) definiert werden. Dies hat mehrere wichtige Gründe:
 
* Modularität und Wiederverwendbarkeit:
 
: Header-Dateien ermöglichen es, den Code in verschiedene Module zu unterteilen. Dadurch können Funktionen und Datenstrukturen in mehreren Quellcodedateien wiederverwendet werden, ohne dass der Code dupliziert werden muss.
* Trennung von Deklaration und Definition:
 
: Die Deklaration einer Funktion in einer Header-Datei informiert den Compiler über die Existenz und das Interface der Funktion (Name, Rückgabewert, Parameter), ohne den vollständigen Funktionscode zur Verfügung zu stellen. Die Definition, die den eigentlichen Code enthält, befindet sich in der entsprechenden .c-Datei.
: Dies unterstützt das Prinzip der Informationsverbergung (Encapsulation), indem es die Implementierungsdetails von der Schnittstelle trennt.
* Vermeidung von Mehrfachdeklarationen:
 
: Durch das Einfügen der Header-Datei in mehrere Quellcodedateien (#include "header.h") wird sichergestellt, dass alle Quellcodedateien die gleichen Funktionsdeklarationen verwenden. Dies verhindert Fehler durch inkonsistente Deklarationen.
* Erleichterung der Wartung:
 
: Änderungen an der Funktionsdeklaration (z.B. Änderung der Parameter) müssen nur in der Header-Datei vorgenommen werden. Alle Quellcodedateien, die diese Header-Datei einbinden, verwenden automatisch die aktualisierte Deklaration.
* Kompilierung und Linken:
 
: Während des Kompilierens überprüft der Compiler die Header-Dateien, um sicherzustellen, dass die Funktionsaufrufe korrekt sind. Beim Linken werden dann die tatsächlichen Funktionsdefinitionen aus den Quellcodedateien zusammengeführt.
: Dies ermöglicht es auch, große Projekte effizient zu kompilieren, indem nur die geänderten Dateien neu kompiliert werden müssen, während die unveränderten Dateien aus der letzten Kompilierung wiederverwendet werden können.
* Interne und Externe Sichtbarkeit:
 
: Header-Dateien können genutzt werden, um die Sichtbarkeit von Funktionen und Variablen zu steuern. Funktionen, die in einer Header-Datei deklariert sind, sind für alle Quellcodedateien sichtbar, die diese Header-Datei einbinden. Funktionen, die nur in der Quellcodedatei definiert sind, sind dagegen nur in dieser Datei sichtbar (statische Funktionen).
 
In unserem Code fehlen nun noch drei Header-Dateien, led.h, time.h und types.h.
==== Die led.h ====
<syntaxhighlight lang="c">
// led.h
 
#ifndef _ms_led_h
#define _ms_led_h
 
void LED_off(void);
void LED_on(void);
 
#endif
</syntaxhighlight>
Diese Header-Datei definiert die Funktionsaufrufe '''LED_off''' und '''LED_on'''. Beide Funktionsaufrufe erwarten keinen Parameter und geben keine Parameter zurück. Dies ist durch das Schlüsselwort '''void''' erkennbar.
 
==== Die time.h====
<syntaxhighlight lang="c">
//time.h
 
#ifndef _ms_time_h
#define _ms_time_h
 
#include "types.h"
 
void wait(u32 zyklen);
 
#endif
</syntaxhighlight>
Hier wird der Funktionsaufruf wait definiert. Diese Funktion gibt keinen Wert zurück, aber erwartet von uns einen u32-Wert. u32 wird in dem nächsten Header "types.h" definiert. Deswegen wurde diese diesem Header inkludiert, damit der Compiler weiß, was u32 bedeutet.
 
==== Die types.h ====
<syntaxhighlight lang="c">
// types.h
 
#ifndef _ms_types_h
#define _ms_types_h
 
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
 
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
 
typedef unsigned long u64;
typedef signed long s64;
 
typedef long intptr;
typedef unsigned long uintptr;
 
typedef unsigned long size_t;
typedef long ssize_t;
 
typedef char boolean;
 
#define ALIGN(n) __attribute__((aligned (n)))
 
#define FALSE 0
#define TRUE 1
 
#endif
</syntaxhighlight>
Der Header types.h definiert verschiedene Datentypen und einige Makros, die in einem C-Programm verwendet werden können. Hier ist eine allgemeine Beschreibung dessen, was in diesem Header passiert:
 
'''Typdefinitionen:'''
* Es werden neue Datentypen mit typedef erstellt, um die Standardtypen in C klarer und konsistenter zu benennen.
** Unsigned Types:
*** u8: ein Alias für unsigned char (8-Bit).
*** u16: ein Alias für unsigned short (16-Bit).
*** u32: ein Alias für unsigned int (32-Bit).
*** u64: ein Alias für unsigned long (64-Bit).
** Signed Types:
*** s8: ein Alias für signed char (8-Bit).
*** s16: ein Alias für signed short (16-Bit).
*** s32: ein Alias für signed int (32-Bit).
*** s64: ein Alias für signed long (64-Bit).
** Pointer Types:
*** intptr: ein Alias für long, um einen Integer zu speichern, der groß genug ist, um einen Zeiger zu halten.
*** uintptr: ein Alias für unsigned long, für einen unsigned Integer, der groß genug ist, um einen Zeiger zu halten.
** Size Types:
*** size_t: ein Alias für unsigned long, typischerweise verwendet für die Größe von Objekten.
*** ssize_t: ein Alias für long, typischerweise verwendet für signierte Größenangaben.


Wie in der Beschreibung zu sehen ist, erwartet die Funktion '''wait''' einen Zähler im Register '''w0''', der angibt, wie lange gewartet werden soll. Die Funktion gibt keinen Wert zurück.
'''Boolean Type:'''
* Es wird ein neuer Typ boolean als Alias für char definiert.
<syntaxhighlight lang="c">
typedef char boolean;
</syntaxhighlight>


Ich habe bereits erwähnt, dass bei einem Aufruf mit '''bl''' (branch with link) die Adresse des nächsten Befehls in '''x30''' gespeichert wird. Da '''x30''' innerhalb einer Funktion verändert werden kann, insbesondere wenn weitere Funktionen aufgerufen werden, sichern wir diesen Wert auf den Stack. Dies geschieht mit '''str x30, [sp, -16]!'''. Der Befehl '''str''' (store) speichert in diesem Fall das Register '''x30''' im Stack und passt den Stack-Pointer (SP) um 16 Bytes nach unten an, sodass der nächste Wert einen freien Platz darauf hat. Am Ende des Codes wird der Wert wieder aus dem Stack in '''x30''' geladen und der Stack-Pointer zurückgesetzt. Der Wert in '''x30''' wird für den nächsten Befehl benötigt, da '''ret''' (return) den Programmzeiger auf diesen Wert setzt und das Programm weiterläuft.
'''Makros für Alignment:'''
* ALIGN(n): ein Makro, das das GCC-spezifische __attribute__((aligned(n))) verwendet, um die Ausrichtung eines Datentyps auf n Bytes zu erzwingen.
<syntaxhighlight lang="c">
#define ALIGN(n) __attribute__((aligned (n)))
</syntaxhighlight>


Die Funktion ist sehr einfach aufgebaut. Zunächst wird das Register '''w1''' mit 0 gefüllt. Dann wird es mit '''w0''' verglichen und solange '''w1''' kleiner ist, wird '''w1''' um eins erhöht und die Schleife beginnt von vorne. Nur wenn '''w1''' größer wird, endet die Schleife, '''x30''' wird wiederhergestellt und es wird zurück zum Hauptprogramm gesprungen.
'''Boolean Values:'''
* Zwei Makros FALSE und TRUE werden definiert, um die Werte 0 und 1 darzustellen.
<syntaxhighlight lang="c">
#define FALSE 0
#define TRUE 1
</syntaxhighlight>


In dieser Funktion habe ich außerdem lokale Labels verwendet, die einfach '''1:''' oder '''2:''' heißen. In bestimmten Fällen ist es hilfreich, solche Labels zu nutzen, da man sich keine umständlichen Namen ausdenken muss und der Code übersichtlicher bleibt. Diese Labels werden mit '''1:''' oder '''2:''' definiert. Soll auf eines dieser Labels gesprungen werden, wird einfach die Zahl mit einem Suffix verwendet, der angibt, ob das Label davor (b) oder danach (f) steht. Der Assembler sucht dann in der angegebenen Richtung nach diesem Label.
Zusammengefasst definiert dieser Header eine Reihe von neuen Typen und Makros, die die Lesbarkeit und Portabilität des Codes verbessern, indem sie standardisierte Namen und Werte bereitstellen.


=== Kompilieren und Ausführen ===
=== Kompilieren und Ausführen ===
Wechsle in das Verzeichnis LED und kompiliere das Programm mit dem Befehl make. Wenn alles erfolgreich war, erhältst du eine Datei kernel_2712.img, die auf eine SD-Karte kopiert und im Raspberry Pi verwendet wird. Schalte den Raspberry Pi ein, und die LED sollte blinken.
Wechsle in das Verzeichnis LED und kompiliere das Programm mit dem Befehl make. Wenn alles erfolgreich war, erhältst du eine Datei kernel_2712.img, die auf eine SD-Karte kopiert und im Raspberry Pi verwendet wird. Schalte den Raspberry Pi ein, und die LED sollte blinken.
Du kannst den Source-Code als ZIP-Datei mit folgenden Link downloaden: https://www.satyria.de/arm/sources/C/led.zip
-----
{| style="width: 100%;
| style="width: 33%;" | [[Unser erstes Programm in C (PI5)|< Zurück (Unser erstes Programm in C (PI5))]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[Fehlerbehandlung in C (PI5)|Weiter (Fehlerbehandlung in C (PI5)) >]]
|}

Aktuelle Version vom 23. August 2024, 10:50 Uhr

Unser jetziges Ziel ist es, die fest eingebaute LED des Raspberry Pi zum Blinken zu bringen. Ich werde jeden Schritt erklären, warum er in diesem Projekt so implementiert wurde.

Vorbereitung des Verzeichnisses

Erstelle zunächst ein neues Verzeichnis, z.B. LED, und platziere darin das Makefile und die Datei linker.ld. Erzeuge auch das Verzeichnis include innerhalb von LED, um unsere Header-Dateien zu organisieren.

Programmierung des Startverhaltens des Raspberry Pi

Leider kommen wir nicht ganz an Assembler vorbei. Der erste Code, der Ausgeführt wird, sollte in Assembler geschrieben sein. In der Regel werden dort einige Eigenschaften der CPU definiert, die "nur" in Assembler möglich ist. Bei diesem Beispiel ist das noch nicht nötig, aber wir erstellen diese Datei, damit wir bei späteren Projekten schon vorbereitet sind. Hierzu erstellen wir die Datei "boot.S":

//
// boot.S
//

#include “config.h”

.section .init  // Ensure the linker places this at the beginning of the kernel image
.global _start  // Execution starts here

_start:
	ldr	x0, =MEM_KERNEL_STACK
	mov	sp, x0			// init its stack	
	b sysinit

Erklärung:

  • #include "config.h": Inkludiert die Konfigurationsdatei, in der wir benötigte Parameter definieren.
  • .section .init: Definiert einen Abschnitt, der am Anfang des Kernel-Images platziert wird. Laut Linker-Script ist dies die erste Position im Speicher, die ab der Adresse 0x80000 abgelegt wird. Dies ist auch die Adresse, die der Raspberry Pi bei einem initialen Start als erstes anspringt.
  • .global _start: Deklariert _start als global, damit es von allen Programmteilen verwendet werden kann.
  • _start:: Label, das den Startpunkt der Ausführung markiert.
  • ldr x0, =MEM_KERNEL_STACK: Lädt die Adresse von MEM_KERNEL_STACK in das Register x0, wie es in "config.h" definiert ist.
  • mov sp, x0: Initialisiert den Stack-Zeiger "SP".
  • b sysinit: Springt zur Funktion "sysinit", die später implementiert wird.

Wenn wir das Programm starten, wird zunächst mit "ldr" (Load Register) das Register "x0" mit dem Wert aus "MEM_KERNEL_STACK" belegt, welcher im Include definiert wurde, und anschließend in den Stack-Zeiger (SP) abgelegt. Mit "b" (Branch, Sprung) springt der Code an das Label "sysinit".

Konfigurationsdatei config.h, eine Header-Datei

Bedeutung und Struktur von Header-Dateien

Unsere "config.h" ist eine Header-Datei. Grundsätzlich werden Header-Dateien in der Programmierung verwendet, um Code zu organisieren und wiederverwendbar zu machen. Hier sind die Hauptgründe erläutert:

  • Strukturierung: Header-Dateien helfen dabei, den Code in kleinere, übersichtliche Teile zu gliedern. Funktionen, Variablen und Konstanten, die in mehreren Dateien verwendet werden, können in einer Header-Datei deklariert werden.
  • Wiederverwendbarkeit: Einmal geschriebener Code in einer Header-Datei kann in mehreren Programmen oder Dateien verwendet werden, ohne den Code jedes Mal neu schreiben zu müssen.
  • Vereinfachung: Durch die Verwendung von Header-Dateien wird der Code übersichtlicher und leichter verständlich. Anstatt alle Funktionen in einer einzigen Datei zu haben, können sie in kleinere, logische Teile aufgeteilt werden.
  • Vermeidung von Doppelarbeit: Wenn sich ein Funktionsprototyp oder eine Konstante ändert, muss diese Änderung nur in der Header-Datei vorgenommen werden, anstatt in jeder Datei, die diese verwendet.

Zusammengefasst: Header-Dateien machen den Code übersichtlicher, wiederverwendbarer und leichter zu pflegen.

Unsere "config.h" ist nun wie folgt beschrieben:

//
// config.h
//

#ifndef _config_h
#define _config_h

#define MEGABYTE 0x100000

#define MEM_KERNEL_START 0x80000          // Startadresse des Hauptprogramms
#define KERNEL_MAX_SIZE (2 * MEGABYTE)
#define MEM_KERNEL_END (MEM_KERNEL_START + KERNEL_MAX_SIZE)
#define KERNEL_STACK_SIZE 0x20000
#define MEM_KERNEL_STACK (MEM_KERNEL_END + KERNEL_STACK_SIZE)

#endif

Erklärung:

  • #ifndef _config_h ... #endif: Verhindert, dass die Datei mehrfach inkludiert wird.
  • #define: Weist Symbolen konstante Werte zu.
  • MEM_KERNEL_STACK: Berechnet den Start des Kernel-Stacks basierend auf der Startadresse des Kernels und seiner maximalen Größe.

In diesem Header wird eine Bedingung abgefragt, ob die Variable _config_h bereits definiert wurde. Genauer gesagt, ob sie noch nicht definiert wurde. #ifndef bedeutet "if not defined", also "wenn nicht definiert". Wenn die Variable noch nicht definiert ist, wird alles verarbeitet, was bis zum #endif steht. Sollte die Variable bereits definiert sein, so wird alles übersprungen, was bis zum #endif steht.

Direkt nach der #ifndef-Anweisung wird die Variable mit #define _config_h definiert und ist damit dem Compiler bekannt.

Warum wird sowas in einem Header abgefragt? Der Compiler mag es überhaupt nicht, wenn Dinge mehrmals festgelegt werden. Sollte diese Header-Datei bereits an einer anderen Stelle aufgerufen worden sein, so überspringt der Compiler diese Festlegungen und es kommt nicht zu Mehrfachdefinitionen.

Funktion sysinit

Als nächstes erstellen wir die Funktion sysinit, die das System initialisiert:

//
// sysinit.S
//

.section .text
.globl sysinit
sysinit:
    b main

Erklärung:

  • .section .text: Definiert einen Abschnitt für ausführbaren Code.
  • .globl sysinit: Deklariert sysinit als global.
  • b main: Springt zur Funktion main.

Hier wird das Label "sysinit" zunächst als global definiert, damit es auch außerhalb der Datei bekannt ist. Im vorhergehenden Code wird darauf verwiesen. Im Gegensatz zum vorhergehenden Code wird hier die Sektion .text verwendet. Diese Sektion definiert ausführbaren Code, der "irgendwo" im Speicher liegt. Die genaue Position wird durch den zuvor geschriebenen Code bestimmt und später vom Linker festgelegt. Der einzige Befehl, der hier steht, ist b main, was bedeutet, dass das Programm einen Sprung (branch) zum Label "main" macht. Später werden wir hier mehr Code einfügen.

Hauptprogramm main

Erstellen wir nun das Hauptprogramm:

//
// kernel.c
//

#include "led.h"
#include "time.h"

int main (void)
{
    while(1)
    {
      LED_off();
      wait(0x3F0000);
      LED_on();
      wait(0x3F0000);
    }
}

Erklärung:

  • while(1): Erzeugt eine Endlosschleife, da 1 immer wahr ist.
  • LED_off(): Schaltet die LED aus.
  • wait(0x3F0000): Ruft die Wartefunktion auf.
  • LED_on(): Schaltet die LED ein.

Mit int main (void) erstellen wir das Label, welches von sysinit aufgerufen wird.

Zunächst definieren wir eine Endlosschleife mit while. Alles was nach der geschweiften Klammer steht wird endlos wiederholt, da 1 immer wahr ist.

Der erste Befehl LED_off() Ruft die entsprechende Funktion auf, die wir später noch schreiben werden. Diese Funktion soll dann die fest eingebaute grüne LED am Raspberry Pi 5 ausschalten.

Als nächstes wird die Funktion wait aufgerufen. Diese Funktion erwartet einen Wert, der angibt, wie lange gewartet werden soll, bevor der nächste Befehl ausgeführt wird.

Nachdem wait aufgerufen wurde, wird die Funktion LED_on() aufgerufen, die die grüne LED wieder einschaltet. Danach wird erneut gewartet und das ganze Programm wird unendlich wiederholt.

LED-Steuerungsfunktionen

Erstellen wir die Datei led.c für die LED-Steuerung:

//
// led.c
//

#include "base.h"
#include "util.h"
#include "types.h"

void LED_off(void)
{
    u32 reg = read32(ARM_GPIO2_DATA0);
    reg &= ~0x200; // Bit 9 auf 0 setzen
    write32 (ARM_GPIO2_DATA0,reg);
}

void LED_on(void)
{
    u32 reg = read32 (ARM_GPIO2_DATA0);
    reg |= 0x200; // Bit 9 auf 1 setzen
    write32 (ARM_GPIO2_DATA0,reg);
}

Erklärung:

  • u32 reg = read32(ARM_GPIO2_DATA0): Der Inhalt des Registers ARM_GPIO2_DATA0 wird gelesen und in reg abgelegt.
  • reg &= ~0x200: Löscht das 9 Bit, um die LED auszuschalten.
  • write32 (ARM_GPIO2_DATA0,reg): schreibt den Inhalt zurück in das Register
  • reg |= 0x200: Setzt das 9 Bit, um die LED einzuschalten.

Der erste Befehl in diesem Sourcecode ist das #include "base.h". In der base.h-Headerdatei sind einige Parameter abgelegt, wie zum Beispiel die Adresse der LED, die wir ansteuern wollen.

Im Raspberry Pi können viele Geräte (Peripherie) über bestimmte Adressen erreicht und dort direkt manipuliert werden. Die grüne LED des Raspberry Pi 5 ist über das GPIO2-Register erreichbar, genauer gesagt über das Register ARM_GPIO2_DATA0. Dort belegt die LED das Bit 9 des Registers. Da die anderen Bits in diesem Register andere Funktionen haben, müssen wir zunächst den gesamten Inhalt des Registers laden.

Der Befehl reg &= ~0x200 führt eine bitweise Operation durch, um ein bestimmtes Bit in der Variable reg zu löschen (auf 0 zu setzen). Hier ist die genaue Funktionsweise:

  • Hexadezimale Zahl: 0x200 ist die hexadezimale Darstellung der Zahl 512. In Binärdarstellung ist das 0000 0010 0000 0000.
  • Bitweise NOT-Operation: Der Operator ~ führt eine bitweise Negation durch, was bedeutet, dass alle Bits invertiert werden. Für 0x200 (binär 0000 0010 0000 0000) ergibt das:
    • ~0x200 = 1111 1101 1111 1111
  • Bitweises AND: Der Operator &= kombiniert die bestehende Variable reg mit dem Ergebnis der bitweisen Negation von 0x200 unter Verwendung des bitweisen AND-Operators (&). Dies bedeutet, dass nur die Bits in reg erhalten bleiben, die auch im invertierten Wert von 0x200 gesetzt sind.

Der Befehl reg &= ~0x200 setzt also das neunte Bit (wenn man von null zählt) von reg auf 0 und belässt alle anderen Bits unverändert.

Danach wird der Wert in das Register zurückgeschrieben und die LED ist aus.

In der LED_on-Funktion wird das gleiche durchgeführt, allerdings manipulieren wir den Wert in die andere Richtung:

Der Befehl reg |= 0x200 führt eine bitweise Operation durch, um ein bestimmtes Bit in der Variable reg zu setzen (auf 1 zu setzen). Hier ist die genaue Funktionsweise:

  • Hexadezimale Zahl: 0x200 ist die hexadezimale Darstellung der Zahl 512. In Binärdarstellung ist das 0000 0010 0000 0000.
  • Bitweises OR: Der Operator |= kombiniert die bestehende Variable reg mit 0x200 unter Verwendung des bitweisen OR-Operators (|). Dies bedeutet, dass jedes Bit in reg auf 1 gesetzt wird, wenn das entsprechende Bit in 0x200 ebenfalls 1 ist.

Der Befehl reg |= 0x200 setzt also das neunte Bit (wenn man von null zählt) von reg auf 1 und belässt alle anderen Bits unverändert.

Basiskonfigurationsdatei base.h

Die Datei base.h enthält Basisadressen für den Raspberry Pi:

//
// base.h
//

#ifndef _base_h
#define _base_h

#define RPI_BASE  0x107C000000UL

// GPIO
#define ARM_GPIO2_BASE RPI_BASE + 0x1517C00
#define ARM_GPIO2_DATA0 ARM_GPIO2_BASE + 0x04

#endif

Erklärung:

  • RPI_BASE: Basisadresse der Peripheriegeräte.
  • ARM_GPIO2_BASE: Basisadresse des GPIO2-Registers.
  • ARM_GPIO2_DATA0: Adresse des Datenregisters, das die LED steuert.

Vorab: Dieser Header wird im Verlauf unseres Kurses kontinuierlich erweitert. Zurzeit benötigen wir jedoch nur die Daten für unsere LED.

Mit RPI_BASE = 0x107C000000UL legen wir die Basisadresse der Peripheriegeräte fest. Diese dient als Grundlage für alle anderen Geräte, die wir verwenden.

Diese Datei wird später in Abschnitte für jedes einzelne Gerät unterteilt. Derzeit verwenden wir jedoch nur das GPIO.

Mit ARM_GPIO2_BASE = RPI_BASE + 0x1517C00 wird die Basisadresse des GPIO2 festgelegt. Der Daten-Offset ist dann 0x04, also insgesamt 0x107C000000 + 0x1517C00 + 0x04 ergibt 0x107D517C04. Dies ist die tatsächliche Speicheradresse im Raspberry Pi 5 für das Register, welches die LED steuert.

Wartefunktion wait

Zuletzt erstellen wir die Datei time.c für die Wartefunktion:

//
// time.c
//

#include "types.h"

void wait(u32 zyklen) 
{
    volatile u32 i;
    for (i = 0; i < zyklen; i++) 
    {
        // Leere Schleife zur Verzögerung
    }
}

Erklärung:

  • volatile u32 i: Erzeugt die Variable "i". Durch volatile wird bei einer Optimierung des Codes die Schleife nicht optimiert!
  • for (i = 0; i < zyklen; i++): Dies erzeugt eine Schleife und zählt "i" hoch, solange i kleiner als zyklen ist.
  • {...]: Hier wird eine leere Schleife generiert.

write32 und read32

Um ein Systemregister zu lesen oder auch dort hineinzuschreiben, benötigen wir noch zwei Funktionen, die wir write32 und read32 nennen. Da es um eine direkte Adressierung im Speicher geht, verwenden wir einfach einen Assemblercode:

//util.s

.globl write32
write32:
	stp x29, x30, [sp, -16]!
	mov x29, sp
	str w1,[x0]
	ldp x29, x30, [sp], 16
	ret

   
.globl read32
read32:
	stp x29, x30, [sp, -16]!
	mov x29, sp
    ldr w0,[x0]
	ldp x29, x30, [sp], 16
	ret

Weitere Header-Dateien

In C werden Funktionen häufig in Header-Dateien (mit der Endung .h) deklariert, bevor sie in den eigentlichen Quellcodedateien (mit der Endung .c) definiert werden. Dies hat mehrere wichtige Gründe:

  • Modularität und Wiederverwendbarkeit:
Header-Dateien ermöglichen es, den Code in verschiedene Module zu unterteilen. Dadurch können Funktionen und Datenstrukturen in mehreren Quellcodedateien wiederverwendet werden, ohne dass der Code dupliziert werden muss.
  • Trennung von Deklaration und Definition:
Die Deklaration einer Funktion in einer Header-Datei informiert den Compiler über die Existenz und das Interface der Funktion (Name, Rückgabewert, Parameter), ohne den vollständigen Funktionscode zur Verfügung zu stellen. Die Definition, die den eigentlichen Code enthält, befindet sich in der entsprechenden .c-Datei.
Dies unterstützt das Prinzip der Informationsverbergung (Encapsulation), indem es die Implementierungsdetails von der Schnittstelle trennt.
  • Vermeidung von Mehrfachdeklarationen:
Durch das Einfügen der Header-Datei in mehrere Quellcodedateien (#include "header.h") wird sichergestellt, dass alle Quellcodedateien die gleichen Funktionsdeklarationen verwenden. Dies verhindert Fehler durch inkonsistente Deklarationen.
  • Erleichterung der Wartung:
Änderungen an der Funktionsdeklaration (z.B. Änderung der Parameter) müssen nur in der Header-Datei vorgenommen werden. Alle Quellcodedateien, die diese Header-Datei einbinden, verwenden automatisch die aktualisierte Deklaration.
  • Kompilierung und Linken:
Während des Kompilierens überprüft der Compiler die Header-Dateien, um sicherzustellen, dass die Funktionsaufrufe korrekt sind. Beim Linken werden dann die tatsächlichen Funktionsdefinitionen aus den Quellcodedateien zusammengeführt.
Dies ermöglicht es auch, große Projekte effizient zu kompilieren, indem nur die geänderten Dateien neu kompiliert werden müssen, während die unveränderten Dateien aus der letzten Kompilierung wiederverwendet werden können.
  • Interne und Externe Sichtbarkeit:
Header-Dateien können genutzt werden, um die Sichtbarkeit von Funktionen und Variablen zu steuern. Funktionen, die in einer Header-Datei deklariert sind, sind für alle Quellcodedateien sichtbar, die diese Header-Datei einbinden. Funktionen, die nur in der Quellcodedatei definiert sind, sind dagegen nur in dieser Datei sichtbar (statische Funktionen).

In unserem Code fehlen nun noch drei Header-Dateien, led.h, time.h und types.h.

Die led.h

// led.h

#ifndef _ms_led_h
#define _ms_led_h

void LED_off(void);
void LED_on(void);

#endif

Diese Header-Datei definiert die Funktionsaufrufe LED_off und LED_on. Beide Funktionsaufrufe erwarten keinen Parameter und geben keine Parameter zurück. Dies ist durch das Schlüsselwort void erkennbar.

Die time.h

//time.h

#ifndef _ms_time_h
#define _ms_time_h

#include "types.h"

void wait(u32 zyklen);

#endif

Hier wird der Funktionsaufruf wait definiert. Diese Funktion gibt keinen Wert zurück, aber erwartet von uns einen u32-Wert. u32 wird in dem nächsten Header "types.h" definiert. Deswegen wurde diese diesem Header inkludiert, damit der Compiler weiß, was u32 bedeutet.

Die types.h

// types.h

#ifndef _ms_types_h
#define _ms_types_h

typedef unsigned char		u8;
typedef unsigned short		u16;
typedef unsigned int		u32;

typedef signed char		s8;
typedef signed short		s16;
typedef signed int		s32;

typedef unsigned long		u64;
typedef signed long		s64;

typedef long			intptr;
typedef unsigned long		uintptr;

typedef unsigned long		size_t;
typedef long			ssize_t;

typedef char		boolean;

#define	ALIGN(n)	__attribute__((aligned (n)))

#define FALSE		0
#define TRUE		1

#endif

Der Header types.h definiert verschiedene Datentypen und einige Makros, die in einem C-Programm verwendet werden können. Hier ist eine allgemeine Beschreibung dessen, was in diesem Header passiert:

Typdefinitionen:

  • Es werden neue Datentypen mit typedef erstellt, um die Standardtypen in C klarer und konsistenter zu benennen.
    • Unsigned Types:
      • u8: ein Alias für unsigned char (8-Bit).
      • u16: ein Alias für unsigned short (16-Bit).
      • u32: ein Alias für unsigned int (32-Bit).
      • u64: ein Alias für unsigned long (64-Bit).
    • Signed Types:
      • s8: ein Alias für signed char (8-Bit).
      • s16: ein Alias für signed short (16-Bit).
      • s32: ein Alias für signed int (32-Bit).
      • s64: ein Alias für signed long (64-Bit).
    • Pointer Types:
      • intptr: ein Alias für long, um einen Integer zu speichern, der groß genug ist, um einen Zeiger zu halten.
      • uintptr: ein Alias für unsigned long, für einen unsigned Integer, der groß genug ist, um einen Zeiger zu halten.
    • Size Types:
      • size_t: ein Alias für unsigned long, typischerweise verwendet für die Größe von Objekten.
      • ssize_t: ein Alias für long, typischerweise verwendet für signierte Größenangaben.

Boolean Type:

  • Es wird ein neuer Typ boolean als Alias für char definiert.
typedef char boolean;

Makros für Alignment:

  • ALIGN(n): ein Makro, das das GCC-spezifische __attribute__((aligned(n))) verwendet, um die Ausrichtung eines Datentyps auf n Bytes zu erzwingen.
#define ALIGN(n) __attribute__((aligned (n)))

Boolean Values:

  • Zwei Makros FALSE und TRUE werden definiert, um die Werte 0 und 1 darzustellen.
#define FALSE 0
#define TRUE 1

Zusammengefasst definiert dieser Header eine Reihe von neuen Typen und Makros, die die Lesbarkeit und Portabilität des Codes verbessern, indem sie standardisierte Namen und Werte bereitstellen.

Kompilieren und Ausführen

Wechsle in das Verzeichnis LED und kompiliere das Programm mit dem Befehl make. Wenn alles erfolgreich war, erhältst du eine Datei kernel_2712.img, die auf eine SD-Karte kopiert und im Raspberry Pi verwendet wird. Schalte den Raspberry Pi ein, und die LED sollte blinken.

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


< Zurück (Unser erstes Programm in C (PI5)) < Hauptseite > Weiter (Fehlerbehandlung in C (PI5)) >