Crate rustsbi

source ·
Expand description

A minimal RISC-V’s SBI implementation library in Rust.

Note: If you are a user looking for binary distribution download for RustSBI, you may consider using the RustSBI Prototyping System which will provide binaries for each platforms. If you are a vendor or contributor who wants to adapt RustSBI to your new product or board, you may consider adapting the Prototyping System first to get your board adapted in an afternoon; you are only advised to build a discrete crate if your team have a lot of time working on this board.

For more details on binary downloads the the RustSBI Prototyping System, see section Prototyping System vs discrete packages.

The crate rustsbi acts as core trait and instance abstraction of the RustSBI ecosystem.

What is RISC-V SBI?

RISC-V SBI is short for RISC-V Supervisor Binary Interface. SBI acts as an interface to environment for your operating system kernel. An SBI implementation will allow furtherly bootstrap your kernel, and provide an environment while the kernel is running.

More generally, The SBI allows supervisor-mode (S-mode or VS-mode) software to be portable across all RISC-V implementations by defining an abstraction for platform (or hypervisor) specific functionality.

Use RustSBI services in your supervisor software

SBI environment features include boot sequence and a kernel environment. To bootstrap your kernel, place kernel into RustSBI implementation defined address, then RustSBI will prepare an environment and call the entry function on this address.

Make SBI environment calls

To use the kernel environment, you either use SBI calls or emulated instructions. SBI calls are similar to operating systems’ syscalls. RISC-V SBI defined many SBI extensions, and in each extension there are different functions, you should pick a function before calling. Then, you should prepare some parameters, whose definition are not the same among functions.

Now you have an extension number, a function number, and a few SBI call parameters. You invoke a special ecall instruction on supervisor level, and it will trap into machine level SBI implementation. It will handle your ecall, similar to your kernel handling system calls from user level.

SBI functions return two values other than one. First value will be an error number, it will tell if SBI call have succeeded, or which error have occurred. Second value is the real return value, its meaning is different according to which function you calls.

Call SBI in different programming languages

Making SBI calls are similar to making system calls.

Extension number is required to put on register a7, function number on a6 if applicable. Parameters should be placed from a0 to a5, first into a0, second into a1, etc. Unused parameters can be set to any value or leave untouched.

After registers are ready, invoke an instruction called ecall. Then, the return value is placed into a0 and a1 registers. The error value could be read from a0, and return value is placed into a1.

In Rust, here is an example to call SBI functions using inline assembly:

#[inline(always)]
fn sbi_call(extension: usize, function: usize, arg0: usize, arg1: usize) -> SbiRet {
    let (error, value);
    match () {
        #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
        () => unsafe { asm!(
            "ecall",
            in("a0") arg0, in("a1") arg1,
            in("a6") function, in("a7") extension,
            lateout("a0") error, lateout("a1") value,
        ) },
        #[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))]
        () => {
            drop((extension, function, arg0, arg1));
            unimplemented!("not RISC-V instruction set architecture")
        }
    };
    SbiRet { error, value }
}

#[inline]
pub fn get_spec_version() -> SbiRet {
    sbi_call(EXTENSION_BASE, FUNCTION_BASE_GET_SPEC_VERSION, 0, 0)
}

SBI functions would return a result thus some of these may fail. In this example we only take the value, but in complete designs we should handle the error returned by SbiRet.

You may use other languages to call SBI environment. In C programming language, we can call like this:

#define SBI_CALL(ext, funct, arg0, arg1, arg2, arg3) ({ \
    register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0); \
    register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1); \
    register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2); \
    register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3); \
    register uintptr_t a6 asm ("a6") = (uintptr_t)(funct); \
    register uintptr_t a7 asm ("a7") = (uintptr_t)(ext); \
    asm volatile ("ecall" \
        : "+r" (a0), "+r" (a1) \
        : "r" (a1), "r" (a2), "r" (a3), "r" (a6), "r" (a7) \
        : "memory") \
    {a0, a1}; \
})

#define SBI_CALL_0(ext, funct) SBI_CALL(ext, funct, 0, 0, 0, 0)

static inline sbiret get_spec_version() {
    SBI_CALL_0(EXTENSION_BASE, FUNCTION_BASE_GET_SPEC_VERSION)
}

Implement RustSBI on machine environment

Boards, SoC vendors, machine environment emulators and research projects may adapt RustSBI to specific environments. RustSBI project supports these demands either by discrete package or the Prototyping System. Developers may choose the Prototyping System to shorten development time, or discrete packages to include fine-grained features.

