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 Prototyper which will provide binaries for each platform. If you are a vendor or contributor who wants to adapt RustSBI to your new product or board, you may consider adapting the Prototyper first to get your board adapted in a short period of time; or build a discrete crate if your team has plenty of time working on this board.

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

The crate rustsbi acts as core trait, extension abstraction and implementation generator 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 the operating system kernel. An SBI implementation will allow further bootstrapping the kernel and provide a supportive 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 the supervisor software

SBI environment features include boot sequence and an S-mode environment. To bootstrap the S-mode software, the kernel (or other supervisor-level software) would be loaded into an implementation-defined address, then RustSBI will prepare an environment and enter the S-mode software on the S-mode visible harts. If the firmware environment provides other boot-loading standards upon SBI, following bootstrap process will provide further information on the supervisor software.

§Make SBI environment calls

To use the underlying environment, the supervisor either uses SBI calls or run software implemented instructions. SBI calls are similar to the system calls for operating systems. The SBI extensions, whether defined by the RISC-V SBI Specification or by custom vendors, would either consume parameters only, or defined a list of functions identified by function IDs, where the S-mode software would pick and call. Definition of parameters varies between extensions and functions.

At this point, we have picked up an extension ID, a function ID, and a few SBI call parameters. Now instead of a conventional jump instruction, the software would invoke a special ecall instruction on supervisor level to transfer the control flow, resulting into a trap to the SBI environment. The SBI environment will process the ecall and fill in SBI call results, similar to what an operating system would handle system calls from user level.

All SBI calls would return two integers: the error number and the return value. The error number will tell if the SBI call has been successfully proceeded, or which error has occurred. The return value indicates the result of a successful SBI call, whose meaning is different among different SBI extensions.

§Call SBI in Rust or other programming languages

Making SBI calls is similar to making system calls; RISC-V SBI calls pass extension ID, function ID (if applicable) and parameters in integer registers.

The extension ID is required to put on register a7, function ID 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, the S-mode software would invoke an ecall instruction. The SBI call will return two values placed in a0 and a1 registers; the error value could be read from a0, and the return value is placed into a1.

In Rust, we would usually use crates like sbi-rt to hide implementation details and focus on supervisor software development. However, if in some cases we have to write them in inline assembly, here is an example to do this:

#[inline(always)]
fn sbi_call_2(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")))]
        () => {
            let _ = (extension, function, arg0, arg1);
            unimplemented!("not RISC-V instruction set architecture")
        }
    };
    SbiRet { error, value }
}

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

SBI calls may fail, returning the corresponding type of error in an error code. In this example, we only take the value, but in complete designs we should handle the error code returned by SbiRet thoroughly.

In other programming languages, similar methods may be achieved by inline assembly or other features; its documentation may suggest which is the best way to achieve this.

§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 Prototyper. Developers may choose the Prototyper 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 provides a different set of features dedicated to emulated or virtual environments.

§Use the Prototyper

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

You may find further documents on RustSBI Prototyper 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 detailed SoC low-power features, management cores and other features would be used in the SBI implementation.

RustSBI supports discrete package development out-of-box. If we are running on bare-metal, we can create a new #![no_std] bare-metal package, add runtime code or use runtime libraries to get started. Then, we add the following lines to Cargo.toml:

[dependencies]
rustsbi = { version = "0.4.0", features = ["machine"] }

The feature machine indicates that RustSBI library is run directly on machine mode RISC-V environment; it will use the riscv crate to fetch machine mode environment information by CSR instructions, which fits our demand of using it on bare metal RISC-V.

After hardware initialization process, the part of firmware with RustSBI linked should run on memory blocks with fast access, as it would be called frequently by operating system. If the implementation treats the supervisor as a generator of traps, we insert rustsbi::RustSBI implementation in a hart executor structure.

use rustsbi::RustSBI;

/// Executes the supervisor within.
struct Executor {
    ctx: SupervisorContext,
    /* other environment variables ... */
    sbi: MySBI,
    /* custom_1: CustomSBI, ... */
}

#[derive(RustSBI)]
struct MySBI {
    console: MyConsole,
    // todo: other extensions ...
    info: MyEnvInfo,
}

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(), the SBI implementation would process the trap returned with the RustSBI instance in executor. Call the function handle_ecall (generated by the derive macro RustSBI) and fill in a 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 the supervisor execution function in your bare metal package to finish the discrete package project. Here is an example of a bare-metal entry; actual projects would either use a library for runtime, or write assemble code only if necessary.

