Bare Metal Rust on the Raspeberry Pi4

published on January 3, 2020

Recently I’ve been getting into embedded programming; since I own multiple Raspberry Pi4 boards, I decided to put them to use. In this post I write about my first steps in this project, and I hope this may help understand how one can face the difficulties they might encounter when taking projects such as this one.

The beginning

I wrote a simple program that prints a string on the UART0 interface. I started with the tutorial on OSDev Wiki. that code needed to be adapted to work on the real hardware though, so I used some of the code that I found on this repo as reference to get started, adapting it to the Pi4 (just changing the base offset of the MMIO registers, really).

First problems

Target Specification

The first problem I encountered was with the target specification file. This file tells rustc (the Rust compiler) how to build and link the executable.

The file that the wiki article used was for 32bit arm, whereas I wanted to compile a 64bit kernel image. For this reason, I created my own specification.

rustc --print target-list | grep aarch64

I used this command to list the existing aarch64 targets that rustc supports, then

rustc +nightly -Z unstable-options --target=aarch64-unknown-none --print target-spec-json > my-spec.json

to save the spec to a JSON file. I found this commands searching on google how to list available targets and their spec JSON.

I finally modified the spec, using more or less the settings that are posted on the Wiki article, and changing just the arch and the data layout. After some attempts, I was able to have the release code work. The only thing I needed to change was the way one variable was initialized (basically I used clone instead of using the implicit copy). Having some similar code, I could look at the differences. At first I thought that the problem was the use of some Neon registers, but that was not it; it was an unaligned memory access. In armv8, unaligned memory accesses are allowed only if the MMU is enabled1.

Memory Alignment

This problem took me at least three days to fully understand. Once I finished writing the code, I tried running it on my Pi4 and it worked. I decided to try compiling it with the release profile, to make sure that it was still working, and lo and behold, it didn’t.

I tried to look at the assembly code generated by the compiler, and compare it to the working one, but the optimized code was very different.

Improving the code

Macro and Workspace

To initialize the UART0 interface, it is needed to use the mailbox property interface. Even though I only needed to make a set clock rate request, there are a lot of different requests, all with the same header structure.

I took this chance to reduce the amount of boilerplate code to learn procedural macros in rust. I created a macro that can be used in a way similar to how #derive[...] is used; here’s an example from the simple kernel:

/// mailbox_request(buffer_size, code, tag_id, tag_size, ?const_name)
/// buffer_size: the total length of the request message, including header and
///              end_tag
/// code: the code of the message (it can actually also be used for responses)
/// tag_id: id of the tag (only supports single tag requests/responses, but it
///         should be the more common case)
/// tag_size: length of the tag
/// const_name: name to give to the generated constant. It's optional, and if
///             not specified, the name of the struct will be converted in upper
///             snake case and the string `_HEADER` will be appended to it. 
///             example: "SetClockRateRequest" -> "SET_CLOCK_RATE_REQUEST_HEADER"

#[mailbox_request(36, MailboxCode::Request, MailboxTag::SetClock, 12)]
struct SetClockRateRequest {
    id: ClockId,
    rate: u32,
    skip_turbo: u32,
}

It is applied to a structure, and takes the values of the fields of the header as parameters; it adds the header to the structure, as well as the end tag, and defines a constant containing the header for the request (as a matter of fact, the header usually is the same for all the requests of the same type). If the parameters are missing (or are not enough), the constant will not be defined.

This macro could definitely be improved, probably with better warning/error messages. This is something I think I will do in the future, if only to understand what’s th best way to have procedural macros with messages.

Remove the GCC toolchain

When I started this project, I followed the Wiki article, which uses a GCC cross-toolchain to compile the assembly file and link the kernel. After I finished, I found the repo https://github.com/rust-embedded/rust-raspi3-OS-tutorials, which provides good tutorials and does not make use of the GCC toolchain. Since installing the toolchain is easy but takes some time, I updated the code to use cargo-binutils, which makes getting started easier.

What’s next

There are a lot more things that I can do:

I will definetly try make some time to explore these.

References


  1. Thread this is the link to the thread on the Raspberry Pi forums where I asked for help. There are links to the relevant documentation, where the concept of normal and device memory is explained ↩︎