Rust on a Raspberry Pi – Part 1
Table of Contents
1 Introduction
The popular Raspberry Pi with its BCM2711 ARM CPU is normally loaded with the Raspberry Pi OS (previously called Raspbian). This Unix-like operating system has been the primary operating system for this hardware since 2013.
In this blog, the Rust programming language is used to create a Rust image that does not link a standard library, instead the linker is modified to generate a Raspberry Pi compatible kernel that when installed on a Raspberry Pi, toggles a specific GPIO pin on the CPU to make a connected LED flash!
All the steps required to complete this cross-compiling example were done using a normal Intel based host computer running Linux Ubuntu 20.04.5 and a target Raspberry Pi 3.
INSTALLING RUST ON THE HOST COMPUTER
The following programs are pre-requisites to cross-compile on a Linux host. Go ahead and install them from inside a Linux terminal as they are not included by default in Linux Ubuntu (Clean Installation). It is often a good idea to ensure the host OS is up-to-date, so run the following commands beforehand. Please select (Y) for any prompts during installation.
sudo apt-get update && sudo apt-get upgrade
(1) Rust Installer uses curl to install rust using a URL, use the following command to install curl:
sudo apt install curl
(2) Now install Rust with the following command
curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh
Select (1) to proceed with the default installation:
(3) Rust does not include its own linker yet, so cross-compilation will return a linker ‘cc’ not found error if build-essential is missing. To prevent this, install build-essential tools:
sudo apt install build-essential
(4) Install ‘rustup’ which makes cross-compiling simpler with binary builds of the standard library for common platforms. Also required is the embedded abi toolchain for the Broadcom BCM2387 CPU used by the Raspberry Pi3 (the last command in the list below):
sudo snap install rustup --classic
rustup install stable
rustup default stable
rustup target add armv7a-none-eabi
(5) Finally, install ‘Binary Utilities’ for the target CPU using the following command as we want to use objdump and objectcopy later to analyse the content of the files we are creating:
sudo apt install binutils-arm-none-eabi
2 Building the project
Generate a Rust project folder that houses the files we will use in this example using cargo, (the Rust package manager) by typing the following into the Linux terminal window. The filename chosen is up to you, but here it is called pi_baremetal_rust
cargo new pi_baremetal_rust
Naviate to the folder that contains the Rust Project:
cd pi_baremetal_rust
Within the pi_baremetal_rust folder, create a new (hidden) folder called .cargo:
mkdir .cargo
Then generate a file called ‘config’ (config has no file extension) inside .cargo
cd .cargo
touch config
Because our host computer is Linux that we run on an x86 based CPU, Rust will naturally try and compile main.rs for that, but we want to ‘cross’ compile for an ARM BC2711 on the Raspberry Pi. Using a text-editor, the config file we just generated should contain details of the arm build chain, the Raspberry Pi uses. Use the following code to ensure that during compilation, the kernel image file is suited to the correct CPU type, a 32-bit ARM device (v7 signifies 32-bit), that there is no underlying OS and that the Application Binary Interface is of the embedded type.
Use a text editor in Linux to modify the empty config file with the following:
[build] target = "armv7a-none-eabi"
3 Modify main.rs
When Rust was installed and the project started, it populated main.rs with the usual hello world program. The location of the main.rs source file for this project example can be found at the following location.
~/pi_baremetal_rust/src/main.rs
Overwrite the hello world program generated by Rust and replace it with the source code below (the comments in the code explain what’s going on):
/*============================================================================*/ /* no_std: Don't incorporate the standard library as this is a bare-metal */ /* based program. */ /*============================================================================*/ #![no_std] /*============================================================================*/ /* no_main: Don't use the 'main' function as our entry point. */ /* It is taken care of elsewhere through a modified linker script. */ /*============================================================================*/ #![no_main] use core::panic::PanicInfo; use core::arch::asm; /*============================================================================*/ /* Because there is the small possibility that _start may not run first, */ /* global assembly is used to ensure that _start is put at the beginning of */ /* the image. */ /*============================================================================*/ mod boot { use core::arch::global_asm; /*========================================================================*/ /* glabal_asm macro: says all the code below this line is in the _start */ /* section. It can be located in the linker file - linker.ld */ /*========================================================================*/ global_asm!(".section .text._start"); } /*============================================================================*/ /* no_mangle: Ensures that name _start is manageable, because by default it */ /* might get 'mangled'. Ensures that in the link environment the symbol name */ /* is _start. */ /*============================================================================*/ #[no_mangle] /*============================================================================*/ /* Declared as public (extern "C") ensuring the _start symbol is globally */ /* accessible and the linker can see it at link time, ordered in the right way*/ /* ===========================================================================*/ pub extern "C" fn _start() -> ! { /*========================================================================*/ /* unsafe: The compiler can trust the code and I don't require Rust's */ /* memory safety guarantees enforced at compile time for this example. */ /*========================================================================*/ unsafe { /*====================================================================*/ /* Set-up GPIO21 to be an output pin. */ /*====================================================================*/ core::ptr::write_volatile(0x3F20_0008 as *mut u32, 1<<3); /*====================================================================*/ // Enter an infinite loop that turns an LED on GPIO 21 On and Off. */ /*====================================================================*/ loop { /*================================================================*/ // Turn the LED ON by toggling the output pin HIGH */ /*================================================================*/ core::ptr::write_volatile(0x3F20_001C as *mut u32, 1<<21); /*================================================================*/ // Wait 50000 ticks, so the LED ON state can be seen. */ /*================================================================*/ for _ in 1..50000 { asm!("nop"); } /*================================================================*/ // Turn the LED OFF by toggling the output pin LOW */ /*================================================================*/ core::ptr::write_volatile(0x3F20_0028 as *mut u32, 1<<21); /*================================================================*/ // Wait 50000 ticks, so the LED ON state can be seen. */ /*================================================================*/ for _ in 1..50000 { asm!("nop"); } } } } /*============================================================================*/ /* The panic handler is entered when the OS kernel detects an error and is */ /* required to ensure error free compilation */ /*============================================================================*/ #[panic_handler] fn panic (_info: &PanicInfo) -> ! { loop {} }
Compile the code above by using the following command:
cargo build
Now let’s check the resultant binary by running the following command to view the structure of the program as seen by the Pi when it is switched on:
arm-none-eabi-objdump -D /home/<user>/pi_baremetal_rust/target/armv7a-none-eabi/debug/pi_baremetal_rust | less
Looking at the binary dump data, there are two issues that need to be fixed. The first is that there is a lot of information above the _start entry point and the second is that the start address is showing as 0x0201E0.
The base address for images put onto the SD card that will plug into the Pi needs to be 0x8000 and _start has to be at the very beginning.
Push ‘Q’ to exit the Object Dump Viewer.
To fix these problems, we will add a linker file that tells the compiler to put _start at the entry point with an address of 0x8000. The linker file script ‘linker.ld’ with contents as shown below should be placed in the pi_baremetal_rust folder
To invoke the cargo build chain with the linker.ld script using the following command:
cargo rustc -- -C link-arg=--script=./linker.ld
–C adds a compiler flag and link-arg states the linker script is equal to our local linker.ld
ENTRY(_start) SECTIONS { . = 0x8000; .text : { *(.text._start) *(.text) } . = ALIGN(4096); .rodata : { *(.rodata) } . = ALIGN(4096); .data : { *(.data) } . = ALIGN(4096); __bss_start = .; .bss : { bss = .; *(.bss) } .ARM.exidx : { *(.ARM.exidx*) } . = ALIGN(4096); __bss_end = .; __bss_size = __bss_end - __bss_start; __end = .; }
If you modify an existing linker script, the build chain won’t run until you delete your target. This is because it scans the source code and only runs if there has been a change there, it does not include changes that are made in the linker script. Always delete the target folder before running the build chain using the following terminal commands:
rm -rf target/
cargo rustc -- -C link-arg=--script=./linker.ld
Now let’s check the binary again to see the changes:
arm-none-eabi-objdump -D /home/<user>/pi_baremetal_rust/target/armv7a-none-eabi/debug/pi_baremetal_rust | less
Notice that _start is now at the beginning of the image and that is has a base address of 0x8000. This is correct. If you look closely, you can also see how the registers R0 and R1 are being loaded with values in our new main.rs that are associated with the GPIO.
4 Creating the Image File
Linux produces *.elf files (Executable and Linkable File Format). We need to rip the code out of the elf file and put it into a flat binary for it to run as a bare-bones image from the micro-SD card in the Pi 3. To do this, use the following command:
arm-none-eabi-objcopy -O binary target/armv7a-none-eabi/debug/pi_baremetal_rust ./kernel7.img
5 Prepare the Micro-SD Card
Format a micro-SD card as FAT32 and copy the newly created kernel7.img
onto it. Some additional files are also required to boot the Pi and these are obtained from the Raspberry Pi firmware repo at the following address:
https://github.com/raspberrypi/firmware
Go to ‘view code’ then ‘boot’ and download fixup.dat
, start.elf
and bootcode.bin
The default firefox web browser should place the files into the download folder, so the following terminal commands will move them into our pi_baremetal_rust working folder using the following command:
cp ~/Downloads/start.elf ~/Downloads/bootcode.bin ~/Downloads/fixup.dat .
Copy these three files onto the micro- SD card.
Finally, create a small file named config (no extension) that contains the line
arm_64bit=0
and copy this file onto the micro-SD card. This ensures that the bootloader will boot into a 32-bit environment.
The Micro-SD card now contains the following 5 files:
6 Raspberry Pi3 Hardware
Connect an LED with the positive leg on GPIO21 with the negative leg to a 1K current limiting resistor and then to GND. It is convenient to use a 2-pin 2.54mm header socket connected to the LED and resistor as this will allow the LED to plug straight onto the GPIO header pins.
It may sometimes be necessary to manually reset the Raspberry Pi 3 after power up to ensure the CPU reset is clean. To achieve this, a small tact switch can be connected to the Pi. You may have to solder a 2-pin header onto the PCB first.
Now insert the card into the raspberry Pi and power it up. If the LED is not flashing, ensure it is fitted the right way round. If it is, push the Reset Button a few times.
Eureka! A Raspberry Pi 3 running bare-metal based code to toggle a GPIO pin. What next? Maybe a UART Output, bit-bash for I2C or even a full Rust OS!
Rust Application Running On Raspberry Pi Video MP4
Download the
or
video.
7 References
Rust Programming Language documentation https://doc.rust-lang.org/book
BAREMETAL RUST Runs on EVERYTHING, Including the Raspberry Pi https://www.youtube.com/watch?v=jZT8APrzvc4
Raspberry Pi Bare Bones https://wiki.osdev.org/Raspberry_Pi_Bare_Bones
Raspberry Pi GPIO Pin-Out https://pinout.xyz/#
BCM2387 CPU datasheet (P89 is GPIO): https://cs140e.sergio.bz/docs/BCM2837-ARM-Peripherals.pdf