Professionelle GUI mit Debugging für Bare-Metal (64-Bit) auf dem Raspberry Pi 4
Viele Entwickler wünschen sich eine professionelle Möglichkeit, die Bare-Metal-Entwicklung direkt in einer grafischen Benutzeroberfläche (GUI) zu realisieren. In dieser Anleitung zeige ich Ihnen, wie Sie eine solche Entwicklungsumgebung aufsetzen können. Als IDE (Integrated Development Environment) nutzen wir Visual Studio Code von Microsoft, das unter der Open-Source-Lizenz MIT veröffentlicht ist.
Der Raspberry Pi unterstützt für solche Kommunikation das JTAG-Protokoll. Leider können Hostsysteme dieses Protokoll selten direkt bereitstellen, weshalb wir auf zusätzliche Hardware angewiesen sind. Eine günstige Option ist das "CJMCU FT232H Modul" (https://amzn.eu/d/hb8tKuA), welches ich für diese Anleitung verwendet habe.
Software-Voraussetzungen
Wie eingangs erwähnt, nutzen wir als GUI Visual Studio Code. Die Software kann kostenlos von der offiziellen Website heruntergeladen werden:
👉 https://code.visualstudio.com/
Zusätzlich benötigen wir die offizielle Toolchain von ARM, um Code für die AArch64-Architektur des Raspberry Pi 5 kompilieren zu können. Diese steht hier zum Download bereit:
Hinweis: Zum Zeitpunkt der Erstellung dieses Tutorials wurde die Version arm-gnu-toolchain-15.2.rel1-mingw-w64-x86_64-aarch64-none-elf.zip verwendet. Da die Toolchain kontinuierlich weiterentwickelt wird, kann sich die Versionsnummer bei Ihrem Download leicht unterscheiden.
Für die Verbindung zwischen dem PC und dem FT232H Modul verwenden wir OpenOCD (Open On-Chip Debugger). Eine vorkompilierte Windows-Version wird über das xPack-Projekt bereitgestellt:
Direkter Download-Link der verwendeten Version: xPack OpenOCD v0.12.0-7
Zuletzt benötigen wir noch das Build-Werkzeug GNU Make, welches wir im nächsten Schritt direkt über die Windows-Konsole installieren.
Installation und Einrichtung
Toolchain einrichten
Da Windows gelegentlich Probleme mit sehr langen Pfadnamen haben kann, benennen wir das heruntergeladene Archiv der Toolchain zunächst in toolchain.zip um.
Entpacken Sie das ZIP-Archiv vollständig.
Erstellen Sie ein neues Verzeichnis direkt auf Ihrem Systemlaufwerk: C:\tools.
Kopieren Sie den gesamten Inhalt des entpackten Toolchain-Ordners in dieses Verzeichnis, sodass die Ordnerstruktur direkt mit C:\tools\bin beginnt.
OpenOCD einrichten und konfigurieren
Entpacken Sie das OpenOCD-ZIP-Archiv.
Im entpackten Ordner finden Sie das Unterverzeichnis xpack-openocd-0.12.0-7 (bzw. Ihre aktuellere Version). Benennen Sie diesen Ordner einfach in openocd um.
Verschieben Sie den gesamten Ordner openocd nach C:\tools, sodass der Pfad C:\tools\openocd entsteht.
Leider fehlt uns eine Konfigurationsdatei, die JTAG und FT232H beschreibt. Daher erstellen wir eine Datei namens ft232h-jtag.cfg mit folgendem Inhalt und speichern diese ins Verzeichnis C:\tools\openocd\openocd\scripts\interface:
adapter speed 3000
adapter driver ftdi
ftdi vid_pid 0x0403 0x6014
ftdi layout_init 0x0078 0x017b
ftdi_tdo_sample_edge falling
ftdi layout_signal nTRST -ndata 0x0010 -noe 0x0040
ftdi layout_signal nSRST -ndata 0x0020 -noe 0x0040
transport select jtagZusätzlich benötigen wir ein Target-Profil für den Broadcom-Chip BCM2711 des Raspberry Pi 4. Erstellen Sie dazu eine neue Datei unter dem Pfad C:\tools\openocd\openocd\scripts\target\bcm2711.cfg und fügen Sie folgenden Inhalt ein:
# SPDX-License-Identifier: GPL-2.0-or-later
# The Broadcom BCM2711 used in Raspberry Pi 4
# No documentation was found on Broadcom website
# Partial information is available on the Raspberry Pi website:
# https://www.raspberrypi.com/documentation/computers/processors.html#bcm2711
if { [info exists CHIPNAME] } {
set _CHIPNAME $CHIPNAME
} else {
set _CHIPNAME bcm2711
}
if { [info exists CHIPCORES] } {
set _cores $CHIPCORES
} else {
set _cores 4
}
if { [info exists USE_SMP] } {
set _USE_SMP $USE_SMP
} else {
set _USE_SMP 0
}
if { [info exists DAP_TAPID] } {
set _DAP_TAPID $DAP_TAPID
} else {
set _DAP_TAPID 0x4ba00477
}
jtag newtap $_CHIPNAME cpu -expected-id $_DAP_TAPID -irlen 4
adapter speed 4000
dap create $_CHIPNAME.dap -chain-position $_CHIPNAME.cpu
# MEM-AP for direct access
target create $_CHIPNAME.ap mem_ap -dap $_CHIPNAME.dap -ap-num 0
# these addresses are obtained from the ROM table via 'dap info 0' command
set _DBGBASE {0x80410000 0x80510000 0x80610000 0x80710000}
set _CTIBASE {0x80420000 0x80520000 0x80620000 0x80720000}
set _smp_command "target smp"
for { set _core 0 } { $_core < $_cores } { incr _core } {
set _CTINAME $_CHIPNAME.cti$_core
set _TARGETNAME $_CHIPNAME.cpu$_core
cti create $_CTINAME -dap $_CHIPNAME.dap -ap-num 0 -baseaddr [lindex $_CTIBASE $_core]
target create $_TARGETNAME aarch64 -dap $_CHIPNAME.dap -ap-num 0 -dbgbase [lindex $_DBGBASE $_core] -cti $_CTINAME
set _smp_command "$_smp_command $_TARGETNAME"
}
if {$_USE_SMP} {
eval $_smp_command
}
# default target is cpu0
targets $_CHIPNAME.cpu0GNU Make installieren
Unter Windows stellt Microsoft ein Paket für GNU Make bereit, das sich komfortabel über den integrierten Paketmanager installieren lässt. Öffnen Sie ein Terminal (Eingabeaufforderung oder PowerShell) und geben Sie folgenden Befehl ein:
winget install GnuWin32.MakeDas Programm wird standardmäßig in das Verzeichnis C:\Program Files (x86)\GnuWin32\bin installiert. Damit Make reibungslos mit unseren anderen Tools zusammenarbeitet, kopieren Sie den gesamten Inhalt dieses bin-Ordners in unser zuvor angelegtes Verzeichnis C:\tools\bin.
Umgebungsvariable „Path“ anpassen
Damit Windows und Visual Studio Code die installierten Tools (Compiler, Make und OpenOCD) systemweit über die Konsole finden können, müssen wir die ausführbaren Dateien dem Systempfad hinzufügen.
Drücken Sie die Windows-Taste und tippen Sie im Suchfeld „Umgebungsvariable“ ein.
Wählen Sie den Punkt „Systemumgebungsvariablen bearbeiten“ und klicken Sie im nächsten Fenster unten auf den Button „Umgebungsvariablen...“.
Suchen Sie im Bereich „Benutzervariablen“ oder „Systemvariablen“ nach dem Eintrag Path (oder PATH) und wählen Sie Bearbeiten.
Fügen Sie der Liste die folgenden zwei Pfade als separate, neue Zeilen hinzu:
C:\tools\bin
C:\tools\openocd\bin
C:\Program Files (x86)\GnuWin32\binBestätigen Sie alle geöffneten Fenster mit OK.
Die grundlegende Toolchain ist damit erfolgreich installiert und eingerichtet. Im nächsten Teil widmen wir uns der Konfiguration von Visual Studio Code für das eigentliche Bare-Metal-Projekt.
Visual Studio Code einrichten
Nachdem die grundlegende Toolchain installiert ist, richten wir nun die Entwicklungsumgebung in Visual Studio Code ein.
Zunächst installiere Visual Studio Code komplett aus dem vorhergehenden Download.
VS Code Erweiterungen (Extensions) installieren
Öffnen Sie Visual Studio Code. Um die Bare-Metal-Entwicklung so komfortabel wie möglich zu gestalten, installieren wir zunächst einige essenzielle Erweiterungen. Klicken Sie dazu auf das Erweiterungs-Icon auf der linken Menüleiste (oder drücken Sie STRG + UMSCHALT + X) und suchen Sie nach folgenden Extensions:
- C/C++ (von Microsoft): Bietet Syntax-Highlighting und Code-Vervollständigung (IntelliSense) für C/C++.
- C/C++ Extension Pack (optional): Bringt zusätzliche nützliche Werkzeuge für die C-Entwicklung mit.
- Cortex-Debug (von marus25): Die Schlüssel-Erweiterung, die es uns ermöglicht, via OpenOCD direkt auf dem Raspberry Pi 5 zu debuggen.
- Arm Assembly (von dan-c-underwood): Bietet exzellente Unterstützung und Highlighting für ARM-Assembler-Code.
Optional: Sprache der Benutzeroberfläche auf Deutsch umstellen
Falls Ihr VS Code englischsprachig ist und Sie zum Beispiel die deutsche Oberfläche bevorzugen:
Drücken Sie STRG + UMSCHALT + P, um die Befehlspalette zu öffnen. Tippen Sie Configure Display Language ein und drücken Sie Enter.
Wählen Sie „Deutsch“ aus (falls nicht aufgelistet, können Sie es darüber direkt nachinstallieren).
Starten Sie VS Code neu.
Projektverzeichnis erstellen und öffnen
Erstellen Sie nun ein Verzeichnis auf Ihrer Festplatte, in dem Ihr Bare-Metal-Projekt leben soll. In diesem Beispiel verwenden wir den Pfad D:\projekt1. Wählen Sie in VS Code Datei -> Ordner öffnen... und wählen Sie das Verzeichnis D:\projekt1 aus. Bestätigen Sie die Sicherheitsabfrage, ob Sie den Autoren des Ordners vertrauen, mit „Ja, ich vertraue den Autoren“.
Die .vscode Konfigurationsdateien anlegen
Damit VS Code weiß, welchen Compiler es nutzen soll und wie der Debugger gestartet wird, legen wir ein Konfigurationsverzeichnis an. Erstellen Sie im Hauptverzeichnis Ihres Projekts einen neuen Ordner mit dem exakten Namen .vscode (den Punkt am Anfang nicht vergessen!). Klicken Sie mit der rechten Maustaste auf den neu erstellten Ordner .vscode und legen Sie nacheinander die folgenden drei Dateien an.
Kopieren Sie jeweils den entsprechenden JSON-Code hinein:
- c_cpp_properties.json: Diese Datei konfiguriert die Code-Vervollständigung (IntelliSense), damit VS Code die ARM-spezifischen Befehle und Header versteht.
{
"configurations": [
{
"name": "Bare-Metal (Pi 4)",
"includePath": [
"${workspaceFolder}/include",
"${workspaceFolder}/src"
],
"compilerPath": "C:/tools/bin/aarch64-none-elf-gcc.exe",
"cStandard": "c11",
"cppStandard": "c++14",
"intelliSenseMode": "windows-gcc-arm64"
}
],
"version": 4
}
- launch.json: Diese Datei steuert den Debugger (Cortex-Debug). Sie sorgt dafür, dass Ihr Code via OpenOCD auf den Raspberry Pi 4 geladen und der Prozessor exakt an der Startadresse gestoppt wird.
{
"version": "0.2.0",
"configurations": [
{
"name": "Bare-Metal Debug (Pi 4) - FT232H",
"cwd": "${workspaceFolder}",
"executable": "${workspaceFolder}/kernel8.elf",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"searchDir": [
"C:/tools/openocd/openocd/scripts",
"${workspaceFolder}"
],
"configFiles": [
"interface/ft232h-jtag.cfg",
"target/bcm2711.cfg"
],
"gdbPath": "C:/tools/bin/aarch64-none-elf-gdb.exe",
"serverpath": "C:/tools/openocd/bin/openocd.exe",
"openOCDLaunchCommands": [
"adapter speed 3000",
"init"
],
"overrideLaunchCommands": [
"monitor halt",
"monitor wait_halt 2000",
"load",
"monitor reg pc 0x80000"
],
"preLaunchTask": "Kompilieren",
"showDevDebugOutput": "raw"
}
]
}
- tasks.json: Hier wird der automatisierte Build-Prozess definiert. Bevor der Debugger startet, wird über diese Task automatisch make aufgerufen.
{
"version": "2.0.0",
"tasks": [
{
"label": "Kompilieren",
"type": "shell",
"command": "make",
"args": ["all"],
"options": {
"env": {
"PATH": "${env:PATH};C:\\tools\\bin"
}
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"]
}
]
}
Speichern Sie alle Konfigurationen ab, indem Sie im Menü auf Datei -> Alles speichern klicken.
Important: Hinweis zu vermeintlichen Fehlermeldungen
Nach dem Speichern der c_cpp_properties.json fällt Ihnen im Datei-Explorer oder im Reiter „Probleme“ (unten in VS Code) vermutlich eine kleine rote „2“ auf. Dies deutet auf aktive Warnungen hin. Wenn Sie auf die Meldung klicken, sehen Sie den Grund: VS Code beschwert sich, dass die beiden Verzeichnis-Pfade ${workspaceFolder}/include und ${workspaceFolder}/src nicht existieren.
💡Keine Sorge: Das ist völlig korrekt! Da wir ein leeres Projekt gestartet haben, gibt es diese Ordner einfach noch nicht. Sobald Sie im weiteren Verlauf Ihres Projekts die Ordner src und include anlegen, verschwindet diese Warnung von selbst.
Tipp: Sollte VS Code neu erstellte Ordner einmal nicht sofort registrieren, können Sie die Entwicklungsumgebung schnell aktualisieren. Drücken Sie dazu F1, tippen Sie Reload Window ein und bestätigen Sie mit Enter. Das lädt die Oberfläche neu, ohne Ihre Arbeit zu unterbrechen.
Sourcecode, Linkerscript und Makefile
Das Quellcode-Verzeichnis (src/)
Um unser Setup praktisch zu testen, verwenden wir ein minimalistisches LED-Blinkbeispiel in C und Assembler (Lass die LED leuchten in C (PI4)). Dieses Projekt nutzt die Aufteilung in verschiedene Dateien, damit Sie die Querverweise im Code und den Komfort einer professionellen GUI direkt erleben können.
Erstellen Sie im Hauptverzeichnis Ihres Projekts (z. B. D:\projekt1) einen neuen Ordner namens src.
💡 Wichtiger Hinweis für Assembler-Dateien: Achten Sie darauf, dass Assembler-Dateien, die den C-Präprozessor nutzen (wie das Einbinden von Header-Dateien via #include), zwingend die Dateiendung mit einem großen „S“ (.S) haben müssen. Ein kleines „s“ führt dazu, dass der Compiler die Includes ignoriert.
Erstellen Sie im Ordner src/ die folgenden sechs Dateien: src/boot.S:
// boot.S
//
#include "config.h"
.section .init // Stellt sicher, dass der Linker dies an den Anfang des Kernel-Images setzt
.globl _start // Hier beginnt die Ausführung
_start:
ldr x0, =MEM_KERNEL_STACK
mov sp, x0 // Stack-Pointer initialisieren
b sysinit
src/kernel.c:
// kernel.c
//
#include "led.h"
#include "time.h"
int main (void)
{
while(1)
{
LED_off();
wait(0x1F0000);
LED_on();
wait(0x1F0000);
}
}
src/led.c:
//
// The LED program for RPI4
// 20.02.2025 www.satyria.de
//
// led.c
//
#include "base.h"
#include "util.h"
#include "types.h"
#include "led.h"
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);
}
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);
}
src/sysinit.S:
// sysinit.S
//
.section .text
.globl sysinit
sysinit:
b main
src/time.c:
// time.c
//
#include "types.h"
void wait(u32 zyklen)
{
volatile u32 i;
for (i = 0; i < zyklen; i++)
{
// Leere Schleife zur Verzögerung
}
}
src/util.S:
// 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
Hinweis: Sobald Sie diese Dateien gespeichert haben, wird Ihnen VS Code im „Probleme“-Tab diverse Fehler anzeigen. Der Code ist mit roten, gewellten Linien unterlegt. Das ist völlig normal, da dem Compiler aktuell die Header-Dateien (.h) fehlen und er die Verweise nicht auflösen kann.
Das Include-Verzeichnis (include/)
Um die Fehler zu beheben, legen wir nun die Header-Dateien an. Erstellen Sie dazu im Hauptverzeichnis Ihres Projekts einen neuen Ordner namens include. Legen Sie darin die folgenden sechs Dateien ab:
include/base.h:
// 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 Peropherals
#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
include/config.h:
// 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
include/led.h:
// 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
void LED_off(void);
void LED_on(void);
void SetGPIOFunction(u32 Pin, u32 Function);
#endif
include/time.h:
// time.h
//
#ifndef _ms_time_h
#define _ms_time_h
#include "types.h"
void wait(u32 zyklen);
#endif
include/types.h:
// types.h
//
#ifndef _ms_types_h
#define _ms_types_h
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef unsigned long u64;
typedef signed long s64;
typedef long intptr;
typedef unsigned long uintptr;
typedef unsigned long size_t;
typedef long ssize_t;
typedef char boolean;
#define ALIGN(n) __attribute__((aligned (n)))
#define FALSE 0
#define TRUE 1
#endif
include/util.h:
// util.h
//
#ifndef _ms_util_h
#define _ms_util_h
#include "types.h"
void write32(u32 a, u32 b);
u32 read32(u32 a);
#endif
Sobald alle Header-Dateien im include-Ordner abgespeichert sind, verschwinden die roten Linien in VS Code automatisch. Die GUI hat die Bezüge erfolgreich erkannt.
Linkerscript und Makefile erstellen
Damit aus dem Quellcode ein ausführbares Bare-Metal-Image für den Raspberry Pi 4 gebaut werden kann, benötigen wir ein Linkerscript und die Steuerungsdatei für GNU Make. Beide Dateien werden direkt im Hauptverzeichnis (Root) Ihres Projekts angelegt.
linker.ld
Dieses Skript definiert die genaue Anordnung der Code-Segmente im Arbeitsspeicher des Raspberry Pi 4.
ENTRY(_start)
SECTIONS
{
.init : {
*(.init)
}
.text : {
*(.text)
*(.text.*)
_etext = .;
}
.rodata : {
*(.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;
Makefile
Das Makefile automatisiert den Aufruf des Compilers und Linkers. Beachten Sie, dass hier explizit die Architektur des Raspberry Pi 4 (-mcpu=cortex-a72) angesprochen wird.
CSRCS := $(wildcard src/*.c)
CPPSRCS := $(wildcard src/*.cpp)
ASRCS := $(wildcard src/*.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:
ifeq ($(MSYSTEM),)
# --- Native Windows Umgebung (CMD / PowerShell) ---
# @cls
@if exist kernel8.elf del /q /f kernel8.elf
@if exist kernel8.img del /q /f kernel8.img
@if exist kernel8.map del /q /f kernel8.map
@if exist src\*.o del /q /f src\*.o
else
# --- MSYS2 / Unix-ähnliche Umgebung ---
/bin/rm -f kernel8.elf kernel8.map src/*.o *.img > /dev/null 2> /dev/null || true
endif
new:
ifeq ($(MSYSTEM),)
@cls
else
/bin/clear
endif
Wichtiger Hinweis zu Makefiles: Verwende keine Leerzeichen um Einrückungen zu erstellen. Dies mag make nicht. Verwende stattdessen immer ein TAB.
Vergessen Sie am Ende nicht, alle geöffneten Dateien im VS-Code-Menü über Datei -> Alles speichern zu sichern. Das Projekt ist nun vollständig konfiguriert und bereit für den ersten Build- und Debugging-Lauf!
Hardware vorbereiten und Debugging in der GUI
Hardware vorbereiten und SD-Karte einrichten
Da wir den Code direkt auf der echten Hardware testen, müssen wir die SD-Karte des Raspberry Pi 4 vorbereiten. Das Betriebssystem (EEPROM/Firmware) des Pi 4 muss wissen, dass wir ein Bare-Metal-Programm via JTAG debuggen möchten.
Formatieren Sie eine MicroSD-Karte auf FAT32 und kopieren Sie folgende drei Dateien in das Hauptverzeichnis (Root) der Karte:
kernel8.img (Diese Datei wird gleich beim ersten Kompilieren erzeugt).
bcm2711-rpi-4-b.dtb (Der originale Device-Tree-Blob aus der offiziellen Raspberry Pi Firmware)
config.txt (Die Konfigurationsdatei für die Firmware).
bootcode.bin
fixup4.dat
start4.elf
Erstellen Sie die config.txt mit exakt folgendem Inhalt:
gpio=22-27=np
enable_jtag_gpio=1💡 Was bewirkt diese Konfiguration?
enable_jtag_gpio=1 schaltet die GPIO-Pins des Raspberry Pi 4 in den JTAG-Modus um. Dadurch wird die Kommunikation mit dem FT232H Modul überhaupt erst möglich.
Hier kannst du den kompletten Inhalt runterladen: https://www.satyria.de/arm/sources/RPI4/C/RPI4_ImageVSCode.zip
Hardware verbinden
Verdrahtung
Die Verdrahtung zwischen dem FT232H Modul und dem Raspberry Pi 4 erfolgt wie folgt:

| FT232H | Raspi 4 | |
|---|---|---|
| Name | GPIO | PIN |
| AD0 | GPIO25 | 22 |
| AD1 | GPIO26 | 37 |
| AD2 | GPIO24 | 18 |
| AD3 | GPIO27 | 13 |
| AD4 | GPIO22 | 15 |
| AD7 | GPIO23 | 16 |
| GND | GND | 6 (9,14,20,25,30,34,39) |
- Stecken Sie die vorbereitete SD-Karte in den Raspberry Pi 4.
- Schließen Sie das FT232H Modul per USB-Kabel an Ihren Windows-Rechner an.
- Schalten Sie den Raspberry Pi 4 ein (Stromversorgung anschließen).
Durch das aktivierte JTAG-Interface wartet der Prozessor nun an der Startadresse auf die Verbindung und Befehle des Debuggers.
Programm kompilieren und Debugger starten
Dank unserer Vorbereitungen in Visual Studio Code können wir den gesamten Build- und Flash-Prozess mit Tastenkombinationen steuern.
Schritt 1:
Kompilieren: Drücken Sie die Tastenkombination STRG + UMSCHALT + B. VS Code führt nun im Hintergrund das Makefile aus (make all). Im Projektverzeichnis entstehen dadurch die Dateien kernel8.elf (für den Debugger inklusive Symbolen) und kernel8.img (das reine Binärformat).
Schritt 2:
Debugger starten: Drücken Sie die Taste F5.
Nun passiert Folgendes vollautomatisch:
- OpenOCD baut die Verbindung zum Raspberry Pi 4 über das FT232H Modul auf.
- Der GDB-Debugger wird gestartet.
- Das neu kompilierte Programm wird direkt in den RAM des Raspberry Pi 4 geladen (load).
- Der Programmzähler des Prozessors wird auf die Startadresse 0x80000 gezwungen.
Da wir in der launch.json festgelegt haben, dass der Debugger beim Start anhalten soll, stoppt die Ausführung exakt am ersten Befehl. In unserem Sourcecode springt der Cursor direkt in die Datei boot.S zum Label _start:
Bedienung des Debuggers in VS Code
Sobald der Debugger aktiv ist, verändert sich die Benutzeroberfläche von VS Code. Am oberen Bildschirmrand erscheint eine schwebende Debug-Steuerungsleiste.
Die Steuerelemente der GUI:
| Icon / Symbol | Aktion | Tastenkürzel | Beschreibung |
|---|---|---|---|
| Pause | F6 | Hält das laufende Programm sofort an der aktuellen Stelle an. | |
| Fortsetzen | F5 | Lässt das Programm normal weiterlaufen (bis zum nächsten Breakpoint). | |
| Einzelschritt (Step Over) | F10 | Führt die aktuelle Zeile aus. Springt bei Funktionen nicht in die Funktion hinein. | |
| Einzelschritt (Step Into) | F11 | Springt direkt in eine Funktion hinein, um sie Zeile für Zeile zu prüfen. | |
| Herausspringen (Step Out) | UMSCHALT + F11 | Führt den Rest der aktuellen Funktion aus und stoppt direkt nach der Rückkehr. | |
| Neustart | STRG + UMSCHALT + F5 | Lädt das Programm neu auf den Pi und startet den Debug-Vorgang von vorne. | |
| Stoppen | UMSCHALT +F5 | Beendet die Debug-Sitzung und schließt die Verbindung zum Pi.5 |
Die Debugger-Ansichten nutzen
Der größte Vorteil einer professionellen GUI gegenüber der GDB-Kommandozeile ist die visuelle Aufbereitung aller Prozessor-Informationen auf der linken Seite des Bildschirms:
- Variablen-Fenster (Variables): Hier werden Ihnen lokale und globale Variablen automatisch angezeigt. Sie sehen sofort, welchen Wert sie aktuell besitzen. Sie können Werte hier während des Haltepunkts sogar per Doppelklick manipulieren, um Testfälle zu simulieren!
- Überwachungs-Fenster (Watch): Wenn Sie bestimmte Variablen oder Registeradressen permanent im Auge behalten wollen, können Sie diese hier hinzufügen.
- Aufrufliste (Call Stack): Zeigt Ihnen genau, durch welche Funktionen das Programm gelaufen ist, um an den aktuellen Punkt zu gelangen.
- Register-Fenster (Cortex-Debug): Ein Highlight für Bare-Metal-Entwickler. Hier sehen Sie die CPU-Register des ARM Cortex-A72 Cores (X0 bis X30, SP, PC, etc.) im Echtzeit-Zugriff. Ändert sich ein Registerwert nach einem Einzelschritt, wird dieser farblich hervorgehoben.
Breakpoints (Haltepunkte) per Mausklick setzen
Das Eintippen von Speicheradressen in GDB gehört der Vergangenheit an. In VS Code bewegen Sie einfach die Maus links neben die Zeilennummern im Sourcecode (z. B. in der kernel.c in der Zeile LED_on();). Ein schwach roter Punkt erscheint. Mit einem einfachen Linksklick aktivieren Sie den Breakpoint (er wird kräftig rot). Wenn Sie nun F5 (Fortsetzen) drücken, läuft das Programm so lange, bis es genau diese Zeile erreicht, und friert die CPU ein. Ein weiterer Klick auf den Punkt entfernt den Haltepunkt wieder.
Wichtiger Hinweis zum Reboot
Da es nicht möglich ist, ein Reset an den Raspberry Pi 4 zu schicken, muss vor jedem Neukompilieren der Raspberry Pi 4 neu gestartet werden.
Zusammenfassung
Damit haben Sie eine vollwertige, professionelle Entwicklungsumgebung mit Hardware-Debugging für den Raspberry Pi 4 eingerichtet!