Hypervisor and supervisor environment emulator developers may refer to Hypervisor and emulator development with RustSBI for such purposes as RustSBI provide different set of features dedicated for emulated or virtual environments.

Use the Prototyping System

The RustSBI Prototyping System aims to get your platform working with SBI in an afternoon. It supports most RISC-V platforms available by providing scalable set of drivers and features. It provides custom features such as Penglai TEE, DramForever’s emulated hypervisor extension, and Raven the firmware debugger framework.

You may find further documents on RustSBI Prototyping System repository.

Discrete RustSBI package on bare metal RISC-V hardware

Discrete packages provide developers with most scalability and complete control of underlying hardware. It is ideal if advanced low power features, management cores and other features should be used in this implementation.

RustSBI supports discrete package by default. Create a new #![no_std] bare-metal package to get started. Add following lines to Cargo.toml:

[dependencies]
rustsbi = "0.3.2"

After hardware initialization process, the part of firmware with RustSBI linked should run on memory blocks with fast accesses, as it would be called frequently by operating system. If the supervisor is called by trap generator semantics, insert rustsbi::RustSBI structure in your hart executor structure.

use rustsbi::RustSBI;

/// Executes the supervisor within.
struct Executor {
    ctx: SupervisorContext,
    /* other environment variables ... */
    sbi: RustSBI<Clint, Clint, MyPlatRfnc, MyPlatHsm, MyBoardPower, MyPlatPmu>,
    /* custom_1: CustomSBI<...> */
}

impl Executor {
    /// A function that runs the provided supervisor, uses `&mut self` for it
    /// modifies `SupervisorContext`.
    ///
    /// It returns for every Trap the supervisor produces. Its handler should read
    /// and modify `self.ctx` if necessary. After handled, `run()` this structure
    /// again or exit execution process.
    pub fn run(&mut self) -> Trap {
        todo!("fill in generic or platform specific trampoline procedure")
    }
}

After each run(), process the trap returned with the RustSBI instance in executor. Call RustSBI::handle_ecall and fill in developer provided SupervisorContext if necessary.

/// Board specific power operations.
enum Operation {
    Reboot,
    Shutdown,
}

/// Execute supervisor in given board parameters.
pub fn execute_supervisor(board_params: BoardParams) -> Operation {
    let mut exec = Executor::new(board_params);
    loop {
        let trap = exec.run();
        if let Trap::Exception(Exception::SupervisorEcall) = trap.cause() {
            let ans = exec.sbi.handle_ecall(
                exec.sbi_extension(),
                exec.sbi_function(),
                exec.sbi_params(),
            );
            if ans.error == MY_SPECIAL_EXIT {
                break Operation::from(ans)
            }
            // This line would also advance `sepc` with `4` to indicate the `ecall` is handled.
            exec.fill_sbi_return(ans);
        } else {
            // other trap types ...
        }
    }
}

Now, call supervisor execution function in your bare metal package to finish the discrete package project.

#[naked]
#[link_section = ".text.entry"]
#[export_name = "_start"]
unsafe extern "C" fn entry() -> ! {
    #[link_section = ".bss.uninit"]
    static mut SBI_STACK: [u8; LEN_STACK_SBI] = [0; LEN_STACK_SBI];

    // Note: actual assembly code varies between platforms.
    // Double check documents before continue on.
    core::arch::asm!(
        // 1. Turn off interrupt
        "csrw  mie, zero",
        // 2. Initialize programming langauge runtime
        // only clear bss if hartid is zero
        "csrr  t0, mhartid",
        "bnez  t0, 2f",
        // clear bss segment
        "la  t0, sbss",
        "la  t1, ebss",
        "1:",
        "bgeu  t0, t1, 2f",
        "sd  zero, 0(t0)",
        "addi  t0, t0, 8",
        "j  1b",
        "2:",
        // 3. Prepare stack for each hart
        "la  sp, {stack}",
        "li  t0, {per_hart_stack_size}",
        "csrr  t1, mhartid",
        "addi  t1, t1, 1",
        "1: ",
        "add  sp, sp, t0",
        "addi  t1, t1, -1",
        "bnez  t1, 1b",
        "j  {rust_main}",
        // 4. Clean up
        "j  {finalize}",
        per_hart_stack_size = const LEN_STACK_PER_HART,
        stack = sym SBI_STACK,
        rust_main = sym rust_main,
        finalize = sym finalize,
        options(noreturn)
    )
}

/// Power operation after main function
enum Operation {
    Reboot,
    Shutdown,
    // Add board specific low power modes if necessary. This will allow the
    // function `finalize` to operate on board specific power management chips.
}

