Lass die LED leuchten in C (PI4)

Aus C und Assembler mit Raspberry

Einführung

Unser Ziel ist es, die fest eingebaute LED des Raspberry Pi 4 zum Blinken zu bringen. Ich werde jeden Schritt erklären und die Gründe für die implementierten Schritte näher erläutern.

Vorbereitung des Verzeichnisses

Erstellen Sie zunächst ein neues Verzeichnis, z.B. LED, und platzieren Sie darin das Makefile und die Datei linker.ld. Erzeugen Sie auch das Verzeichnis "include" innerhalb von LED, um unsere Header-Dateien zu organisieren.


Programmierung des Startverhaltens des Raspberry Pi

Leider kommen wir nicht ganz ohne Assembler aus. Der erste Code, der ausgeführt wird, sollte in Assembler geschrieben sein. In der Regel werden einige Eigenschaften der CPU definiert, die nur in Assembler möglich sind. Für dieses Beispiel benötigen wir das noch nicht, aber wir erstellen diese Datei, um für spätere Projekte vorbereitet zu sein. Erstellen Sie die Datei boot.S:

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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 benötigte Parameter definiert sind.
  • .section .init: Definiert einen Abschnitt, der am Anfang des Kernel-Images platziert wird.
  • .global _start: Deklariert _start als global, damit es von allen Teilen des Programms 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.
  • mov sp, x0: Initialisiert den Stack-Zeiger (SP).
  • b sysinit: Springt zur Funktion sysinit, die später implementiert wird.

Wenn das Programm startet, wird zunächst mit ldr (Load Register) das Register x0 mit dem Wert aus MEM_KERNEL_STACK belegt, der in der Include-Datei definiert wurde, und anschließend in den Stack-Zeiger (SP) abgelegt. Mit b (Branch) wird der Code zum Label sysinit verzweigt.

Konfigurationsdatei config.h (eine Header-Datei)

Bedeutung und Struktur von Header-Dateien

Unsere "config.h" ist eine Header-Datei. Header-Dateien werden verwendet, um Code zu organisieren und wieder verwendbar zu machen.

Hauptgründe sind:

  • Strukturierung: Header-Dateien helfen dabei, den Code in kleinere, übersichtliche Teile zu gliedern.
  • Wiederverwendbarkeit: Einmal geschriebener Code kann in mehreren Programmen oder Dateien verwendet werden.
  • Vereinfachung: Macht den Code übersichtlicher und leichter verständlich.
  • Vermeidung von Doppelarbeit: Änderungen müssen nur in der Header-Datei vorgenommen werden.

Unsere "config.h" ist wie folgt beschrieben:

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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.

Die #ifndef-Anweisung (if not defined) stellt sicher, dass die Datei nur einmal verarbeitet wird. Danach wird die Variable mit #define _config_h definiert und ist dem Compiler bekannt.

Funktion sysinit

Erstellen Sie die Funktion sysinit, die das System initialisiert:

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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.

Das Label "sysinit" wird global definiert, damit es auch außerhalb der Datei bekannt ist. Der einzige Befehl b main bedeutet, dass das Programm einen Sprung zum Label main macht. Später werden wir hier mehr Code einfügen.

Hauptprogramm main

Erstellen Sie nun das Hauptprogramm:

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// kernel.c
//

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

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

Erklärung

  • while(1): Erzeugt eine Endlosschleife.
  • LED_off(): Schaltet die LED aus.
  • wait(0x1F0000): Ruft die Wartefunktion auf.
  • LED_on(): Schaltet die LED ein.

Mit int main (void) erstellen wir das Label, welches von sysinit aufgerufen wird. Die Endlosschleife while(1) bedeutet, dass der Code innerhalb der Schleife fortlaufend ausgeführt wird. LED_off(), wait(), LED_on() und erneut wait() wiederholen den Zyklus unendlich.

LED-Steuerungsfunktionen

Auf dem Raspberry Pi 4 können wir auf die GPIO-PINs direkt zugreifen, die für die Kommunikation mit der Außenwelt verwendet werden. Da eine PIN mit einer fest eingebauten LED verbunden ist, können wir auf externe Hardware verzichten.

Zugriff auf die GPIO-Pins

Der Raspberry Pi 4 hat 40 GPIO-Pins, von denen 26 als Ausgang oder Eingang programmiert werden können. Zusätzlich gibt es interne GPIO-Adressen, die per Programmierschnittstelle programmiert werden können. Eine davon ist die grüne LED, die über den Port 42 angesteuert wird.

Konfiguration des GPIO-Registers

Um die LED zu steuern, müssen wir das Register GPFSEL verwenden. Das Register GPFSEL4 ist für GPIO-Adresse 42 zuständig und jede GPIO-Adresse wird durch 3 Bits konfiguriert:

Status des GPIO-Registers
000 PIN ist ein Eingang
001 PIN ist ein Ausgang
100 PIN hat die alternative Funktion 0
101 PIN hat die alternative Funktion 1
110 PIN hat die alternative Funktion 2
111 PIN hat die alternative Funktion 3
011 PIN hat die alternative Funktion 4
010 PIN hat die alternative Funktion 5

Funktion zur Konfiguration von GPIO-Pins

void SetGPIOFunction(u32 Pin, u32 Function)
{
  u32 GPSEL = GPIO_GPFSEL0;
  while (Pin >= 10)
  {
    Pin -= 10;
    GPSEL += 4;
  }
  Pin *= 3;
  Function <<= Pin;
  u32 mask = 0b111 << Pin;
  mask = ~mask; // Bitweise negieren

  u32 sel = read32(GPSEL);
  sel &= mask;
  sel |= Function;
  write32(GPSEL, sel);
}

Erklärung

Diese Funktion bestimmt das richtige GPFSEL-Register und setzt die entsprechende Funktion für den Pin. Zunächst wird die Basis-Adresse für GPIO_GPFSEL0 geladen, und durch eine Schleife die richtige Registeradresse und Pin-Position ermittelt. Der Inhalt des Registers wird gelesen, die entsprechenden Bits gelöscht und mit der neuen Funktion überschrieben.

Funktionen zur Steuerung der LED

void LED_on(void)
{
  u32 LED_Pin = 42;
  SetGPIOFunction(LED_Pin, GPIO_output);

  u32 GPSET = GPIO_GPSET0;
  if (LED_Pin > 31)
  {
    GPSET += 4;
    LED_Pin -= 32;
  }
  write32(GPSET, 1 << LED_Pin);
}

void LED_off(void)
{
  u32 LED_Pin = 42;
  SetGPIOFunction(LED_Pin, GPIO_output);

  u32 GPCLR = GPIO_GPCLR0;
  if (LED_Pin > 31)
  {
    GPCLR += 4;
    LED_Pin -= 32;
  }
  write32(GPCLR, 1 << LED_Pin);
}

Erklärung

Wir erstellen zwei Funktionen, LED_on und LED_off, um die LED ein- und auszuschalten. Beide Funktionen setzen den entsprechenden Pin (42) auf "Output", ermitteln das richtige GPSET oder GPCLR-Register und setzen bzw. löschen das entsprechende Bit, um die LED zu steuern.