#[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 language runtime
        // only clear bss if hart ID 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(_hart_id: 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 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 go here
    }
}

Now RustSBI would run on machine environment, a kernel may be started 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, the first one being relatively small, not fast but instantly available after chip start, while the second one is larger 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 a situation, RustSBI implementation should be treated as or concatenated to the second stage bootloader, and the first stage could be a standalone binary package bundled with it.

§Hypervisor and emulator development with RustSBI

RustSBI crate supports developing 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 the demands of virtualized systems, hypervisors may either provide transparent information from the 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 accepting an implementation of the EnvInfo trait. If RISC-V hypervisors choose to use existing information on the current machine, it may require calling underlying M-mode environment using SBI calls and fill in information into the variable implementing trait EnvInfo. If hypervisors use customized information other than taking the same one from the environment they reside in, they may build custom structures implementing EnvInfo to provide customized machine information. Deriving a RustSBI instance without bare-metal support would require an EnvInfo implementation as an input of the derive-macro.

To begin with, include the RustSBI library in file Cargo.toml:

[dependencies]
rustsbi = "0.4.0"

This will disable the default feature machine which will assume that RustSBI runs on M-mode directly, which is not appropriate for our purpose. After that, define an SBI structure and derive its RustSBI implementation using #[derive(RustSBI)]. The defined SBI structure can be placed in a virtual machine structure representing a control flow executor to prepare for SBI environment:

use rustsbi::RustSBI;

#[derive(RustSBI)]
struct MySBI {
    // add other fields later ...
    // The environment information must be provided on
    // non-bare-metal RustSBI development.
    info: MyEnvInfo,
}

struct VmHart {
    // other fields ...
    sbi: MySBI,
}

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 sbi.handle_ecall function. RustSBI will handle with SBI standard constants, call the corresponding extension field and provide parameters according to the extension and function IDs (if applicable).

Crate rustsbi adapts to standard RISC-V SBI calls. If the hypervisor has 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_sbi.handle_ecall(hart.trap_params());
        // If the 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 a 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.sbi.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 accelerated 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 similar process with writing hypervisors, see Hypervisors using RustSBI for details.

§Download binary file: the Prototyper vs. discrete packages

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

The RustSBI Prototyper is a universal support package provided by RustSBI ecosystem. It is designed to save development time while providing most SBI features possible. It also includes a simple test kernel to allow testing SBI implementations on current environment. Users may choose to download from Prototyper 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 the Prototyper.

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 maintenance situation would vary between vendors, and it would likely 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 depend on whether the vendor provides it.

To download binary package for the Prototyper, visit its project website for a download link. To download them for discrete packages, RustSBI users may visit distribution sources of SoC or board manufacturers. Additionally, users may visit the awesome page for a curated list of both Prototyper 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 and a derive-macro to provide basic SBI features. When building for a specific platform, implement traits in this library and pass the types into a structure to derive RustSBI macro onto. After that, handle_ecall would be called in the platform-specific exception handler. The derive macro RustSBI would dispatch parameters from supervisor to the trait implementations to handle the SBI calls.

The library also implements useful constants which may help with platform-specific binaries. The LOGO and information on VERSION can be printed if necessary on SBI initialization processes.

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

§Hardware discovery and feature detection

According to the RISC-V SBI specification, the 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, ACPI, vendor-specific ones or upcoming configptr CSRs) 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 require to depend on operating system calls or use a signal-trap procedure to detect any RISC-V core features.

Re-exports§

pub extern crate sbi_spec as spec;

Structs§

Forward
Forwards SBI calls onto current supervisor environment.
HartMask
Hart mask structure in SBI function calls.
Physical
Physical slice wrapper with type annotation.
SbiRet
SBI functions return type.
SharedPtr
Shared memory physical address raw pointer with type annotation.

Constants§

LOGO
The RustSBI logo without blank lines on the beginning.
VERSION
RustSBI version as a string.

Traits§

Console
Debug Console extension.
Cppc
SBI CPPC support extension.
EnvInfo
Machine environment information.
Fence
Remote Fence support extension.
Hsm
Hart State Management extension.
Ipi
Inter-processor interrupt support.
Nacl
Nested Acceleration extension.
Pmu
Performance Monitoring Unit extension.
Reset
System Reset extension.
RustSBI
RustSBI environment call handler.
Sta
Steal-time Accounting extension.
Susp
System-Suspend extension.
Timer
Timer programmer support extension.

Derive Macros§

RustSBI
Generate RustSBI implementation for structure of each extension.