The Hello World!!
for bare metal environments. A must do from my point of view 😄 to
get things started and it is best suited to verify the tools and the toolchain are smoothly working
and creating a run-able kernel for the Raspberry Pi.
As the Raspberry Pi that runs without any OS is usually not able to display a greeting message to a connected screen we will use the provided GPIO pins to connect a LED to it at let the Raspberry Pi blink this LED to greet the world.
It is assumed for this tutorial that you have performed the initial setup as described in this README
The easiest way to get a basic project struture to begin with is by using an existing project template. A mimimal version could be found here.
To use it you use the following command:
$> cargo generate --git https://github.com/RusPiRo/ruspiro_templates.git --branch templates/01_minimal --name hello_led
This will create a new subfolder hello-led
and create the files and folders based on the template
in the github repo.
For new-comers lets start with the basic folder structure of our project (as it is structured in this repo and how it will look like at your end, if you used the template mentionned above):
01_BLINKLED // project root folder (will be the name of the project you have choosen)
├─ src // here goes all the rust source files
│ └─ kernel.rs // the current one and only rust source file containing our code to execute on the Raspberry Pi
├─ Cargo.toml // the build configuration file to tell Rust how to build our bare metal binary and what we
│ // depend on (which crates to incorporate into the build)
├─ build.rs // specif build script to provide the correct linker script file
├─ Makefile.toml // the makefile to execute the rust build with *cargo make*
├─ LICENSE-* // the license files for your crate
└─ README.md // well, this is the file you are currently reading, at this very moment :)
The maybe first thing to do is to describe the package we would like to build adjusting the
Cargo.toml
file. The first chapter [package]
specifies some meta-data. The minimum required entries here
are already provided by the template. The publish = false
prevents accidently publishing of this
crate to the public crate library of Rust crates.io.
You may want to adjust the description of your crate:
[package]
description = """
This is a RusPiRo template
"""
The second chapter [[bin]]
defines the requested output Rust should create while building this
crate. This could either be a library or a binary. In our case we will build a binary that will be
deployed to the Raspberry Pi. The configuration defines the binary name and the entry rust file to
be used for compilation. Any functionality that should be compiled into the binary must be referenced
in this main file.
[[bin]]
name = "kernel" # name of the binary to build
path = "./src/kernel.rs" # main source file to build the binary from
Finally in the third chapter [dependencies]
we list all dependend crates (libraries so to speek)
we would like to use functionality from in our own crate. Those crates can be found on
crates.io. You will also find all the ruspiro-*
crates there with links to
their documentation and the github repo's.
To start a lightweight first bare metal kernel for Raspberry Pi we would need the following dependencies to be configured:
[dependencies]
ruspiro-boot = "0.3"
ruspiro-allocator = "0.4"
ruspiro-gpio = "0.4"
ruspiro-timer = "0.4"
We will also specify a features that will be passed to the dependend crates where necessary to choos the Raspberry Pi model we are targeting. This is required as different models may have different MMIO register base addresses.
[features]
ruspiro_pi3 = [
"ruspiro-boot/ruspiro_pi3",
"ruspiro-gpio/ruspiro_pi3",
"ruspiro-timer/ruspiro_pi3"
]
Except the ruspiro-gpio
and the ruspiro-timer
the dependency section should be prefilled from the
template.
What does those dependencies provide:
Dependent crate | Description |
---|---|
ruspiro-boot |
Booting the Raspberry Pi in a baremetal setup without an OS requires some initial assembly and preparation. This crate provides all the required boot strapping code and is responsible to kick off the cores of the Raspberry Pi and branch into the code written in Rust. |
ruspiro-allocator |
Providing a lightweight HEAP memory allocator |
ruspiro-gpio |
This is the API crate to access the GPIO pins available with the Raspberry Pi. It hides the complexity of the setup and usage of the different pin's behind easy to consume function calls. |
ruspiro-timer |
Simple timing functions to allow to pause execution for a specific amount of time. The timing is done based on the internal free running counter of the Raspberry Pi system timer that is incremented each micro second. |
Even though the functionality we will implement in Rust could be organized accross different files
and folders ( so called modules), there is one main file, given in the Cargo.toml
section
[[bin]]
that is carrying our entry point into our bare metal kernel. Let's take a look into the
structure of this file.
At the very beginning we provide two compiler attributes:
#![no_std]
#![no_main]
💡 Both are quite important:
The first one tells the Rust compiler that we do not want to use the Rust standard library. With this we get only access to thecore
functions and features of Rust (check-out the documentation for details). The required functions the documentation assumes to be there are provided by theruspiro-boot
crate. The second one ensures that the Rust compiler and linker should not expect amain
function to be present. Theruspiro-boot
crate provides the necessary entry point.
The next part is to refer to the dependent crates to have access to the functions they provide:
#[macro_use]
extern crate ruspiro_boot; // link in the bootstrap functions
extern crate ruspiro_allocator; // link in the custom heap allocator
use ruspiro_gpio::GPIO; // provide access to the GPIO api
use ruspiro_timer as timer; // provide access to the timer function with an alias 'timer'
use ruspiro_mmu as mmu; // provide access to the Memory Management Unit of the Raspberry Pi
After all the declarations its now time to implement the functionality we would like our kernel to
perform. To provide the implementations for the entry points the bootstrapper is calling the
ruspiro-boot
comes with 2 macros. So you define 2 functions, one for a one-time initialization
and a second one for the main processing loop. Then you use the afformentioned macros to "mark" those
functions as the required entry points. This looks like this in code:
// Set the function that is called on each core once it is alive and prepared to branch
// into the Rust 'world'
come_alive_with!(alive);
/// Any one-time initialization might be done here.
fn alive(_core: u32) {
// nothing to do at this time...
// configure the mmu as we will deal with atomic operations (within the memory
// allocator that is used by the singleton under the hood to store the data
// within the HEAP)
// use some arbitrary values for VideoCore memory start and size. This is fine
// as we will use a small lower amount of the ARM memory only.
unsafe { mmu::initialize(core, 0x3000_0000, 0x001_000) };
}
// Set the function that is called on each core after the ``come_alive_with`` function has
// finished it's preparation. This function is intended to never return as there is nothing
// to be executed on the cores once this kernel has done what it is supposed to
run_with!(running);
/// Do the actual work on any core
fn running(core: u32) -> ! {
// based on the core provided use a different GPIO pin to blink a different LED
let pin = match core {
0 => GPIO.with_mut(|gpio| gpio.get_pin(17)).unwrap().to_output(),
1 => GPIO.with_mut(|gpio| gpio.get_pin(18)).unwrap().to_output(),
2 => GPIO.with_mut(|gpio| gpio.get_pin(20)).unwrap().to_output(),
3 => GPIO.with_mut(|gpio| gpio.get_pin(21)).unwrap().to_output(),
_ => unreachable!()
};
// now blink the LED with an intervall based on the core number to visualize this is really the different core
// blinking the LED
loop {
pin.high();
timer::sleep(timer::Useconds(100_000*(core + 1) as u64));
pin.low();
timer::sleep(timer::Useconds(50_000*(core + 1) as u64));
} // never return here...
}
This code basically aquires - based on the core id the functions enters with - a dedicated GPIO pin
and configures the same as Output
. This allows to set the pin to high()
which will lit the
LED and to low()
which will clear the LED respectively. The loop will apply some core number
specific sleep intervall to let the LED for each core blink in a bit different intervall.
⚠️ HINT: Please ensure to put a resistor between the LED and ground when connecting the LED to the GPIO pin.
If all tools has been successfully configured ( as described here), building the kernel could be done by executing one the following command in the projects root folder:
$> cargo make pi3
This might take a while at the first attempt as it does download the dependend crates from crates.io and does cross compile the Rust core library. As the build process is incremental by default the next times the build will be much more faster.
The result of a successful execution of the build is the binary file kernel8.img
in the target
subfolder.
There are two options available to deploy the kernel to your Raspberry Pi:
Put this file to the SD card of your Raspberry Pi alongside
with the bootcode.bin
, start.elf
and fixup.dat
. Those files could be found here or the latest
version on the official Raspberry Pi firmware repo.
Now you could put this card into the Raspberry Pi and power it up. If you properly connected the
LED's to the GPIO pins 17, 18, 20 and 21 they should blink. HEUREKA. This way of deploying
requires you to remove the SD card, update the kernel image file and then insert it again into the
Raspberry Pi for a next round of testing. This "SD-Card-Dance" will become cumbersome quite soon.
This approach eliminates the "SD-Card-Dance". You will put all files contained in the RPi
subfolder of this repo to your SD card. Including the kernel8.img
which actually is the bootloader.
Connect your Raspberry Pi miniUART GPIO's to the serial Port of your development machine (usually done
through a serial TTLB-USB dongle) and power up your raspberry Pi.
Once a new kernel has been build and is present in the target
subfolder of your project just execute
$> cargo ruspiro-push -k ./target/kernel8.img -p COM5
The serial port identifier may be different on your machine - COM5
is the one on my Windows one.
For each new test cycle, just power of/on the Raspberry Pi and use the afformentioned command to push
a new version to the device.
The bootstrapping of any bare metal kernel is by default kicking off onle 1 core of the Raspberry Pi. If you'd rather like to use all 4 cores you could activate the multicore
feature for the ruspiro-boot
crate in the [dependencies]
section of the Cargo.toml
file.
[dependencies]
ruspiro-boot = { version = "0.5.3", features = ["multicore"] }