Basiskonfigurationsdatei base.h

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

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// base.h
//

#ifndef _base_h
#define _base_h

#define RPI_BASE 0xFE000000

// General Purpose I/O (GPIO)                 
#define GPIO_BASE                           (RPI_BASE + 0x200000)
#define GPIO_GPFSEL0                        (GPIO_BASE + 0x00) // GPIO Function Select 0        
#define GPIO_GPFSEL1                        (GPIO_BASE + 0x04) // GPIO Function Select 1        
#define GPIO_GPFSEL2                        (GPIO_BASE + 0x08) // GPIO Function Select 2        
#define GPIO_GPFSEL3                        (GPIO_BASE + 0x0c) // GPIO Function Select 3        
#define GPIO_GPFSEL4                        (GPIO_BASE + 0x10) // GPIO Function Select 4        
#define GPIO_GPFSEL5                        (GPIO_BASE + 0x14) // GPIO Function Select 5        
#define GPIO_GPSET0                         (GPIO_BASE + 0x1c) // GPIO Pin Output Set 0       
#define GPIO_GPSET1                         (GPIO_BASE + 0x20) // GPIO Pin Output Set 1       
#define GPIO_GPCLR0                         (GPIO_BASE + 0x28) // GPIO Pin Output Clear 0       
#define GPIO_GPCLR1                         (GPIO_BASE + 0x2c) // GPIO Pin Output Clear 1       
#define GPIO_GPLEV0                         (GPIO_BASE + 0x34) // GPIO Pin Level 0        
#define GPIO_GPLEV1                         (GPIO_BASE + 0x38) // GPIO Pin Level 1        
#define GPIO_GPEDS0                         (GPIO_BASE + 0x40) // GPIO Pin Event Detect Status 0      
#define GPIO_GPEDS1                         (GPIO_BASE + 0x44) // GPIO Pin Event Detect Status 1      
#define GPIO_GPREN0                         (GPIO_BASE + 0x4c) // GPIO Pin Rising Edge Detect Enable 0     
#define GPIO_GPREN1                         (GPIO_BASE + 0x50) // GPIO Pin Rising Edge Detect Enable 1     
#define GPIO_GPFEN0                         (GPIO_BASE + 0x58) // GPIO Pin Falling Edge Detect Enable 0     
#define GPIO_GPFEN1                         (GPIO_BASE + 0x5c) // GPIO Pin Falling Edge Detect Enable 1     
#define GPIO_GPHEN0                         (GPIO_BASE + 0x64) // GPIO Pin High Detect Enable 0      
#define GPIO_GPHEN1                         (GPIO_BASE + 0x68) // GPIO Pin High Detect Enable 1      
#define GPIO_GPLEN0                         (GPIO_BASE + 0x70) // GPIO Pin Low Detect Enable 0      
#define GPIO_GPLEN1                         (GPIO_BASE + 0x74) // GPIO Pin Low Detect Enable 1      
#define GPIO_GPAREN0                        (GPIO_BASE + 0x7c) // GPIO Pin Async. Rising Edge Detect 0     
#define GPIO_GPAREN1                        (GPIO_BASE + 0x80) // GPIO Pin Async. Rising Edge Detect 1     
#define GPIO_GPAFEN0                        (GPIO_BASE + 0x88) // GPIO Pin Async. Falling Edge Detect 0     
#define GPIO_GPAFEN1                        (GPIO_BASE + 0x8c) // GPIO Pin Async. Falling Edge Detect 1     
#define GPIO_GPIO_PUP_PDN_CNTRL_REG0        (GPIO_BASE + 0xe4) // GPIO Pull-up / Pull-down Register 0      
#define GPIO_GPIO_PUP_PDN_CNTRL_REG1        (GPIO_BASE + 0xe8) // GPIO Pull-up / Pull-down Register 1      
#define GPIO_GPIO_PUP_PDN_CNTRL_REG2        (GPIO_BASE + 0xec) // GPIO Pull-up / Pull-down Register 2      
#define GPIO_GPIO_PUP_PDN_CNTRL_REG3        (GPIO_BASE + 0xf0) // GPIO Pull-up / Pull-down Register 3      
// Information from BCM2835 ARM Peripherals                 
#define GPIO_GPPUD                          (GPIO_BASE + 0x94) // GPIO Pin Pull-up/down Enable        
#define GPIO_GPPUDCLK0                      (GPIO_BASE + 0x98) // GPIO Pin Pull-up/down Enable Clock 0      
#define GPIO_GPPUDCLK1                      (GPIO_BASE + 0x9c) // GPIO Pin Pull-up/down Enable Clock 1      

#endif

Erklärung

In dieser Header-Datei wurden alle Funktionen (Adressen) definiert, die GPIO betreffen. Diese Datei wird im Verlauf unseres Kurses kontinuierlich erweitert; aktuell benötigen wir jedoch nur die Daten für unsere LED.

Mit #define RPI_BASE 0xFE000000 legen wir die Basisadresse der Peripheriegeräte fest. Dies dient als Grundlage für alle anderen Geräte, die wir verwenden. #define GPIO_BASE (RPI_BASE + 0x200000) legt die Basisadresse des GPIO fest.

read32 und write32

Diese zwei Funktionen ermöglichen direkten Zugriff auf den Speicher des Raspberry Pi 4. Wir definieren diese Funktionen in Assembler, um einfacher auf die Hardware zugreifen zu können.

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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

Speichern Sie die Datei als util.S.

Erklärung

write32 nutzt die Zeile str w1,[x0] (store), um den zweiten Wert an die Adresse des ersten Wertes zu schreiben. read32 nutzt die Zeile ldr w0,[x0] (load), um den Wert an der übergebenen Adresse zu laden und zurückzugeben.

Damit C weiß, welche Werte hier erwartet werden, müssen wir in einer Header-Datei festlegen, was wir erwarten:

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// util.h
//

#ifndef _ms_util_h
#define _ms_util_h

#include "types.h"

void write32(u32 a, u32 b);
u32 read32(u32 a);

#endif

Wartefunktion wait

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

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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 die Schleife bei der Code-Optimierung nicht optimiert.
  • for (i = 0; i < zyklen; i++): Erzeugt eine Schleife und zählt i hoch, solange i kleiner als zyklen ist.
  • {...]: Leere Schleife zur Verzögerung.

stes Programm in C auf Raspberry Pi 4 0 attached files Delete all Sent MS Matthias 10:51 AM • Today Überprüfe folgenden Text auf Inhalt und ändere es für einen Tutor: Unser erstes Programm in C (PI4): Die Programmierung des Raspberry Pi 4 wird ähnlich durchgeführt, wie die Programmierung des Raspberry's Pi 5. Ich habe hierzu den den Kurs dort herauskopiert und es entsprechend angepasst. Ich habe es aus den Gründen gemacht, da ich USB unterstützen möchte und es inzwischen mehr Informationen zum RPI4 gibt, als für den RPI5.

Idealerweise hatte ich dort ein Terminal programmiert, welches ich sehr gut zum Debuggen verwenden kann. In diesem Sinne, kann ich den Kurs entsprechend hier auch einführen.

