TLDR
I want to code, compile, and run bare metal code on a raspberry Pi in Rust and blink an LED.
Compile code into ELF:
cargo build --release
Flatten into binary
arm-none-eabi-objcopy -O binary ./target/armv7a-none-eabi/release/led ./kernel.img
Flash the SD
Load files into FAT32 formated SD card From this directory and the kernel.img.
Connect
Connect an LED on GPIO 2 (PIN 3).
Enjoy the blink
Hardware setup
The board
I’ll be using a Raspberry PI 3 Model B v1.2
.
The Processor
We first need to find out what processor it’s using to get the datasheet for later. We can find out on the Raspberry PI Website
The processor is the Broadcom BCM2837 Arm processor
.
The datasheet : BCM2837-ARM-Peripherals.pdf
LED circuit
We’ll be attaching a random LED in series with a resistor to a Raspberry GPIO.
We can seach for the 40 pins pinout of the raspberry:
I’m gonna use the GPIO 2
on PIN 3
and PIN 6
for gnd
because i want to. Here is a schematics of the small circuit we’ll be using:
Workflow setup
The software part is done on a host computer with Rust installed.
We want to compile code for a specific architecture, this mean we’ll need to cross-compile the code because the architecture of our PC is different from the one on the PI. We also want to use it Bare Metal which means we don’t want it to run on an existing Operating System so we can’t use existing OS’ libraries.
For that we’ll need need to specify the target architecture in a config file of cargo (Cargo is our build system).
Configure target architecture
Create .cargo directory:
mkdir .cargo
and create a file .cargo/config
with:
[build]
target = "armv7a-none-eabi"
armv7a
: The architecture of the processornone
: This means we’re not using any underlying Operating systemeabi
: Means we’re using the extended ABI
We’ll probably need to install the target architecture:
rustup target add armv7a-none-eabi
Change the main
The current main uses the std
library which is not available for our target architecture. We’ll need to change it.
Tell the compiler to only use bare metal code and tell it we’re gonna take care of the entry point:
#![no_std]
#![no_main]
We’ll need to define the entry point of our program:
#[no_mangle]
pub extern "C" fn _start() -> !
{
loop {}
}
#[no_mangle]
: Tells the compiler to avoid name mangling (don’t change the name of the function)-> !
: Tells the compiler that the function never returnsextern "C"
: Exposes to the linker
If the processor crashes, it needs to know what to do. We’ll need to define a panic handler:
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Compile
We can now compile the code:
cargo build --release
Ouput binary
ELF format
We can see the output executable in the Executable and Linkable (ELF) format:
readelf -a .\target\armv7a-none-eabi\release\led
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x3F0e4
Start of program headers: 52 (bytes into file)
Start of section headers: 472 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 5
Size of section headers: 40 (bytes)
Number of section headers: 8
Section header string table index: 6
[ ... ]
No version information found in this file.
Attribute Section: aeabi
File Attributes
Tag_conformance: "2.09"
Tag_CPU_arch: v7
Tag_CPU_arch_profile: Application
Tag_ARM_ISA_use: Yes
Tag_THUMB_ISA_use: Thumb-2
Tag_FP_arch: VFPv3
Tag_ABI_PCS_R9_use: V6
Tag_ABI_PCS_GOT_use: direct
Tag_ABI_FP_denormal: Needed
Tag_ABI_FP_exceptions: Unused
Tag_ABI_FP_number_model: IEEE 754
Tag_ABI_align_needed: 8-byte
Tag_ABI_align_preserved: 8-byte, except leaf SP
Tag_ABI_optimization_goals: Aggressive Speed
Tag_CPU_unaligned_access: None
Tag_ABI_FP_16bit_format: IEEE 754
Installing arm-none-eabi toolchain
We could dump the binary to see the memory layout but we need an arm toolchain to do that:
sudo apt install gcc-arm-none-eabi
or
Dumping the binary
We can now dump the binary:
arm-none-eabi-objdump -D .\target\armv7a-none-eabi\release\led
We can see the memory layout:
target/armv7a-none-eabi/release/led: file format elf32-littlearm
Disassembly of section .ARM.exidx:
000100d4 <.ARM.exidx>:
100d4: 00010010 andeq r0, r1, r0, lsl r0
100d8: 00000001 andeq r0, r0, r1
100dc: 0001000c andeq r0, r1, ip
100e0: 00000001 andeq r0, r0, r1
Disassembly of section .text:
000200e4 <_start>:
200e4: eafffffe b 200e4 <_start>
Disassembly of section .ARM.attributes:
00000000 <.ARM.attributes>:
0: 00003341 andeq r3, r0, r1, asr #6
4: 61656100 cmnvs r5, r0, lsl #2
8: 01006962 tsteq r0, r2, ror #18
c: 00000029 andeq r0, r0, r9, lsr #32
10: 302e3243 eorcc r3, lr, r3, asr #4
14: 0a060039 beq 180100 <_start+0x16001c>
18: 01084107 tsteq r8, r7, lsl #2
1c: 030a0209 movweq r0, #41481 ; 0xa209
20: 0111000e tsteq r1, lr
24: 00150114 andseq r0, r5, r4, lsl r1
28: 01180317 tsteq r8, r7, lsl r3
2c: 021e0119 andseq r0, lr, #1073741830 ; 0x40000006
30: 01260022 ; <UNDEFINED> instruction: 0x01260022
Disassembly of section .comment:
00000000 <.comment>:
0: 6b6e694c blvs 1b9a538 <_start+0x1b7a454>
4: 203a7265 eorscs r7, sl, r5, ror #4
8: 20444c4c subcs r4, r4, ip, asr #24
c: 302e3531 eorcc r3, lr, r1, lsr r5
10: Address 0x0000000000000010 is out of bounds.
Linker script
The thing is that we need to have the <_start>
at the beginning of the binary AND we need it to be at address 0x8000 because it’s where the Raspi bootloader will loads the binary.
To achieve that we need to create a linker script that will describe the memory layout of our binary (Source).
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 = .;
}
We also need to add something on the start to tell the linker to use this to put __start at the beginning of the binary:
#[no_mangle]
#[link_section = ".text._start"]
pub extern "C" fn _start() -> !
{
loop {}
}
Compiling with linker script
We’ll have to add the linker script as argument to rustc by adding this to our .cargo/config
:
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = ["-C", "link-arg=-Tlinker.ld",]
We can now compile our binary with the linker script:
cargo rustc --release
We should now be able to dump the binary and see the start first @0x8000
:
arm-none-eabi-objdump -D .\target\armv7a-none-eabi\release\led
We can see the memory layout:
target/armv7a-none-eabi/release/led: file format elf32-littlearm
Disassembly of section .text:
00008000 <_start>:
8000: eafffffe b 8000 <_start>
Disassembly of section .ARM.exidx:
[ ... ]
Disassembly of section .ARM.attributes:
[ ... ]
Disassembly of section .comment:
[ ... ]
Flattening the binary
The current binary is in ELF format and we need to flatten it to a raw binary. We can do that with objcopy
(Included in the arm-none-eabi
toolchain talked about earlier):
arm-none-eabi-objcopy -O binary ./target/armv7a-none-eabi/release/led ./kernel.img
Loading the binary into SD card
We need to download some files: Raspi Repo with necessary files
We need to download and copy the necessary files into the SD:
bootcode.bin
fixup.dat
start.elf
Then add a new config.txt
file with the following content:
arm_64bit=0
We can now copy our binary into the SD card:
cp ./image.bin /Volumes/boot/
And the SD card is ready to be plugged in the Raspi.
LED blinking
Now that we have a binary that can be loaded by the Raspi bootloader, we can start writing some code.
GPIO
We want to blink an LED so we need to control the GPIO. The GPIO is controlled by setting some values to addresses in the processor memory. For that we’ll need to look at the BCM2835 Datasheet - ARM Peripherals documentation.
GPIO Section is at page 89 : 6. General Purpose I/O (GPIO)
We’ve chosen GPIO 2 (Pin 3) and we need to:
- Set the GPIO Function Select 1 to output
- Set the GPIO On and Off
Addresses
The peripheral addresses are not directly accessed through memory addresses, but are mapped through some stuff, this is why the datasheet says:
Physical addresses range from 0x3F000000 to 0x3FFFFFFF for peripherals. The bus addresses for peripherals are set up to map onto the peripheral bus address range starting at 0x7E000000. Thus a peripheral advertised here at bus address 0x7Ennnnnn is available at physical address 0x3Fnnnnnn.
This means that each time the datasheet says 0x7Ennnnnn
we need to set 0x3Fnnnnnn
as the actual address.
GPIO Addresses
These are the 3 addresses we’l need to use:
Converted:
0x3F20 0000
: GPIO Function Select 00x3F20 001C
: GPIO Set 00x3F20 0028
: GPIO Clear 0
GPIO Function Select
The GPIO Function Select needs to be set to 001
to set the GPIO as output. We can do that by setting the 3 bits 6 to 8
to 001
:
GPIO Set and Clear
In order to set and clear the GPIO we need to set the bit corresponding to the GPIO we want to set/clear. For GPIO 2 (Pin 3) we need to set the bit 8
to 1
in the GPIO Set and Clear registers.
Rust Code to blink the LED
unsafe
{
// Set GPIO 2 (PIN 3) as output
core::ptr::write_volatile(0x3F200000 as *mut u32, 1 << 6);
loop
{
// Set GPIO 2 (PIN 3) to HIGH
core::ptr::write_volatile(0x3F20001C as *mut u32, 1 << 2);
// Wait
for _ in 0..500000
{
asm!("nop");
}
// Set GPIO 2 (PIN 3) to LOW
core::ptr::write_volatile(0x3F200028 as *mut u32, 1 << 2);
// Wait
for _ in 0..500000
{
asm!("nop");
}
}
}
Let’s see the light!
We need to:
- Setup the LED circuit
- Compile into ELF
- Flatten the binary
- push it to an SD card
- Boot the Raspi
- Enjoy
And voilà!
Code
Sources
BAREMETAL RUST Runs on EVERYTHING, Including the Raspberry Pi
BCM2835-ARM-Peripherals.pdf
https://www.rust-lang.org
Images
https://live.staticflickr.com/4754/39697205632_dcf16271dd_b.jpg