/// Rust entry, call in `entry` assembly function
extern "C" fn rust_main(_hartid: usize, opaque: usize) -> Operation {
    // .. board initialization process ...
    let board_params = board_init_once();
    // .. print necessary information and rustsbi::LOGO ..
    print_information_once();
    // execute supervisor, return as Operation
    execute_supervisor(&board_params)
}

/// Perform board specific power operations
///
/// The function here provides a stub to example power operations.
/// Actual board developers should provide with more practical communications
/// to external chips on power operation.
unsafe extern "C" fn finalize(op: Operation) -> ! {
    match op {
        Operation::Shutdown => {
            // easiest way to make a hart look like powered off
            loop { wfi(); }
        }
        Operation::Reboot => {
            // easiest software reset is to jump to entry directly
            entry()
        }
        // .. more power operations goes here ..
    }
}

Now RustSBI would run on machine environment, you may start a kernel or use an SBI test suite to check if it is properly implemented.

Some platforms would provide system memory under different grades in speed and size to reduce product cost. Those platforms would typically provide two parts of code memory, first one being relatively small, not fast but instantly available after chip start, while the second one is larger in size but typically requires memory training. The former one would include built-in SRAM memory, and the later would include external SRAM or DDR memory. On those platforms, a first stage bootloader is typically needed to train memory for later stages. In such situation, RustSBI implementation should be linked or concated to the second stage bootloader, and the first stage could be a standalone binary package bundled with it.

Discrete RustSBI package by singleton based interface

Note: Using singleton based RustSBI interface is discouraged in newer designs, for it requires nightly Rust and global static memory. It takes extra bss and data storage to build a global singleton interface. New designs should follow the instance based interface to build discrete RustSBI packages.

Other than instance based interface, some users may find it convenient by using global singleton semantics. To use it users should enable the singleton feature by:

[dependencies]
rustsbi = { version = "0.3.2", features = ["singleton"] }

RustSBI library will disable all instance based interfaces but provide init_* functions to allow initialize global RustSBI singleton instance. By enabling this feature, RustSBI uses unstable Rust features to create a universal lock structure by using atomic amo instructions other than lr/sc.

Hypervisor and emulator development with RustSBI

RustSBI crate supports to develop RISC-V emulators, and both Type-1 and Type-2 hypervisors. Hypervisor developers may find it easy to handle standard SBI functions with an instance based RustSBI interface.

Hypervisors using RustSBI

Both Type-1 and Type-2 hypervisors on RISC-V run on HS-mode hardware. Depending on demands of virtualized systems, hypervisors may either provide transparent information from host machine or provide another set of information to override the current environment. Notably, RISC-V hypervisors do not have direct access to machine mode (M-mode) registers.

RustSBI supports both by providing a MachineInfo structure in instance based interface. If RISC-V hypervisors choose to use existing information on current machine, it may require to call underlying M-mode environment using SBI calls and fill in information into MachineInfo. If hypervisors use customized information other than taking the same one from the environment they reside in, they may fill in custom one into MachineInfo structures. When creating RustSBI instance, MachineInfo structure is required as an input of constructor.

To begin with, disable default features in file Cargo.toml:

[dependencies]
rustsbi = { version = "0.3.2", default-features = false }

This will disable default feature machine which will assume that RustSBI runs on M-mode directly, which is not appropriate in our purpose. After that, a RustSBI instance may be placed in the virtual machine structure to prepare for SBI environment:

struct VmHart {
    // other fields ...
    env: RustSBI</* Types, .. */>,
}

When the virtual machine hart traps into hypervisor, its code should decide whether this trap is an SBI environment call. If that is true, pass in parameters by env.handle_ecall function. RustSBI will handle with SBI standard constants, call corresponding extension module and provide parameters according to the extension and function IDs.

Crate rustsbi adapts to standard RISC-V SBI calls. If the hypervisor have custom SBI extensions that RustSBI does not recognize, those extension and function IDs can be checked before calling RustSBI env.handle_ecall.

let mut hart = VmHart::new();
loop {
    let trap = hart.run();
    if let Trap::Exception(Exception::SupervisorEcall) = trap.cause() {
        // Firstly, handle custom extensions
        let my_extension_sbiret = hart.my_extension_env.handle_ecall(hart.trap_params());
        // If custom extension handles correctly, fill in its result and continue to hart.
        // The custom handler may handle `probe_extension` in `base` extension as well
        // to allow detections to whether custom extension exists.
        if my_extension_sbiret != SbiRet::not_supported() {
            hart.fill_in(my_extension_sbiret);
            continue;
        }
        // Then, if it's not a custom extension, handle it using standard SBI handler.
        let standard_sbiret = hart.env.handle_ecall(hart.trap_params());
        hart.fill_in(standard_sbiret);
    }
}