Unser erstes Programm wird zunächst nichts tun. Es wird einfach eine Dauerschleife durchlaufen. Dies dient als Grundbaustein für alle weiteren Versuche. Ich werde erklären, wie ein solches Programm erstellt, kompiliert und ausgeführt wird. Zunächst schauen wir, ob alles funktioniert.

Sourcecode erstellen Leider kommen wir bei BareMetal mit C nicht an Assembler vorbei. Auch wenn das Assemblerprogramm zur Zeit nicht wirklich viel tut, wird es später zum Beispiel für die Interrupt-Progrogramierung benötigt.

Öffnen Sie ein Textprogramm, um unser erstes Assemblerprogramm zu schreiben:

// // The first program for RPI4 // 20.02.2025 www.satyria.de //

.section .init // Ensure the linker places this at the beginning of the kernel image .globl _start // Generates a global label _start: // The label _start (entry address)

 mov sp, #0x80000  // Create a stack of 512KB (524288 bytes)
 b main            // Branch to "main"

Speichern Sie die Datei im Standardverzeichnis unter Windows: C:\msys64\home\xxx (wobei xxx in der Regel Ihr Benutzername ist). Unter Linux können Sie die Datei im Home-Verzeichnis speichern. Geben Sie der Datei den Namen boot.S. Die Endung .S kennzeichnet die Datei als Assembler-Sourcecode.

Da wir ja in C programmieren wollen schreiben wir noch ein kleines main:

// // main.c // The first program for RPI4 // 20.02.2025 www.satyria.de //

int main (void) {

 while (1){}

} Hier wird einfach eine Endlosschleife erzeugt.

Kompilieren des Programms mit make Um nun unser Programm zu Kompilieren, muss zunächst die Programmierumgebung eingerichtet werden. Verwende dazu folgende Anleitungen. Diese funktionieren auch für den RPI4:

Programmierumgebung erstellen (Konsole) Programmierumgebung erstellen (64-Bit)

Wenn wir dies nun erstellt haben, können wir ein Makefile erstellen, welches wir für den RPI4 angepasst haben:

CSRCS := $(wildcard *.c) CPPSRCS := $(wildcard *.cpp) ASRCS := $(wildcard *.S) COBJS := $(CSRCS:.c=.o) CPPOBJS := $(CPPSRCS:.cpp=.o) AOBJS := $(ASRCS:.S=.o) AllOBJS := $(COBJS) $(CPPOBJS) $(AOBJS) LOADADDR = 0x80000

GCCFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -O0 -ffreestanding \

          -nostartfiles -nostdlib -nostdinc -g -I ./include

AFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -I ./include -O0 -g

CFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char -ffreestanding -g \

        -I ./include -O0 -fno-exceptions 

CPPFLAGS = -fno-exceptions -fno-rtti -nostdinc++ -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char \ -ffreestanding -g -I ./include -O0 -mstrict-align -std=c++14 -Wno-aligned-new

all: clean new kernel8.img

%.o: %.S @echo "as $@" @aarch64-none-elf-gcc $(AFLAGS) -c $< -o $@

%.o: %.c @echo "gcc $@" @aarch64-none-elf-gcc $(CFLAGS) -c $< -o $@

%.o: %.cpp @echo "g++ $@" @aarch64-none-elf-g++ $(CPPFLAGS) -c $< -o $@

kernel8.img: $(AllOBJS) @echo "=============================================================================" @echo "Linking..." @aarch64-none-elf-ld -o kernel8.elf -Map kernel8.map -nostdlib \ --section-start=.init=$(LOADADDR) --no-warn-rwx-segments \ -g -T linker.ld $(AllOBJS) aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img

clean: /bin/rm -f kernel8.elf kernel8.map *.o *.img > /dev/null 2> /dev/null || true

new: /bin/clear Speicher diese Datei als "makefile" ab. Wenn du mehr über den Inhalt erfahren möchtest, so schaue unter Arbeiten mit Make und Linker-Script nach.

Was nun noch fehlt, ist ein Linker-Script, welches wir bereits auch unter Arbeiten mit Make und Linker-Script beschrieben haben:

ENTRY(_start)

SECTIONS { .init : { *(.init) } .text : { *(.text*) _etext = .; } .rodata : { *(.rodata*) } .init_array : { __init_start = .; KEEP(*(.init_array*)) __init_end = .; } .ARM.exidx : { __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; } .eh_frame : { *(.eh_frame*) } .data : { *(.data*) } .bss : { __bss_start = .; *(.bss*) *(COMMON) __bss_end = .; } } __bss_size = (__bss_end - __bss_start) >> 3; Speichere diese Datei als "linker.ld" ab.

Nun können wir unser erstes Programm kompilieren:

make Damit unser Kernel nun funktionsfähig ist, muss der Kernel auf die SD-Karte, die in FAT32 formatiert ist, kopiert werden. Zusätzlich benötigt der Raspberry Pi 4 folgende Dateien auf der SD-Karte:

LISTE!!!

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


Received

RocheChat 10:51 AM • Today Hier ist der überarbeitete Text, angepasst für einen Tutor:

Unser erstes Programm in C (für den Raspberry Pi 4) Einführung In diesem Kurs werden wir die Grundlagen der Programmierung des Raspberry Pi 4 erlernen. Ähnlich wie bei der Programmierung des Raspberry Pi 5, gibt es einige Unterschiede, hauptsächlich bedingt durch die Unterstützung von USB und die umfangreichere Dokumentation für den Raspberry Pi 4. Für diesen Kurs habe ich Material vom Raspberry Pi 5 angepasst, um die Programmierung des Raspberry Pi 4 zu erläutern.

Ein Terminal wurde programmiert, das sich sehr gut zum Debuggen eignet. Dies werden wir in unserem Kurs ebenfalls verwenden.

Ziel unseres ersten Programms Unser erstes Programm wird eine einfache Endlosschleife enthalten. Dies dient als Basis für weitere Experimente und Versuche. Ich werde erklären, wie ein solches Programm erstellt, kompiliert und ausgeführt wird.

Erstellung des Sourcecodes Leider kommen wir bei Bare-Metal-Programmierung in C nicht um Assembler herum. Auch wenn das Assemblerprogramm zunächst nicht viel tut, wird es später beispielsweise für die Interrupt-Programmierung benötigt.

Assembler-Code (boot.S) Öffnen Sie ein Textprogramm und schreiben Sie folgendes erstes Assemblerprogramm:

ASSEMBLY Copy code // // The first program for RPI4 // 20.02.2025 www.satyria.de //

.section .init // Ensure the linker places this at the beginning of the kernel image .globl _start // Generates a global label _start: // The label _start (entry address)

 mov sp, #0x80000  // Create a stack of 512KB (524288 bytes)
 b main            // Branch to "main"

Speichern Sie die Datei im Standardverzeichnis unter Windows: C:\msys64\home\xxx (wobei xxx in der Regel Ihr Benutzername ist). Unter Linux können Sie die Datei im Home-Verzeichnis speichern. Geben Sie der Datei den Namen boot.S. Die Endung .S kennzeichnet die Datei als Assembler-Sourcecode.

C-Code (main.c) Nun erstellen wir eine main.c Datei für unser erstes Programm:

