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

Aus C und Assembler mit Raspberry
 
(6 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 17: Zeile 17:


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


_start:
_start:
Zeile 25: Zeile 25:
</syntaxhighlight>
</syntaxhighlight>
'''Erklärung:'''
'''Erklärung:'''
 
* '''#include "config.h"''': Inkludiert die Konfigurationsdatei, in der wir benötigte Parameter definieren.
'''#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.
'''.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.
* '''_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.
'''.global _start''': Deklariert _start als global, damit es von allen Programmteilen verwendet werden kann.
* '''mov sp, x0''': Initialisiert den Stack-Zeiger "'''SP'''".
 
* '''b sysinit''': Springt zur Funktion "'''sysinit'''", die später implementiert wird.
'''_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'''".
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'''".
Zeile 75: Zeile 68:


'''Erklärung:'''
'''Erklärung:'''
* '''#ifndef _config_h ... #endif''': Verhindert, dass die Datei mehrfach inkludiert wird.
* '''.equ''': 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:
<syntaxhighlight lang="asm">
//
// sysinit.S
//
.section .text
.globl sysinit
sysinit:
    b main
</syntaxhighlight>
'''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.


'''#ifndef _config_h ... #endif''': Verhindert, dass die Datei mehrfach inkludiert wird.
=== Hauptprogramm '''main''' ===
Erstellen wir nun das Hauptprogramm:


'''.equ''': Weist Symbolen konstante Werte zu.
<syntaxhighlight lang="asm">
//
// kernel.S
//


'''MEM_KERNEL_STACK''': Berechnet den Start des Kernel-Stacks basierend auf der Startadresse des Kernels und seiner maximalen Größe.
.section .text
.globl main
main:
    bl LED_off
    mov w0, #0x3F0000
    bl wait
    bl LED_on
    mov w0, #0x3F0000
    bl wait
    b main
</syntaxhighlight>
'''Erklärung:'''
* '''.section .text''': Definiert den ausführbaren Codeabschnitt.
* '''.globl main''': Deklariert main als global.
* '''bl LED_off''': Schaltet die LED aus.
* '''mov w0, #0x3F0000''': Setzt den Wert für die Wartezeit.
* '''bl wait''': Ruft die Wartefunktion auf.
* '''bl LED_on''': Schaltet die LED ein.
* '''b main''': Wiederholt die Schleife.


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.
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.
 
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.
 
=== Parameterübergabe an Funktionen ===
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.
 
=== 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 mit '''b main''' das ganze Programm unendlich wiederholt.
 
=== LED-Steuerungsfunktionen ===
Erstellen wir die Datei led.S für die LED-Steuerung:
 
<syntaxhighlight lang="asm">
//
// led.S
//
 
#include "base.h"
 
// void LED_off(void) // Schaltet die LED aus
.section .text
.globl LED_off
LED_off:
    ldr x1, =ARM_GPIO2_DATA0
    ldr w0, [x1]
    bic w0, w0, #0x200
    str w0, [x1]
    ret
 
// void LED_on(void) // Schaltet die LED ein
.section .text
.globl LED_on
LED_on:
    ldr x1, =ARM_GPIO2_DATA0
    ldr w0, [x1]
    orr w0, w0, #0x200
    str w0, [x1]
    ret
</syntaxhighlight>
 
'''Erklärung:'''
* '''ldr x1, =ARM_GPIO2_DATA0''': Lädt die Adresse der LED.
* '''bic w0, w0, #0x200''': Löscht das Bit, um die LED auszuschalten.
* '''orr w0, w0, #0x200''': Setzt das 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.
 
Folgendes wird in diesem Code durchgeführt:
 
# '''ldr x1, =ARM_GPIO2_DATA0''': Lädt die Adresse von ARM_GPIO2_DATA0 in das Register x1.
# '''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.
 
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.
 
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:
 
<syntaxhighlight lang="asm">
// 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''' ===
Die Datei '''base.h''' enthält Basisadressen für den Raspberry Pi:
 
<syntaxhighlight lang="asm">
//
// base.h
//
 
#ifndef _base_h
#define _base_h
 
.equ RPI_BASE, 0x107C000000UL
 
// GPIO
.equ ARM_GPIO2_BASE, RPI_BASE + 0x1517C00
.equ ARM_GPIO2_DATA0, ARM_GPIO2_BASE + 0x04
 
#endif
</syntaxhighlight>
'''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.S''' für die Wartefunktion:
 
<syntaxhighlight lang="asm">
//
// time.S
//
 
// void wait(int loop -> w0)
.section .text
.globl wait
wait:
    str x30, [sp, -16]!
    mov w1, 0
1:
    cmp w1, w0
    bgt 2f
    add w1, w1, 1
    b 1b
2:
    ldr x30, [sp], 16
    ret
</syntaxhighlight>
'''Erklärung''':
* '''str x30, [sp, -16]!''': Sichert die Rücksprungadresse auf dem Stack.
* '''mov w1, 0''': Initialisiert das Zählregister.
* '''cmp w1, w0''': Vergleicht das Zählregister mit dem übergebenen Wert.
* '''bgt 2f''': Springt zu Label 2, wenn w1 größer als w0 ist.
* '''add w1, w1, 1''': Erhöht das Zählregister.
* '''b 1b''': Wiederholt die Schleife.
* '''ldr x30, [sp], 16''': Stellt die Rücksprungadresse wieder her.
* '''ret''': Kehrt zur aufrufenden Funktion zurück.
 
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.
 
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.
 
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.
 
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.
 
=== 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.


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.
{| style="width: 100%;
| style="width: 33%;" | [[Unser erstes Programm (PI5)|< Zurück (Unser erstes Programm (PI5))]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[Fehlerbehandlung|Weiter (Fehlerbehandlung) >]]
|}

Aktuelle Version vom 21. November 2024, 14:39 Uhr

Mit den zuvor erstellten Makefile und Linker-Script haben wir eine Basis geschaffen, um ein sinnvolles Programm zu entwickeln. Unser 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

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

.equ MEGABYTE, 0x100000

.equ MEM_KERNEL_START, 0x80000          // Startadresse des Hauptprogramms
.equ KERNEL_MAX_SIZE, (2 * MEGABYTE)
.equ MEM_KERNEL_END, (MEM_KERNEL_START + KERNEL_MAX_SIZE)
.equ KERNEL_STACK_SIZE, 0x20000
.equ MEM_KERNEL_STACK, (MEM_KERNEL_END + KERNEL_STACK_SIZE)

#endif

Erklärung:

  • #ifndef _config_h ... #endif: Verhindert, dass die Datei mehrfach inkludiert wird.
  • .equ: 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.S
//

.section .text
.globl main
main:
    bl LED_off
    mov w0, #0x3F0000
    bl wait
    bl LED_on
    mov w0, #0x3F0000
    bl wait
    b main

Erklärung:

  • .section .text: Definiert den ausführbaren Codeabschnitt.
  • .globl main: Deklariert main als global.
  • bl LED_off: Schaltet die LED aus.
  • mov w0, #0x3F0000: Setzt den Wert für die Wartezeit.
  • 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.

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.

Parameterübergabe an Funktionen

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.

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 mit b main das ganze Programm unendlich wiederholt.

LED-Steuerungsfunktionen

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

//
// led.S
//

#include "base.h"

// void LED_off(void) // Schaltet die LED aus
.section .text
.globl LED_off
LED_off:
    ldr x1, =ARM_GPIO2_DATA0
    ldr w0, [x1]
    bic w0, w0, #0x200
    str w0, [x1]
    ret

// void LED_on(void) // Schaltet die LED ein
.section .text
.globl LED_on
LED_on:
    ldr x1, =ARM_GPIO2_DATA0
    ldr w0, [x1]
    orr w0, w0, #0x200
    str w0, [x1]
    ret

Erklärung:

  • ldr x1, =ARM_GPIO2_DATA0: Lädt die Adresse der LED.
  • bic w0, w0, #0x200: Löscht das Bit, um die LED auszuschalten.
  • orr w0, w0, #0x200: Setzt das 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.

Folgendes wird in diesem Code durchgeführt:

  1. ldr x1, =ARM_GPIO2_DATA0: Lädt die Adresse von ARM_GPIO2_DATA0 in das Register x1.
  2. ldr w0, [x1]: Lädt den aktuellen Wert von ARM_GPIO2_DATA0 in das Register w0.
  3. bic w0, w0, #0x200: Setzt das Bit, das die LED steuert, auf 0 (schaltet die LED aus).
  4. str w0, [x1]: Speichert den neuen Wert zurück in ARM_GPIO2_DATA0.
  5. 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.

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.

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:

// void LED_off(void) // Turns off the LED

In diesem Fall steht void dafür, dass hier nichts übergeben und nichts zurückgegeben wird.

Basiskonfigurationsdatei base.h

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

//
// base.h
//

#ifndef _base_h
#define _base_h

.equ RPI_BASE, 0x107C000000UL

// GPIO
.equ ARM_GPIO2_BASE, RPI_BASE + 0x1517C00
.equ 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.S für die Wartefunktion:

//
// time.S
//

// void wait(int loop -> w0)
.section .text
.globl wait
wait:
    str x30, [sp, -16]!
    mov w1, 0
1:
    cmp w1, w0
    bgt 2f
    add w1, w1, 1
    b 1b
2:
    ldr x30, [sp], 16
    ret

Erklärung:

  • str x30, [sp, -16]!: Sichert die Rücksprungadresse auf dem Stack.
  • mov w1, 0: Initialisiert das Zählregister.
  • cmp w1, w0: Vergleicht das Zählregister mit dem übergebenen Wert.
  • bgt 2f: Springt zu Label 2, wenn w1 größer als w0 ist.
  • add w1, w1, 1: Erhöht das Zählregister.
  • b 1b: Wiederholt die Schleife.
  • ldr x30, [sp], 16: Stellt die Rücksprungadresse wieder her.
  • ret: Kehrt zur aufrufenden Funktion zurück.

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.

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.

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.

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.

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.


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