You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.0 KiB

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::*};

    const CORE_0: u64 = 0;
    const CORE_MASK: u64 = 0x3;
    const STACK_START: u64 = 0x80_000;

    if CORE_0 == MPIDR_EL1.get() & CORE_MASK {
        SP.set(STACK_START);
        reset()
    } else {
        // if not core0, infinitely wait for events
        loop {
            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.

ferris@box:~$ cargo objdump --target aarch64-unknown-none-softfloat -- -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 0d 32 	orr	w8, wzr, #0x80000
   80018:	1f 01 00 91 	mov	sp, x8
   8001c:	e0 01 00 94 	bl	#0x780 <raspi3_boot::reset::h6e794100bed457dc>

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