.. | ||
.cargo | ||
raspi3_boot | ||
src | ||
Cargo.lock | ||
Cargo.toml | ||
kernel8 | ||
kernel8.img | ||
link.ld | ||
Makefile | ||
README.md |
Tutorial 07 - Abstraction
This is a short one regarding code changes, but has lots of text because two important Rust principles are introduced: Abstraction and modularity.
From a functional perspective, this tutorial is the same as 05_uart0
, but with
the key difference that we threw out all manually crafted assembler. Both the
main and the boot crate do not use #![feature(global_asm)]
or
#![feature(asm)]
anymore. Instead, we pulled in the cortex-a crate,
which now provides cortex-a
specific features like register access or safe
wrappers around assembly instructions.
For single assembler instructions, we now have the cortex-a::asm
namespace,
e.g. providing asm::nop()
.
For registers, there is cortex-a::regs
. The interface is the same as we have
it for MMIO accesses, aka provided by register-rs and therefore
based on tock-regs. For registers like the stack pointer, which are
generally read and written as a whole, there's the common get() and
set() functions which take and return primitive integer types.
Registers that are divided into multiple fields, like CNTP_CTL_EL0
, on the
other hand, are backed by a respective type definition that allow
for fine-grained reading and modifications.
The register API is based on the tock project's register interface. Please see their homepage for all the details.
To some extent, this namespacing also makes our code more portable. For example,
if we want to reuse parts of it on another processor architecture, we could pull
in the respective crate and change our use-clause from use cortex-a::asm
to
use new_architecture::asm
. Of course this also demands that both crates adhere
to a common set of wrappers that provide the same functionality. Assembler and
register instructions like we use them here are actually a weak example. Where
this modular approach can really pay off is for common peripherals like timers
or memory management units, where implementations differ between processors, but
usage is often the same (e.g. setting a timer for x amount of microseconds).
In Rust, we have the Embedded Devices Working Group, which among other goals, tries to establish a common set of wrapper- and interface-crates that introduce abstraction on different levels of the system. Check out the Awesome Embedded Rust list for an overview.
Boot Code
Like mentioned above, we threw out the boot_cores.S
assembler file and
replaced it with a Rust function. Why? Because we can, for the fun of it.
#[link_section = ".text.boot"]
#[no_mangle]
pub unsafe extern "C" fn _boot_cores() -> ! {
use cortex_a::{asm, regs::mpidr_el1::*, regs::sp::*};
const CORE_MASK: u64 = 0x3;
const STACK_START: u64 = 0x80_000;
match MPIDR_EL1.get() & CORE_MASK {
0 => {
SP.set(STACK_START);
reset()
}
_ => loop {
// if not core0, infinitely wait for events
asm::wfe();
},
}
}
Since this is the first code that the RPi3 will execute, the stack has not been set up yet. Actually it is this function that will do it for the first time. Therefore, it is important to check that code generated from this function does not call any subroutines that need a working stack themselves.
The get()
and asm
wrappers that we use from the cortex-a
crate are all
inlined, so we fulfill this requirement. The compilation result of this function
should yield something like the following, where you can see that the stack
pointer is not used apart from ourselves setting it.
[andre:/work] $ cargo objdump --target aarch64-raspi3-none-elf.json -- -disassemble -print-imm-hex kernel8
[...] (Some output omitted)
_boot_cores:
80000: a8 00 38 d5 mrs x8, MPIDR_EL1
80004: 1f 05 40 f2 tst x8, #0x3
80008: 60 00 00 54 b.eq #0xc <_boot_cores+0x14>
8000c: 5f 20 03 d5 wfe
80010: ff ff ff 17 b #-0x4 <_boot_cores+0xc>
80014: e8 03 09 32 orr w8, wzr, #0x800000
80018: 1f 01 00 91 mov sp, x8
8001c: 35 02 00 94 bl #0x8d4 <raspi3_boot::reset::h90bc56752de44d1b>
It is important to always manually check this, and not blindly rely on the compiler.
Test it
Since this is the first tutorial after we've written our own bootloader over serial, you can now for the first time test this convenient interface:
make raspboot