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