RustSBI would interact well with custom extension environments in this way.

Emulators using RustSBI

RustSBI library may be used to write RISC-V emulators. Other than hardware accelereted binary translation methods, emulators typically do not use host hardware specific features, thus may build and run on any architecture. Like hardware RISC-V implementations, software emulated RISC-V environment would still need SBI implementation to support supervisor environment.

Writing emulators would follow the similiar process with writing hypervisors, see Hypervisors using RustSBI for details.

Download binary file: the Prototyping System vs discrete packages

RustSBI ecosystem would typically provide support for most platforms. Those support packages would be provided either from the RustSBI Prototyping System or vendor provided discrete SBI implementation packages.

The RustSBI Prototyping System is a universal support package provided by RustSBI ecosystem. It is designed to save development time while providing most SBI feature possible. It also includes a universal test kernel to allow testing SBI implementations on current environment. Users may choose to download from Prototyping System repository to get various types of RustSBI packages for their boards. Vendors and contributors may find it easy to adapt new SoCs and boards into Prototyping System.

Discrete SBI packages are SBI environment support packages specially designed for one board or SoC, it will be provided by board vendor or RustSBI ecosystem. Vendors may find it easy to include fine grained features in each support package, but the maintainence situation would vary between vendors and it would likely to cost a lot of time to develop from a bare-metal executable. Users may find a boost in performance, energy saving indexes and feature granularity in discrete packages, but it would depends on whether the vendor provide it.

To download binary package for the Prototyping System, visit its project website for a download link. To download them for discrete packages, RustSBI users may visit distribution source of SoC or board manufacturers. Additionally, users may visit the awesome page for a curated list ofboth Prototyping System and discrete packages provided by RustSBI ecosystem.

Notes for RustSBI developers

Following useful hints are for firmware and kernel developers when working with SBI and RustSBI.

RustSBI is a library for interfaces

This library adapts to individual Rust traits to provide basic SBI features. When building for own platform, implement traits in this library and pass them to the functions begin with init. After that, you may call rustsbi::ecall, RustSBI::handle_ecall or similiar functions in your own exception handler. It would dispatch parameters from supervisor to the traits to execute SBI functions.

The library also implements useful functions which may help with platform specific binaries. The LOGO can be printed if necessary when the binary is initializing.

Note that this crate is a library which contains common building blocks in SBI implementation. The RustSBI ecosystem would provide different level of support for each board, those support packages would use rustsbi crate as library to provide different type of SBI binary releases.

Hardware discovery and feature detection

According to the RISC-V SBI specification, SBI itself does not specify any method for hardware discovery. The supervisor software must rely on the other industry standard hardware discovery methods (i.e. Device Tree or ACPI) for that purpose.

To detect any feature under bare metal or under supervisor level, developers may depend on any hardware discovery methods, or use try-execute-trap method to detect any instructions or CSRs. If SBI is implemented in user level emulators, it may requires to depend on operating system calls or use the signal trap method to detect any RISC-V core features.

Legacy SBI extension

Note: RustSBI legacy support is only designed for backward compability of deprecated legacy RISC-V SBI standard. It’s disabled by default and it’s not suggested to include legacy extensions in newer firmware designs. Extensions other than legacy console are replaced by individual extensions in SBI. Kernels are not suggested to use legacy extensions in practice. If you are a kernel developer, newer designs should consider relying on each SBI extension other than legacy extensions.

The SBI includes legacy extension which dated back to SBI 0.1 specification. Most of its features are replaced by individual SBI extensions, thus the entire legacy extension is deprecated by SBI version 0.2. However, some users may find out SBI 0.1 legacy console useful in some situations even if it’s deprecated.

RustSBI keeps SBI 0.1 legacy support under feature gate legacy. To use RustSBI with deprecated legacy feature, you may change dependency configuration to:

[dependencies]
rustsbi = { version = "0.3.2", features = ["legacy"] }

Re-exports

Structs

  • Structure to build a RustSBI instance
  • Hart mask structure reference
  • Shared memory physical address range with type annotation
  • RustSBI instance including standard extensions

Constants

  • The RustSBI logo without blank lines on the beginning
  • RustSBI version as a string

Traits

  • Debug Console Extension
  • Remote fence support
  • Hart State Management Extension
  • Inter-processor interrupt support
  • Performance Monitoring Unit Extension
  • System Reset Extension
  • Timer programmer support