Raspberry Pi Bare Metal Programming with Rust
16 Jan 2016After reading the excellent http://intermezzos.github.io/, I felt excited again to work on my small kernel, but I was decided to focus on the ARM arch, since it’s been awhile since I bought my Raspberry Pi B+, but never spent some time playing with it. So I started to read more about it on osdev wiki and how to get started with bare metal raspberry development. There isn’t a lot of references for this, especially if you’re using Rust. Most of the tutorials/examples uses C or plain assembly for the task, so it would be fun to try to build these things just using Rust.
So, to get started we can do the embedded systems hello world: blink a led. My goal was to achieve that with as little assembly as possible, bringing all the cool stuff to Rust, and to my surprise, this was something fairly easy to achieve. To get started we’re going to need arm-none-eabi
toolchain, for cross-compiling, Rust nightly with libcore
compiled for this architecture, Raspberry’s boot files and a Raspberry Pi B+ to run our code. I’ll walk-through the process.
Setup the cross-compiler
The steps below were done on OSX, but you can easily adapt to Linux. To install the arm-none-eabi toolchain you can either use homebrew or download the tarball, for OSX, Linux and Windows.
With homebrew you just have to:
arm-none-eabi-gcc (GNU Tools for ARM Embedded Processors) 4.9.3 20150529 (release) [ARM/embedded-4_9-branch revision 227977]
If you’re using the tarball, just download it and add it to your $PATH
.
Setup Rust
Some of the Rust’s features we’re going to use are behind a feature gate, so we need to use Rust nightly. To install it you can run:
Now we need to compile Rust’s libcore
to our target architecture. This is a minimal dependency-free foundation of the standard library. For that we need to compile it from source:
Note that the git checkout
hash I used is the same I get from rustc — version
, you should use your own to avoid obscure errors.
Raspberry Pi boot files
Those files can be found here: https://github.com/raspberrypi/firmware/tree/master/boot
You’re going to need bootcode.bin
and start.elf
. The kernel.img
is going to be written by us.
Having a bootable SD card, you can delete all files and just put the bootcode.bin
and start.elf
there. Unfortunately, we don’t have access to the code of these files and we have to use the compiled version for now. I’m planning to research more about that and try to figure out how we can write our own boot files, but this is going to be a topic for another post.
Getting started
With all that set, we can start writing code. We’re going to make the ACT led blink, it’s the green one beside the power led. So our program basically need to turn it on, wait for some time, turn it off and loop. So fire up your editor and create our kernel.rs
file.
This would be a standard hello world example, but the println!
macro is part of the rust standard library and uses the platform I/O to print something to the screen and right now we don’t have a platform to run on top! So, we need to inform the compiler that our code freestanding will be freestanding:
In the first line, we’re informing the compiler that we’re going to use a gated feature no_std
, it’s with the #![feature()]
attribute that we inform such things to the compiler. With the #![crate_type]
we say that we’re compiling a static library, which basically means that all our code is self-contained and finally, with the #![no_std]
we’re saying that we’re not going to use the standard library, so it doesn’t get automatically loaded in the prelude process. Now, if we try to compile that, we’ll get an error:
As expected, there is no defined println!
macro, because there is no implemented I/O and runtime, we’re on our own!
If we remove the println!
call and try to compile again we’ll get another error:
This means that the minimal Rust runtime expects these two functions to be defined. Both functions are used by the failure mechanism/handling of the compiler. For a simple freestanding hello world, we can just define them as empty implementations:
The #[lang]
attribute informs the compiler that this function is being defined for the runtime. Since this attribute is also feature gated, we need to add it to our #![feature()]
attribute list. Now to compile, we also need to inform the compiler that we just want it to emit an object file. An object file is basically your compiled code with the defined symbols. We’re going to use the generated object file to link with arm-none-eabi-gcc
and generate our kernel.elf
file. We’ll also add the -O
flag to remove some unnecessary symbols and do some optimizations.
We also changed our main
function to be public and extern. This informs the compiler that our function will be used externally.
So now we run:
This will generate our kernel.o
file. We can make sure our cross-compiling is working and emiting ARM compatible machine code using the file
command:
We can inspect our object file symbols using arm-none-eabi-nm
:
This show us the three defined symbols by our source code. The weird one is our main function. This happens because of something called name mangling, which is something the compiler does to avoid name collisions. We want our symbols to be clear, so external tools knows how to reference to it. To tell the compiler to not mangle our main function, we need to:
If we compile again and check our symbols we’ll see:
They’re all clear! This is going to be useful when we need to call it from assembly.
Making the LED blink
Raspberry’s Pi B+ GPIO base address is 0x20200000
. The register that will allow us to turn our LED on is at the base address + 0x8
offset and we need to set the 15th bit to 1 to turn it on. To turn it off, we need to set the 15th bit to 1 on the base address + 0xB
offset. Between these changes, we need to block the program for some time. Our code will look like:
We added an sleep
function which basically is an empty loop for a given range. The unsafe { asm!(“”) }
part was the way I found to avoid the compiler to strip this part of the code out during the optimization phase, as it doesn’t seem to do anything. In the main function, we create a raw pointer to the GPIO_BASE
address, then we get a mutable pointer to the locations we want to change, which are the one to turn the led on and to turn it off. Since the offset call involves dereferencing a raw pointer, its an unsafe operation, thus the unsafe
blocks. With that we can just do an infinity loop that turns the led on, wait, turn it off and wait.
Preparing your code to run on the Raspberry PI
Now we can compile our code to run on our raspberry:
With our kernel.img
built, we just need to copy it to our Raspberry SD together with the other boot files (start.elf
and bootcode.bin
), stick our micro SD back into the Raspberry PI and turn it on to see our tiny kernel running and the LED blinking!
There is a lot of other cool stuff we can do, like using the system timer on chip to implement our sleep
function or create some abstractions of top of the GPIO addresses to encapsulate all the unsafe
calls in a more elegant API. But I think for now this is a good way to get started with bare metal programming with Rust.