Lass die LED leuchten (PI5)

Aus C und Assembler mit Raspberry

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

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