Skip to content

01. Environment Setup for Raspberry Pi OS Development

This series builds a small bare-metal ARM64 operating system, not a modified Raspberry Pi OS Linux installation. The primary target is Raspberry Pi 4 Model B with BCM2711 and Cortex-A72. Raspberry Pi 5 uses a different SoC, interrupt controller, peripheral map, and default kernel filename, so do not assume Pi 4 code will run unchanged.

What you need

Use a development computer, a spare microSD card, a Raspberry Pi 4, a 3.3V USB-to-TTL serial adapter, and a reliable power supply. Keep your normal Raspberry Pi OS card separate from the experimental card.

Choose a Bare-Metal Compiler

The preferred compiler prefix is aarch64-none-elf-, which explicitly targets freestanding AArch64 firmware. Arm publishes current builds on its GNU Toolchain downloads page.

aarch64-linux-gnu-gcc can also compile the early examples when every hosted runtime dependency is disabled, but it targets a Linux ABI by default. The two toolchains must not be mixed in one build directory.

1
2
3
4
5
sudo apt update
sudo apt install build-essential cmake git ninja-build \
  gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu

export CROSS_COMPILE=aarch64-linux-gnu-
1
2
3
4
# Extract the archive downloaded from Arm, then adjust this path.
export TOOLCHAIN_ROOT="$HOME/opt/arm-gnu-toolchain"
export PATH="$TOOLCHAIN_ROOT/bin:$PATH"
export CROSS_COMPILE=aarch64-none-elf-
1
2
3
brew install cmake ninja git
# Install Arm GNU Toolchain and add its bin directory to PATH.
export CROSS_COMPILE=aarch64-none-elf-

Verify that all tools come from the expected prefix:

1
2
3
4
"${CROSS_COMPILE}gcc" --version
"${CROSS_COMPILE}ld" --version
"${CROSS_COMPILE}objcopy" --version
cmake --version

Create the Project Layout

1
2
3
4
5
6
7
8
9
os-rasp/
├── CMakeLists.txt
├── cmake/
│   └── aarch64-bare-metal.cmake
├── kernel/
│   ├── boot.S
│   ├── kernel.c
│   └── linker.ld
└── build/

Create a clean build directory whenever you change compilers:

1
2
3
4
5
6
git clone <your-repository-url> os-rasp
cd os-rasp
rm -rf build
cmake -S . -B build -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=cmake/aarch64-bare-metal.cmake
cmake --build build --verbose

CMake Toolchain File

Use the environment variable so the same repository works with either compiler prefix:

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

if(NOT DEFINED ENV{CROSS_COMPILE})
  set(ENV{CROSS_COMPILE} aarch64-none-elf-)
endif()

set(CMAKE_C_COMPILER   "$ENV{CROSS_COMPILE}gcc")
set(CMAKE_ASM_COMPILER "$ENV{CROSS_COMPILE}gcc")
set(CMAKE_OBJCOPY      "$ENV{CROSS_COMPILE}objcopy")

set(BARE_METAL_FLAGS
  -mcpu=cortex-a72
  -ffreestanding
  -fno-stack-protector
  -fno-pie
  -nostdlib
  -nostartfiles
)

add_compile_options(${BARE_METAL_FLAGS})

CMAKE_TRY_COMPILE_TARGET_TYPE prevents CMake from trying to execute an ARM test program on the development computer.

Minimal Build Definition

Your main CMakeLists.txt should enable C and assembly, link with the custom script, and convert the ELF into the raw image expected by Pi 4 firmware:

cmake_minimum_required(VERSION 3.20)
project(os_rasp LANGUAGES C ASM)

add_executable(kernel8.elf
  kernel/boot.S
  kernel/kernel.c
)

target_link_options(kernel8.elf PRIVATE
  -T${CMAKE_SOURCE_DIR}/kernel/linker.ld
  -nostdlib
  -Wl,--build-id=none
  -Wl,-Map=${CMAKE_BINARY_DIR}/kernel8.map
)

add_custom_command(TARGET kernel8.elf POST_BUILD
  COMMAND ${CMAKE_OBJCOPY} -O binary
          $<TARGET_FILE:kernel8.elf>
          ${CMAKE_BINARY_DIR}/kernel8.img
)

Validate Before Copying to the SD Card

A successful compiler exit does not prove the image is usable. Inspect architecture, entry point, sections, unresolved symbols, and raw image size:

1
2
3
4
5
"${CROSS_COMPILE}readelf" -h build/kernel8.elf
"${CROSS_COMPILE}readelf" -S build/kernel8.elf
"${CROSS_COMPILE}nm" -u build/kernel8.elf
"${CROSS_COMPILE}objdump" -d build/kernel8.elf | head -80
ls -lh build/kernel8.elf build/kernel8.img build/kernel8.map

Expected checks:

  • Machine is AArch64.
  • The entry point matches the linker script and _start symbol.
  • nm -u prints no unexpected runtime symbols such as memcpy, __stack_chk_fail, or __libc_start_main.
  • kernel8.img is non-empty.

Deploy Without Overwriting Your Main System

Start with a Raspberry Pi OS boot partition because it already contains compatible firmware and device-tree files. Back it up, then replace only kernel8.img on the experimental card.

Add these settings to that card's config.txt:

1
2
3
arm_64bit=1
enable_uart=1
kernel=kernel8.img

Copy the image and flush pending writes:

cp build/kernel8.img /path/to/bootfs/kernel8.img
sync

Connect the serial adapter with GND to GND, adapter RX to Pi GPIO14/TXD, and adapter TX to Pi GPIO15/RXD. Never connect a 5V serial signal to Raspberry Pi GPIO.

Reproducible Build Measurements

Record tool versions and build time before comparing optimisations:

1
2
3
4
5
6
7
8
"${CROSS_COMPILE}gcc" --version | head -1
cmake --version | head -1
git rev-parse --short HEAD

/usr/bin/time -f 'elapsed=%e max_rss_kb=%M' \
  cmake --build build --clean-first
sha256sum build/kernel8.img
stat --printf='bytes=%s\n' build/kernel8.img

These are host build measurements, not Raspberry Pi runtime benchmarks. Later articles add UART timestamps and the system timer for on-device measurements.

Common Failures

CMake says the compiler cannot build a test program

Confirm CMAKE_SYSTEM_NAME Generic and CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY, then delete the cached build/ directory before configuring again.

Linker reports __stack_chk_fail or PIE relocations

Ensure -fno-stack-protector, -fno-pie, -nostdlib, and -nostartfiles are active in the verbose command. Some Linux-targeting distributions enable hardening flags by default.

The Pi shows no UART output

Check the serial adapter is 3.3V, RX and TX are crossed, the baud rate matches, enable_uart=1 is present, and the boot code initialises the correct UART. Confirm that the activity LED pattern does not indicate a missing kernel file.

The image works on Pi 4 but not Pi 5

That is expected for this series. Pi 5 normally boots kernel_2712.img and has RP1-connected peripherals. Port the boot code and drivers deliberately instead of merely renaming the image.

FAQ

Why not use the normal host gcc?

It emits code for the development computer, usually x86-64. A cross-compiler emits AArch64 instructions and provides matching assembler, linker, objcopy, and inspection tools.

Is -mcpu=cortex-a72+nosimd required?

No. Start with -mcpu=cortex-a72. Disable floating-point and SIMD usage only when the kernel has not yet saved the relevant processor state or your coding rules require it.

Can QEMU replace physical hardware?

QEMU is useful for CPU-level experiments, but it does not accurately reproduce all Raspberry Pi 4 firmware and peripherals. UART output on a real Pi remains the reference test for this series.

Next Steps