Why Build a Bare Metal Kernel for the Raspberry Pi? Link to heading

Recently, a friend asked me to develop a bare metal program for his project, which involves controlling an LED matrix. Although I’ve worked in embedded systems before, I never had the chance to build something from scratch — a true bare metal program.

His idea intrigued me, so I decided to dive deeper into this topic. The closest device I had lying around was my Raspberry Pi 3 B+, so I chose that as my platform to experiment with.

What is the Goal? Link to heading

Short answer: I don’t know yet.

I have some big ideas brewing in my mind, but when it comes to exploratory projects like this, planning too far ahead can quickly lead to burnout. So instead of setting rigid long-term goals, I’ll focus on short-term milestones and see where the journey takes me.

The important thing is to keep learning, experimenting, and sharing what I discover along the way.

Prerequisites Link to heading

You’ll need:

  • A Raspberry Pi 3 B+ (new PIs vary in functionality and some implementation steps may differ)
  • A UART-USB adapter for GPIO pins (This becomes important when implementing UART - so it can’t hurt to buy one now)
  • Basic understanding of Rust
  • Basic understanding of computer architecture

Don’t worry if you’re not 100% confident. I’ll try to explain as much as I can.

Preparations Link to heading

Toolchain Link to heading

A Raspberry Pi 3 B+ uses ARM Cortex-A53 chip where the instruction set differs from a “normal” computer or laptop which often is based on x86. To be able to compile our project for the PI we need to install the correct toolchain.

We can install it via following command:

> rustup target add aarch64-unknown-none

Let’s break down what this target triple means:

aarch64 - tells us its a toolchain for a ARM64 architecture.

unknown - is the vendor field the the triple, which is often a place holder

none - is the operating system, but in our case we are developing for bare metal so we just use none

If you’d prefer a 32bit architecture you can use the armv7-unknown-none-eabihf toolchain.

Setting Up Rust for Cross-Compilation Link to heading

Because we will always be building for the aarch64 architecture we can save us time by telling Cargo to use that target by default.

Create a .cargo/config.toml in your project root and add following:

[build]
target = "aarch64-unknown-none"

This configures Cargo to always use that target when building.

Linking Link to heading

The compiler converts the code into assembly code for the target architecture, in our case aarch64.

Normally an operating system helps us and manages the memory layout for us, but because we are building our own we also have to do that on our own.

Yes! In bare-metal we are responsible for the memory layout. Doesn’t that sound fun :)

For this we need a so called linker that links our assembly code to the correct memory locations on the PI.

In the root of the project we create a file called link.ld and fill it with:

SECTIONS {
    . = 0x80000;

    .text : {
        KEEP(*(.text._start))
        *(.text .text.*)
    }

    .rodata : {
        *(.rodata .rodata.*)
    }

    .data : {
        _data = .;
        *(.data .data.*)
    }

    .bss (NOLOAD) : {
        . = ALIGN(16);
        __bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        __bss_end = .;
    }

    _end = .;
}

__bss_size = (__bss_end - __bss_start) >> 3;

I’m no expert on linker files and there is a great blog post on how they work!

But what it basically does is:

. = 0x80000; - set the address where we want to start in memory(The raspberry PI firmware jumps to this address after the bootloader finishes)

.text - The linker will link all of our code into this section

.text._start - we define this in the beginning of the section because our main function should be the first thing run as soon as the firmware jumps

.text .text.* - are all the other functions where the order doesn’t really matter

.rodata - are all variables that are defined but are constant

.data - all global and static variables go here

.bss - bss stands for Block Started by Symbol and all uninitialized global variables go here.

Now we just have to tell our compiler to run the linker during the build process by adding following to our .cargo/config.toml:

[target.aarch64-unknown-none]
rustflags = ["-C", "link-arg=-Tlink.ld"]

Whats next Link to heading

In the next few steps we will:

  • build our Hello World that can run on a real PI
  • setup a simulator for testing
  • log via UART
  • timing

Git Repository Link to heading