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:
- As mentioned above, improve the macro
- use the
cortex_a
crate and remove the assembly code - use the MMU and possibly the cache
- create a memory allocator, so that I can use the collections such as
String
andVec
- access the SD
- look at circle and try reproduce the USB support in Rust
I will definetly try make some time to explore these.