C Copy code // // main.c // The first program for RPI4 // 20.02.2025 www.satyria.de //

int main (void) {

 while (1){}

} Hier wird eine einfache Endlosschleife erzeugt.

Kompilieren des Programms mit Make Um unser Programm zu kompilieren, muss zunächst die Programmierumgebung eingerichtet werden. Verwenden Sie hierzu die entsprechenden Anleitungen. Diese funktionieren auch für den Raspberry Pi 4:

Programmierumgebung erstellen (Konsole) Programmierumgebung erstellen (64-Bit) Erstellen Sie dann ein Makefile, angepasst für den Raspberry Pi 4:

MAKEFILE Copy code CSRCS := $(wildcard *.c) CPPSRCS := $(wildcard *.cpp) ASRCS := $(wildcard *.S) COBJS := $(CSRCS:.c=.o) CPPOBJS := $(CPPSRCS:.cpp=.o) AOBJS := $(ASRCS:.S=.o) AllOBJS := $(COBJS) $(CPPOBJS) $(AOBJS) LOADADDR = 0x80000

GCCFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -O0 -ffreestanding \

          -nostartfiles -nostdlib -nostdinc -g -I ./include

AFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -I ./include -O0 -g

CFLAGS = -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char -ffreestanding -g \

        -I ./include -O0 -fno-exceptions 

CPPFLAGS = -fno-exceptions -fno-rtti -nostdinc++ -DAARCH=64 -mcpu=cortex-a72 -mlittle-endian -Wall -fsigned-char \

             -ffreestanding -g -I ./include -O0 -mstrict-align -std=c++14 -Wno-aligned-new

all: clean new kernel8.img

%.o: %.S

   @echo "as $@"
   @aarch64-none-elf-gcc $(AFLAGS) -c $< -o $@

%.o: %.c

   @echo "gcc $@"
   @aarch64-none-elf-gcc $(CFLAGS) -c $< -o $@

%.o: %.cpp

   @echo "g++ $@"
   @aarch64-none-elf-g++ $(CPPFLAGS) -c $< -o $@

kernel8.img: $(AllOBJS)

   @echo "============================================================================="
   @echo "Linking..."
   @aarch64-none-elf-ld -o kernel8.elf -Map kernel8.map -nostdlib \
       --section-start=.init=$(LOADADDR) --no-warn-rwx-segments \
       -g -T linker.ld $(AllOBJS)
   aarch64-none-elf-objcopy -O binary kernel8.elf kernel8.img

clean:

   /bin/rm -f kernel8.elf kernel8.map *.o *.img > /dev/null 2> /dev/null || true

new:

   /bin/clear

Speichern Sie diese Datei unter dem Namen Makefile. Wenn Sie mehr über den Inhalt erfahren möchten, schauen Sie unter "Arbeiten mit Make und Linker-Script" nach.

Linker-Script (linker.ld) Erstellen Sie das folgende Linker-Script, welches wir unter "Arbeiten mit Make und Linker-Script" beschrieben haben:

LINKER Copy code ENTRY(_start)

SECTIONS {

   .init : {
       *(.init)
   }
   .text : {
       *(.text*)
       _etext = .;
   }
   .rodata : {
       *(.rodata*)
   }
   .init_array : {
       __init_start = .;
       KEEP(*(.init_array*))
       __init_end = .;
   }
   .ARM.exidx : {
       __exidx_start = .;
       *(.ARM.exidx*)
       __exidx_end = .;
   }
   .eh_frame : {
       *(.eh_frame*)
   }
   .data : {
       *(.data*)
   }
   .bss : {
       __bss_start = .;
       *(.bss*)
       *(COMMON)
       __bss_end = .;
   }

} __bss_size = (__bss_end - __bss_start) >> 3; Speichern Sie diese Datei unter dem Namen linker.ld.

Kompilieren und Ausführen Nun können wir unser erstes Programm kompilieren:

SH Copy code make Damit unser Kernel funktionsfähig ist, muss der Kernel auf eine SD-Karte, die in FAT32 formatiert ist, kopiert werden. Zusätzlich benötigt der Raspberry Pi 4 folgende Dateien auf der SD-Karte:

bootcode.bin start.elf config.txt kernel8.img (Ihr kompiliertes Programm) Den Source-Code können Sie als ZIP-Datei hier herunterladen.



MS Matthias 11:14 AM • Today Überprüfe den ersten Teil des Text auf Inhalt und ändere es für einen Tutor: Lass die LED leuchten in C (PI4)

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":

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // boot.S //

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

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

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // config.h //

  1. ifndef _config_h
  2. define _config_h
  1. define MEGABYTE 0x100000
  1. define MEM_KERNEL_START 0x80000 // Startadresse des Hauptprogramms
  2. define KERNEL_MAX_SIZE (2 * MEGABYTE)
  3. define MEM_KERNEL_END (MEM_KERNEL_START + KERNEL_MAX_SIZE)
  4. define KERNEL_STACK_SIZE 0x20000
  5. define MEM_KERNEL_STACK (MEM_KERNEL_END + KERNEL_STACK_SIZE)
  1. endif

Erklärung:

  1. ifndef _config_h ... #endif: Verhindert, dass die Datei mehrfach inkludiert wird.
  2. 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:

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // 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:

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // kernel.c //

  1. include "led.h"
  2. include "time.h"

int main (void) {

 while(1)
 {
   LED_off();
   wait(0x1F0000);
   LED_on();
   wait(0x1F0000);
 }

} Erklärung:

while(1): Erzeugt eine Endlosschleife, da 1 immer wahr ist. LED_off(): Schaltet die LED aus. wait(0x1F0000): 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 4 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 Auf dem Raspberry Pi 4 können wir auf die GPIO-PINs direkt einwirken, die der Raspberry Pi für die Kommunikation mit der Außenwelt verwendet werden können. Idealerweise ist ein PIN mit einer LED, die auf dem Board verbaut ist, verbunden, so dass wir zunächst auf externe Hardware verzichten können und effektiv ein Ergebnis zu sehen. Unter den Baremetal-Programmieren kommt dies einem “HelloWorld” Programm am nächsten. Damit wir diese LED blinken lassen können, müssen wir auf diese GPIO-Pins zugreifen.

Der Raspberry Pi besitzt einige GPIO, die gerade für Hardware Freunde interessant sind, da diese einfach zu programmieren sind und sich gut mit der Außenwelt verbinden lassen. Auf dem Raspberry Pi 4 gibt es 40 GPIO-Pins, von denen 26 Pins als Ausgang oder Eingang programmiert werden können.

Zusätzlich gibt es noch weitere interne GPIO-Adressen, die auch über die Programmierschnittstelle programmiert werden können. Eine davon ist die grüne LED, die über den Port 42 auf dem Raspberry Pi 4 angesteuert werden kann.

