on
Raspberry Pi 3 Bare Metal in Rust
I bought a Raspberry Pi 3 several days ago. Playing with Linux seems boring for me, so I decided to pay some attention on ARM architecture. There are a lot of documents about bare metal development of Raspberry Pi, but almost all of them are using C & assembly. Since I’m learning Rust these days, it should be fun to try to implement something using Rust.
To get started, the most simplest task is to turn on the LED. Although there are some excellent resources available on how to turn on the LED for Raspberry Pi 1 & 2, unfortunately, all of them don’t work for Raspberry Pi 3, until I found this site.
Basically, the LED isn’t directly connected to GPIO anymore because GPIO pins are now used for Bluetooth and WiFi. To access the LED, you need to use Virtual GPIO, which is controlled by the VideoCore GPU. The way that ARM communicates with VideoCore is through Mailboxes.
Assembly Version #
According to the website I mentioned above, the assembly version is easy. We know the address of the mailbox, so we check the status of it and send it a message. I don’t want to get into the hardware details, so that’s it. The only thing we need to take care of is that the message itself is 16-byte aligned as only the upper 28 bits of the address can be passed via the mailbox.
Assembly Code #
.global _start
.section .data
.align 4
PropertyInfo:
.int PropertyInfoEnd - PropertyInfo
.int 0
.int 0x00038041
.int 8
.int 0
.int 130
.int 1
.int 0
PropertyInfoEnd:
.section .text
_start:
mailbox .req r0
ldr mailbox, =0x3f00b880
wait1$:
status .req r1
ldr status, [mailbox, #0x18]
tst status, #0x80000000
.unreq status
bne wait1$
message .req r1
ldr message, =PropertyInfo
add message, #8
str message, [mailbox, #0x20]
.unreq message
hang:
b hang
Linking #
The linking script is also very short. It places the .text
section at 0x8000
and the .data
section follows the .text
section.
SECTIONS {
.text 0x8000 : {
*(.text)
}
.data : {
*(.data)
}
}
Building #
Make sure that you have installed arm-none-eabi
toolchain.
# compile the assembly to an object file
$ arm-none-eabi-as kernel.s -o kernel.o
# link the object file using our linking script
$ arm-none-eabi-ld kernel.o -o kernel.elf -T kernel.ld
# we need pure binary code, not an ELF file, so strip the ELF header
$ arm-none-eabi-objcopy kernel.elf -O binary kernel.img
Now we’ve generated a binary image, put it into your SD card together with bootcode.bin
and start.elf
.
C Version #
In fact it’s not necessary to use assembly, so I translate it into C. The first item in msg
is the length of the message itself. In the assembly version, this length is calculated through PropertyInfoEnd - PropertyInfo
. Here I just hard code it.
C code #
unsigned int msg[] = {32, 0, 0x00038041, 8, 0, 130, 1, 0};
const unsigned int MAILBOX = 0x3f00b880;
void open_led() {
unsigned int status;
do {
status = *(unsigned int *)(MAILBOX + 0x18);
} while (status & 0x80000000);
*(unsigned int *)(MAILBOX + 0x20) = (unsigned int)msg + 8;
while (1)
;
}
Linking #
There are two differences in the linking script:
- The entry point is the function
open_led
. - The data section should be 16-byte aligned.
ENTRY(open_led)
SECTIONS {
.text 0x8000 : {
*(.text)
}
. = ALIGN(16);
.data : {
*(.data)
}
}
Building #
# compile the c code to an object file
$ arm-none-eabi-gcc -O2 -c led.c -o led.o
# link the object file using our linking script
$ arm-none-eabi-ld led.o -o led -T led.ld
# we need pure binary code, not an ELF file, so strip the ELF header
$ arm-none-eabi-objcopy led -O binary kernel.img
The -O2
flag for gcc is necessary. Without this option, the compiler will try to store variables in the stack, but we don’t have a stack right now! The -O2
flag tells the compiler to use registers thus avoid this problem.
Rust Version #
Finally, we’re now trying to write this program in Rust. The first thing we need to do is installing the appropriate toolchain. Here is a good resource about cross-compilation of Rust. If you don’t know what “cross-compilation” is, please read it first. We use rustup to manage the installation process.
Follow the instruction in the website above to install rustup. Then add arm-unknown-linux-gnueabihf
target:
rustup target add arm-unknown-linux-gnueabihf
Very Basic Example #
For simplicity, I want to first construct a minimal bare-metal binary, so let’s begin from a infinite loop:
// test.rs
fn open_led() {
loop {}
}
Compile and get error message:
$ rustc --target arm-unknown-linux-gnueabihf --emit=obj -O test.rs
error[E0601]: main function not found
error: aborting due to previous error
By default, the compiler tries to generate a binary, so we need to specify that we want a library:
// test.rs
#![crate_type = "staticlib"]
fn open_led() {
loop {}
}
But the compiler complains that the open_led
is never used. What’s worse, the function seems to be deleted as an optimization.
$ rustc --target arm-unknown-linux-gnueabihf --emit=obj -O test.rs
warning: function is never used: `open_led`
--> test.rs:3:1
|
3 | fn open_led() {
| ^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
In C, functions are extern
by default, this error implies that Rust is different. So we explicitly declare it as global. no_mangle
is used to avoid renaming.
// test.rs
#![crate_type = "staticlib"]
#[no_mangle]
pub extern fn open_led() {
loop {}
}
Good, no error! Now try to link it:
$ arm-none-eabi-ld -e open_led test.o -o test
test.o:(.ARM.exidx.text.open_led+0x0): undefined reference to `__aeabi_unwind_cpp_pr0'
OK, Rust needs this function. Either way, just define it as empty:
// test.rs
#![crate_type = "staticlib"]
#[no_mangle]
pub extern fn open_led() {
loop {}
}
#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr0() {}
Compile and link it again and finally we get a binary.
Write Code in Rust #
Now we are ready to implement open_led
function in Rust:
// test.rs
#![crate_type = "staticlib"]
const MAILBOX: u32 = 0x3f00b880;
const MSG: [u32;8] = [32, 0, 0x00038041, 8, 0, 130, 1, 0];
#[no_mangle]
pub extern fn open_led() {
let mut status: u32 = 0x80000000u32;
while (status & 0x80000000u32) != 0 {
status = unsafe{*((MAILBOX + 0x18) as *const u32)};
}
unsafe{*((MAILBOX + 0x20) as *mut u32) = (&MSG as *const u32 as *const u8).offset(8) as u32};
loop {}
}
#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr0() {}
Linking #
The names of the segments that rustc
generates are a little complicated:
$ readelf -e test.o
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .strtab STRTAB 00000000 0001d8 0000f5 00 0 0 1
[ 2] .text PROGBITS 00000000 000034 000000 00 AX 0 0 4
[ 3] .text.open_led PROGBITS 00000000 000034 000030 00 AX 0 0 4
[ 4] .rel.text.open_le REL 00000000 0001b0 000008 08 13 3 4
[ 5] .ARM.exidx.text.o ARM_EXIDX 00000000 000064 000008 00 AL 3 0 4
[ 6] .rel.ARM.exidx.te REL 00000000 0001b8 000010 08 13 5 4
[ 7] .text.__aeabi_unw PROGBITS 00000000 00006c 000004 00 AX 0 0 4
[ 8] .ARM.exidx.text._ ARM_EXIDX 00000000 000070 000008 00 AL 7 0 4
[ 9] .rel.ARM.exidx.te REL 00000000 0001c8 000010 08 13 8 4
[10] .rodata.cst32 PROGBITS 00000000 000078 000020 20 AM 0 0 4
[11] .note.GNU-stack PROGBITS 00000000 000098 000000 00 0 0 1
[12] .ARM.attributes ARM_ATTRIBUTES 00000000 000098 000036 00 0 0 1
[13] .symtab SYMTAB 00000000 0000d0 0000e0 10 1 12 4
Our code is in section .text.open_led
, the data can be found in .rodata.cst32
. The linking script is as follows:
ENTRY(open_led)
SECTIONS {
.text 0x8000 : {
*(.text.open_led)
}
. = ALIGN(16);
.rodata : {
*(.rodata.cst32)
}
}
Building #
$ rustc --target arm-unknown-linux-gnueabihf --emit=obj -O test.rs
$ arm-none-eabi-ld -T link.ld test.o -o test
$ arm-none-eabi-objcopy test -O binary kernel.img
Run #
Place kernel.img
we generate together with bitcode.bin
and start.elf
in your SD card. Stick the SD card into your Raspberry Pi 3 and turn it on. You will see the green LED is on!