Zunächst müssen wir dem Raspberry mitteilen, was wir vorhaben. Dazu müssen wir dem Register GPFSEL mitteilen, wie er seine Pins verwendet soll. Laut Dokumentation im Kapitel 5 gibt es hierfür fünf GPFSEL Register, die für verschiedene GPIO-Adressen zuständig sind. GPFSEL0 ist für die GPIO-Adressen 0-9, GPFSEL1 für 10-19 usw. zuständig. Da unsere LED über GPIO-Adresse 42 angesprochen wird, müssen wir GPFSEL4 verwenden. Innerhalb des Registers hat jede GPIO-Adresse 3 Bits zur Verfügung um deren Funktion zu definieren oder aber auch abfragen zu können. Diese drei Bits sind wie folgt definiert:

Status des GPIO-Registers 000 PIN ist ein Eingang 001 PIN ist ein Ausgang 100 PIN hat die alternative Funktion 0 101 PIN hat die alternative Funktion 1 110 PIN hat die alternative Funktion 2 111 PIN hat die alternative Funktion 3 011 PIN hat die alternative Funktion 4 010 PIN hat die alternative Funktion 5 Als Status werden nur zwei Zustände fest angegeben. Dies sind die Status, den Pin entweder als Eingang oder als Ausgang zu definieren. Alle anderen Status werden einer alternativen Funktion zugeschrieben. Hierbei bedeutet die alternative Funktion des einen PINs nicht die gleiche Funktion des anderen Bits. Dazu kann in der Dokumentation im Kapitel 5.3 “Alternative Function Assignments” geschaut werden, welche es da gibt. Später, werden wir diese auch verwenden. Da wir nun eine LED ansteuern wollen, müssen wir hier die Funktion “001 - PIN ist ein Ausgang” verwenden. Im Register GPFSEL4 sind die drei Bits 8-6 zuständig für den Pin 42. Also hier müssen wir die Bitfolge “001” eintragen. Eine Möglichkeit wäre, diesen Bitcode einfach direkt reinzuschreiben “0b001000000”, aber da wir etwas in Zukunft blicken verwenden wir eine Funktion, deren wir einfach den GPIO-Pin und die Funktion übergeben.

void SetGPIOFunction(u32 Pin, u32 Function) {

 u32 GPSEL = GPIO_GPFSEL0;
 while (Pin >= 10)
 {
   Pin = Pin -10;
   GPSEL = GPSEL + 4;
 }
 Pin = Pin *3;
 Function = Function << Pin;
 u32 mask = 0b111 << Pin;
 mask =~mask; //Bitweise negieren
 u32 sel=read32(GPSEL);
 sel=sel&mask;
 sel=sel|Function;
 write32(GPSEL,sel);

} Diese Funktion lädt zunächst die "Basis"-Adresse für GPIO_GPFSEL0 nach GPSEL. Da jeweils 10 Pins mit jeder Adresse GPIO_GPFSEL verwendet werden, müssen wir zunächst feststellen, welches das richtige Register ist. Wir machen es uns recht einfach, in dem wir einfach eine Schleife generieren, die jeweils die Übergabe der Pin-Nummer prüft, bis diese <= 10 ist. Solange dies nicht ist, wird einfach der Pin um 10 reduziert und die "Basis-Adresse" um 4 erhöht, was dann beim ersten Durchlauf dann auf GPIO_GPFSEL1 zeigt usw...

Da die "Funktion" der Pins jeweils drei Bits hat, multiplizieren wir das Ergebnis des Pins mit drei. Zunächst lesen wir den Inhalt raus und löschen die entsprechenden Bits und setzen dann den Wert aus der Übergabe "Funktion" dort hinein. Somit ist sichergestellt, das andere Pins nicht verändert werden. Diesen Wert schreiben wir dann ins GPIO_GPFSEL-Register.

Kommen wir zur Steuerung der LED:

void LED_on (void) {

 u32 LED_Pin = 42;
 SetGPIOFunction(LED_Pin,GPIO_output);
 u32 GPSET = GPIO_GPSET0; 
 if (LED_Pin>31)
 {
   GPSET = GPSET + 4;
   LED_Pin = LED_Pin - 32;
 }
 write32(GPSET,1<<LED_Pin);

}

void LED_off (void) {

 u32 LED_Pin = 42;
 SetGPIOFunction(LED_Pin,GPIO_output);
 u32 GPCLR = GPIO_GPCLR0; 
 if (LED_Pin>31)
 {
   GPCLR = GPCLR + 4;
   LED_Pin = LED_Pin - 32;
 }
 write32(GPCLR,1<<LED_Pin);

} Wir erstellen zwei Funktionen, eine zum Ausschalten der LED (LED_off) und eine, um die LED anzuschalten. Vom Grunde her sind diese zwei Funktion fast identisch. Zunächst wird der entsprechende PIN (RPI4 -> 42) festgelegt. Dies übergeben wir der Funktion SetGPIOFunction und das es sich hierbei um einen "Output" handelt. Das Register, welches wir hier nun benötigen ist zum anschalten der LED "GPIO_GPSET" und zum Ausschalten "GPIO_GPCLR". Jedes Register kann maximal 32 Pins bedienen, sodass wir zunächst festellen müssen, welches Register das richtige ist. Da wir hier den festen Wert 42 haben, könnten wir auch direkt auf GPIO_GPSET1 und GPIO_GPCLR1 schreiben, aber es soll verdeutlichen, wie wir flexibel auf die Anforderungen eingehen können. Nun können wir einfach das "BIT" an die richtige Position schreiben und die LED geht an oder aus.


Received

RocheChat 11:14 AM • Today Hier ist der überarbeitete Text, angepasst für einen Tutor:

Lass die LED leuchten in C (für den Raspberry Pi 4) Einführung Unser Ziel ist es, die fest eingebaute LED des Raspberry Pi 4 zum Blinken zu bringen. Ich werde jeden Schritt erklären und die Gründe für die implementierten Schritte näher erläutern.

Vorbereitung des Verzeichnisses Erstellen Sie zunächst ein neues Verzeichnis, z.B. LED, und platzieren Sie darin das Makefile und die Datei linker.ld. Erzeugen Sie auch das Verzeichnis include innerhalb von LED, um unsere Header-Dateien zu organisieren.

Programmierung des Startverhaltens des Raspberry Pi Leider kommen wir nicht ganz ohne Assembler aus. Der erste Code, der ausgeführt wird, sollte in Assembler geschrieben sein. In der Regel werden einige Eigenschaften der CPU definiert, die nur in Assembler möglich sind. Für dieses Beispiel benötigen wir das noch nicht, aber wir erstellen diese Datei, um für spätere Projekte vorbereitet zu sein. Erstellen Sie die Datei boot.S:

ASSEMBLY Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // boot.S //

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

  1. include "config.h": Inkludiert die Konfigurationsdatei, in der benötigte Parameter definiert sind.

.section .init: Definiert einen Abschnitt, der am Anfang des Kernel-Images platziert wird. .global _start: Deklariert _start als global, damit es von allen Teilen des Programms 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. mov sp, x0: Initialisiert den Stack-Zeiger (SP). b sysinit: Springt zur Funktion sysinit, die später implementiert wird. Wenn das Programm startet, wird zunächst mit ldr (Load Register) das Register x0 mit dem Wert aus MEM_KERNEL_STACK belegt, der in der Include-Datei definiert wurde, und anschließend in den Stack-Zeiger (SP) abgelegt. Mit b (Branch) wird der Code zum Label sysinit verzweigt.

Konfigurationsdatei config.h (eine Header-Datei) Bedeutung und Struktur von Header-Dateien Unsere config.h ist eine Header-Datei. Header-Dateien werden verwendet, um Code zu organisieren und wiederverwendbar zu machen. Hauptgründe sind:

Strukturierung: Header-Dateien helfen dabei, den Code in kleinere, übersichtliche Teile zu gliedern. Wiederverwendbarkeit: Einmal geschriebener Code kann in mehreren Programmen oder Dateien verwendet werden. Vereinfachung: Macht den Code übersichtlicher und leichter verständlich. Vermeidung von Doppelarbeit: Änderungen müssen nur in der Header-Datei vorgenommen werden. Unsere config.h ist wie folgt beschrieben:

C Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // config.h //

  1. ifndef _config_h
  2. define _config_h
  1. define MEGABYTE 0x100000
  1. define MEM_KERNEL_START 0x80000 // Startadresse des Hauptprogramms
  2. define KERNEL_MAX_SIZE (2 * MEGABYTE)
  3. define MEM_KERNEL_END (MEM_KERNEL_START + KERNEL_MAX_SIZE)
  4. define KERNEL_STACK_SIZE 0x20000
  5. define MEM_KERNEL_STACK (MEM_KERNEL_END + KERNEL_STACK_SIZE)
  1. endif

Erklärung:

  1. ifndef _config_h ... #endif: Verhindert, dass die Datei mehrfach inkludiert wird.
  2. 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. Die #ifndef-Anweisung (if not defined) stellt sicher, dass die Datei nur einmal verarbeitet wird. Danach wird die Variable mit #define _config_h definiert und ist dem Compiler bekannt.

Funktion sysinit Erstellen Sie die Funktion sysinit, die das System initialisiert:

ASSEMBLY Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // 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. Das Label sysinit wird global definiert, damit es auch außerhalb der Datei bekannt ist. Der einzige Befehl b main bedeutet, dass das Programm einen Sprung zum Label main macht. Später werden wir hier mehr Code einfügen.

Hauptprogramm main Erstellen Sie nun das Hauptprogramm:

C Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // kernel.c //

  1. include "led.h"
  2. include "time.h"

int main (void) {

 while(1)
 {
   LED_off();
   wait(0x1F0000);
   LED_on();
   wait(0x1F0000);
 }

} Erklärung: while(1): Erzeugt eine Endlosschleife. LED_off(): Schaltet die LED aus. wait(0x1F0000): Ruft die Wartefunktion auf. LED_on(): Schaltet die LED ein. Mit int main (void) erstellen wir das Label, welches von sysinit aufgerufen wird. Die Endlosschleife while(1) bedeutet, dass der Code innerhalb der Schleife fortlaufend ausgeführt wird. LED_off(), wait(), LED_on() und erneut wait() wiederholen den Zyklus unendlich.

LED-Steuerungsfunktionen Auf dem Raspberry Pi 4 können wir auf die GPIO-PINs direkt zugreifen, die für die Kommunikation mit der Außenwelt verwendet werden. Da eine PIN mit einer fest eingebauten LED verbunden ist, können wir auf externe Hardware verzichten.

Zugriff auf die GPIO-Pins Der Raspberry Pi 4 hat 40 GPIO-Pins, von denen 26 als Ausgang oder Eingang programmiert werden können. Zusätzlich gibt es interne GPIO-Adressen, die per Programmierschnittstelle programmiert werden können. Eine davon ist die grüne LED, die über den Port 42 angesteuert wird.

Konfiguration des GPIO-Registers Um die LED zu steuern, müssen wir das Register GPFSEL verwenden. Das Register GPFSEL4 ist für GPIO-Adresse 42 zuständig und jede GPIO-Adresse wird durch 3 Bits konfiguriert:

C Copy code

  1. define GPIO_input 0b000
  2. define GPIO_output 0b001
  3. define GPIO_alt0 0b100
  4. define GPIO_alt1 0b101
  5. define GPIO_alt2 0b110
  6. define GPIO_alt3 0b111
  7. define GPIO_alt4 0b011
  8. define GPIO_alt5 0b010

Funktion zur Konfiguration von GPIO-Pins C Copy code void SetGPIOFunction(u32 Pin, u32 Function) {

 u32 GPSEL = GPIO_GPFSEL0;
 while (Pin >= 10)
 {
   Pin -= 10;
   GPSEL += 4;
 }
 Pin *= 3;
 Function <<= Pin;
 u32 mask = 0b111 << Pin;
 mask = ~mask; // Bitweise negieren
 u32 sel = read32(GPSEL);
 sel &= mask;
 sel |= Function;
 write32(GPSEL, sel);

} Erklärung: Diese Funktion bestimmt das richtige GPFSEL-Register und setzt die entsprechende Funktion für den Pin. Zunächst wird die Basis-Adresse für GPIO_GPFSEL0 geladen, und durch eine Schleife die richtige Registeradresse und Pin-Position ermittelt. Der Inhalt des Registers wird gelesen, die entsprechenden Bits gelöscht und mit der neuen Funktion überschrieben.

Funktionen zur Steuerung der LED C Copy code void LED_on(void) {

 u32 LED_Pin = 42;
 SetGPIOFunction(LED_Pin, GPIO_output);
 u32 GPSET = GPIO_GPSET0;
 if (LED_Pin > 31)
 {
   GPSET += 4;
   LED_Pin -= 32;
 }
 write32(GPSET, 1 << LED_Pin);

}

void LED_off(void) {

 u32 LED_Pin = 42;
 SetGPIOFunction(LED_Pin, GPIO_output);
 u32 GPCLR = GPIO_GPCLR0;
 if (LED_Pin > 31)
 {
   GPCLR += 4;
   LED_Pin -= 32;
 }
 write32(GPCLR, 1 << LED_Pin);

} Erklärung: Wir erstellen zwei Funktionen, LED_on und LED_off, um die LED ein- und auszuschalten. Beide Funktionen setzen den entsprechenden Pin (42) auf "Output", ermitteln das richtige GPSET oder GPCLR-Register und setzen bzw. löschen das entsprechende Bit, um die LED zu steuern.

Dieser Text sollte nun ausführlicher und verständlicher für einen Tutor sein.



MS Matthias 12:41 PM • Today Hier der vortgesetzte Text. Es folgt noch ein dritter Teil. Prüfe den Text auf Inhalt und ändere es für einen Tutor. Beschreibe auch deine Änderung: Basiskonfigurationsdatei base.h Die Datei base.h enthält Basisadressen für den Raspberry Pi:

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // base.h //

  1. ifndef _base_h
  2. define _base_h
  1. define RPI_BASE 0xFE000000

// General Purpose I/O (GPIO)

  1. define GPIO_BASE RPI_BASE + 0x200000
  2. define GPIO_GPFSEL0 GPIO_BASE + 0x00 // GPIO Function Select 0
  3. define GPIO_GPFSEL1 GPIO_BASE + 0x04 // GPIO Function Select 1
  4. define GPIO_GPFSEL2 GPIO_BASE + 0x08 // GPIO Function Select 2
  5. define GPIO_GPFSEL3 GPIO_BASE + 0x0c // GPIO Function Select 3
  6. define GPIO_GPFSEL4 GPIO_BASE + 0x10 // GPIO Function Select 4
  7. define GPIO_GPFSEL5 GPIO_BASE + 0x14 // GPIO Function Select 5
  8. define GPIO_GPSET0 GPIO_BASE + 0x1c // GPIO Pin Output Set 0
  9. define GPIO_GPSET1 GPIO_BASE + 0x20 // GPIO Pin Output Set 1
  10. define GPIO_GPCLR0 GPIO_BASE + 0x28 // GPIO Pin Output Clear 0
  11. define GPIO_GPCLR1 GPIO_BASE + 0x2c // GPIO Pin Output Clear 1
  12. define GPIO_GPLEV0 GPIO_BASE + 0x34 // GPIO Pin Level 0
  13. define GPIO_GPLEV1 GPIO_BASE + 0x38 // GPIO Pin Level 1
  14. define GPIO_GPEDS0 GPIO_BASE + 0x40 // GPIO Pin Event Detect Status 0
  15. define GPIO_GPEDS1 GPIO_BASE + 0x44 // GPIO Pin Event Detect Status 1
  16. define GPIO_GPREN0 GPIO_BASE + 0x4c // GPIO Pin Rising Edge Detect Enable 0
  17. define GPIO_GPREN1 GPIO_BASE + 0x50 // GPIO Pin Rising Edge Detect Enable 1
  18. define GPIO_GPFEN0 GPIO_BASE + 0x58 // GPIO Pin Falling Edge Detect Enable 0
  19. define GPIO_GPFEN1 GPIO_BASE + 0x5c // GPIO Pin Falling Edge Detect Enable 1
  20. define GPIO_GPHEN0 GPIO_BASE + 0x64 // GPIO Pin High Detect Enable 0
  21. define GPIO_GPHEN1 GPIO_BASE + 0x68 // GPIO Pin High Detect Enable 1
  22. define GPIO_GPLEN0 GPIO_BASE + 0x70 // GPIO Pin Low Detect Enable 0
  23. define GPIO_GPLEN1 GPIO_BASE + 0x74 // GPIO Pin Low Detect Enable 1
  24. define GPIO_GPAREN0 GPIO_BASE + 0x7c // GPIO Pin Async. Rising Edge Detect 0
  25. define GPIO_GPAREN1 GPIO_BASE + 0x80 // GPIO Pin Async. Rising Edge Detect 1
  26. define GPIO_GPAFEN0 GPIO_BASE + 0x88 // GPIO Pin Async. Falling Edge Detect 0
  27. define GPIO_GPAFEN1 GPIO_BASE + 0x8c // GPIO Pin Async. Falling Edge Detect 1
  28. define GPIO_GPIO_PUP_PDN_CNTRL_REG0 GPIO_BASE + 0xe4 // GPIO Pull-up / Pull-down Register 0
  29. define GPIO_GPIO_PUP_PDN_CNTRL_REG1 GPIO_BASE + 0xe8 // GPIO Pull-up / Pull-down Register 1
  30. define GPIO_GPIO_PUP_PDN_CNTRL_REG2 GPIO_BASE + 0xec // GPIO Pull-up / Pull-down Register 2
  31. define GPIO_GPIO_PUP_PDN_CNTRL_REG3 GPIO_BASE + 0xf0 // GPIO Pull-up / Pull-down Register 3

// Information from BCM2835 ARM Peropherals

  1. define GPIO_GPPUD GPIO_BASE + 0x94 // GPIO Pin Pull-up/down Enable
  2. define GPIO_GPPUDCLK0 GPIO_BASE + 0x98 // GPIO Pin Pull-up/down Enable Clock 0
  3. define GPIO_GPPUDCLK1 GPIO_BASE + 0x9c // GPIO Pin Pull-up/down Enable Clock 1
  1. endif

In diesem Header wurden alle Funktionen (Adressen), die GPIO betreffen, definiert.

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 = 0xFE000000 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 GPIO_BASE RPI_BASE + 0x200000 wird die Basisadresse des GPIO festgelegt.

read32 und write32 Wir haben hier zwei Funktionen verwendet, die direkt mit dem Speicher des Raspberry Pi 4 arbeiten können. Am einfachsten ist es, wenn wir diese zwei Funktionen in Assembler definieren, da wir hier viel einfacher auf die Hardware zugreifen können.

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // 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

Wir speichern es als util.S ab.

Bei "write32" ist die Zeile str w1,[x0] (store) relevant. Dies schreibt den zweiten Wert an die Adresse des ersten Wertes, welches der Funktion übergeben wird. Alle anderen Zeilen sind für das korrekte Funktionieren mit C relevant, hat aber als solches für uns zunächst keine Bedeutung. Bei "read32" ist die Zeile ldr w0,[x0] (load) relevant. Hier wird der übergeben Wert als Adresse interpretiert und der Wert, der dort abgelegt ist, der Funktion zurückgegeben.

Damit "C" weiß, welche Werte hier erwartet werden, müssen wir noch in einer Header-Datei festlegen, was wir erwarten:

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // util.h //

  1. ifndef _ms_util_h
  2. define _ms_util_h
  1. include "types.h"

void write32(u32 a, u32 b); u32 read32(u32 a);

  1. endif

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

// // The LED program for RPI4 // 20.02.2025 www.satyria.de // // time.c //

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



RocheChat 12:41 PM • Today Hier ist der überarbeitete Text, angepasst für einen Tutor:

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

C Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // base.h //

  1. ifndef _base_h
  2. define _base_h
  1. define RPI_BASE 0xFE000000

// General Purpose I/O (GPIO)

  1. define GPIO_BASE (RPI_BASE + 0x200000)
  2. define GPIO_GPFSEL0 (GPIO_BASE + 0x00) // GPIO Function Select 0
  3. define GPIO_GPFSEL1 (GPIO_BASE + 0x04) // GPIO Function Select 1
  4. define GPIO_GPFSEL2 (GPIO_BASE + 0x08) // GPIO Function Select 2
  5. define GPIO_GPFSEL3 (GPIO_BASE + 0x0c) // GPIO Function Select 3
  6. define GPIO_GPFSEL4 (GPIO_BASE + 0x10) // GPIO Function Select 4
  7. define GPIO_GPFSEL5 (GPIO_BASE + 0x14) // GPIO Function Select 5
  8. define GPIO_GPSET0 (GPIO_BASE + 0x1c) // GPIO Pin Output Set 0
  9. define GPIO_GPSET1 (GPIO_BASE + 0x20) // GPIO Pin Output Set 1
  10. define GPIO_GPCLR0 (GPIO_BASE + 0x28) // GPIO Pin Output Clear 0
  11. define GPIO_GPCLR1 (GPIO_BASE + 0x2c) // GPIO Pin Output Clear 1
  12. define GPIO_GPLEV0 (GPIO_BASE + 0x34) // GPIO Pin Level 0
  13. define GPIO_GPLEV1 (GPIO_BASE + 0x38) // GPIO Pin Level 1
  14. define GPIO_GPEDS0 (GPIO_BASE + 0x40) // GPIO Pin Event Detect Status 0
  15. define GPIO_GPEDS1 (GPIO_BASE + 0x44) // GPIO Pin Event Detect Status 1
  16. define GPIO_GPREN0 (GPIO_BASE + 0x4c) // GPIO Pin Rising Edge Detect Enable 0
  17. define GPIO_GPREN1 (GPIO_BASE + 0x50) // GPIO Pin Rising Edge Detect Enable 1
  18. define GPIO_GPFEN0 (GPIO_BASE + 0x58) // GPIO Pin Falling Edge Detect Enable 0
  19. define GPIO_GPFEN1 (GPIO_BASE + 0x5c) // GPIO Pin Falling Edge Detect Enable 1
  20. define GPIO_GPHEN0 (GPIO_BASE + 0x64) // GPIO Pin High Detect Enable 0
  21. define GPIO_GPHEN1 (GPIO_BASE + 0x68) // GPIO Pin High Detect Enable 1
  22. define GPIO_GPLEN0 (GPIO_BASE + 0x70) // GPIO Pin Low Detect Enable 0
  23. define GPIO_GPLEN1 (GPIO_BASE + 0x74) // GPIO Pin Low Detect Enable 1
  24. define GPIO_GPAREN0 (GPIO_BASE + 0x7c) // GPIO Pin Async. Rising Edge Detect 0
  25. define GPIO_GPAREN1 (GPIO_BASE + 0x80) // GPIO Pin Async. Rising Edge Detect 1
  26. define GPIO_GPAFEN0 (GPIO_BASE + 0x88) // GPIO Pin Async. Falling Edge Detect 0
  27. define GPIO_GPAFEN1 (GPIO_BASE + 0x8c) // GPIO Pin Async. Falling Edge Detect 1
  28. define GPIO_GPIO_PUP_PDN_CNTRL_REG0 (GPIO_BASE + 0xe4) // GPIO Pull-up / Pull-down Register 0
  29. define GPIO_GPIO_PUP_PDN_CNTRL_REG1 (GPIO_BASE + 0xe8) // GPIO Pull-up / Pull-down Register 1
  30. define GPIO_GPIO_PUP_PDN_CNTRL_REG2 (GPIO_BASE + 0xec) // GPIO Pull-up / Pull-down Register 2
  31. define GPIO_GPIO_PUP_PDN_CNTRL_REG3 (GPIO_BASE + 0xf0) // GPIO Pull-up / Pull-down Register 3

// Information from BCM2835 ARM Peripherals

  1. define GPIO_GPPUD (GPIO_BASE + 0x94) // GPIO Pin Pull-up/down Enable
  2. define GPIO_GPPUDCLK0 (GPIO_BASE + 0x98) // GPIO Pin Pull-up/down Enable Clock 0
  3. define GPIO_GPPUDCLK1 (GPIO_BASE + 0x9c) // GPIO Pin Pull-up/down Enable Clock 1
  1. endif

Erklärung: In dieser Header-Datei wurden alle Funktionen (Adressen) definiert, die GPIO betreffen. Diese Datei wird im Verlauf unseres Kurses kontinuierlich erweitert; aktuell benötigen wir jedoch nur die Daten für unsere LED.

Mit #define RPI_BASE 0xFE000000 legen wir die Basisadresse der Peripheriegeräte fest. Dies dient als Grundlage für alle anderen Geräte, die wir verwenden. #define GPIO_BASE (RPI_BASE + 0x200000) legt die Basisadresse des GPIO fest.

read32 und write32 Diese zwei Funktionen ermöglichen direkten Zugriff auf den Speicher des Raspberry Pi 4. Wir definieren diese Funktionen in Assembler, um einfacher auf die Hardware zugreifen zu können.

ASSEMBLY Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // 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

Speichern Sie die Datei als util.S.

Erklärung: write32 nutzt die Zeile str w1,[x0] (store), um den zweiten Wert an die Adresse des ersten Wertes zu schreiben. read32 nutzt die Zeile ldr w0,[x0] (load), um den Wert an der übergebenen Adresse zu laden und zurückzugeben. Damit C weiß, welche Werte hier erwartet werden, müssen wir in einer Header-Datei festlegen, was wir erwarten:

C Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // util.h //

  1. ifndef _ms_util_h
  2. define _ms_util_h
  1. include "types.h"

void write32(u32 a, u32 b); u32 read32(u32 a);

  1. endif

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

C Copy code // // The LED program for RPI4 // 20.02.2025 www.satyria.de // // time.c //

  1. 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 die Schleife bei der Code-Optimierung nicht optimiert. for (i = 0; i < zyklen; i++): Erzeugt eine Schleife und zählt i hoch, solange i kleiner als zyklen ist. {...}: Leere Schleife zur Verzögerung.

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 den Code duplizieren zu müssen.
  • 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

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// led.h
//

#ifndef _ms_led_h
#define _ms_led_h

#include "types.h"

#define GPIO_input  0b000      // Input
#define GPIO_output 0b001      // Output
#define GPIO_alt0   0b100      // Alternate function 0
#define GPIO_alt1   0b101      // Alternate function 1
#define GPIO_alt2   0b110      // Alternate function 2
#define GPIO_alt3   0b111      // Alternate function 3
#define GPIO_alt4   0b011      // Alternate function 4
#define GPIO_alt5   0b010      // Alternate function 5

#define GPPUD_OFF    0b00      // Pull-up/down disable

void LED_off(void);
void LED_on(void);
void SetGPIOFunction(u32 Pin, u32 Function);

#endif

Erklärung

Diese Header-Datei definiert die Funktionsaufrufe LED_off, LED_on und SetGPIOFunction. Die Funktionsaufrufe LED_off und LED_on erwarten keine Parameter und geben keine Parameter zurück, was durch das Schlüsselwort void erkennbar ist. SetGPIOFunction erwartet den PIN und die Funktion als u32-Wert.

Die time.h

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// time.h
//

#ifndef _ms_time_h
#define _ms_time_h

#include "types.h"

void wait(u32 zyklen);

#endif

Erklärung

Hier wird der Funktionsaufruf wait definiert. Diese Funktion gibt keinen Wert zurück, erwartet aber einen u32-Wert als Parameter. Der Typ u32 wird im nächsten Header types.h definiert. Deswegen wurde diese Datei inkludiert, damit der Compiler weiß, was u32 bedeutet.

Die types.h

//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// 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

Erklärung

Der Header types.h definiert verschiedene Datentypen und einige Makros, die in einem C-Programm verwendet werden können:

  • Typdefinitionen:
    • 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:
      • Ein neuer Typ boolean als Alias für char
  • 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
  • Boolean Values:
    • FALSE und TRUE werden als 0 und 1 definiert

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

Wechseln Sie in das Verzeichnis LED und kompilieren Sie das Programm mit dem Befehl make. Wenn alles erfolgreich war, erhalten Sie eine Datei kernel8.img, die auf eine SD-Karte kopiert und im Raspberry Pi verwendet wird. Schalten Sie den Raspberry Pi ein, und die LED sollte blinken.

Sie können den Source-Code als ZIP-Datei mit folgendem Link herunterladen: https://www.satyria.de/arm/sources/RPI4/C/2.zip


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