Learning Objectives

  • Understand the OpenSBI ABI.
  • Understand how to communicate with OpenSBI.
  • Understand where the operating system sits in relation to OpenSBI.
  • Understand and be able to implement OpenSBI console functions.
  • Understand and be able to implement OpenSBI hart functions.
  • Understand and be able to implement OpenSBI timer functions.

References


OpenSBI Interface

OpenSBI sits at the machine mode level, and it has a handler registered with the mtvec (machine trap vector) register. This means that if our operating system causes a trap, whether on purpose or not, then the OpenSBI system will handle it. To make requests to OpenSBI, we will use the ecall instruction, which is used to make a system call on the RISC-V architecture.


OpenSBI Functions

OpenSBI has several functions that we will be using in our operating system. First, we can use OpenSBI to handle serial I/O for us. This means we don't have to write a driver for QEMU (NS16550A) and for the board (Sifive UART). Instead, we make one system call to send characters to the console and one to receive characters from the console.

The other function we will use is starting and stopping non-bootstrap harts (hardware threads, i.e. cores). We can use OpenSBI to park a hart or to start one. This is how we will send applications to different cores.

Many functions require using the sbiret (SBI return) data structure, which is defined as the following.

  struct sbiret {
        long error;
        long value;
    };

As you can see, all you have here is a 16-bit structure with an error and a value. The interesting part is that the 16 bits is split between two registers. a0 usually contains the error portion whereas a1 contains the value portion.

The error portion of sbiret can be one of the following, whereas the value is specific to the function being executed.

ErrorError Value
Success0
Failed-1
Not supported-2
Invalid parameter-3
Denied-4
Invalid address-5
Already available-6
Error codes that can be returned in sbiret.error.

Recall that all of these functions go through a system call at the supervisor level. This will elevate privileges to the machine mode and get into the OpenSBI trap vector. These functions require a function id and an extension id (for some functions). The function id is placed in register a6 and the extension id is placed in register a7. Any parameters to the functions are placed in normal register a0 through a5. Finally, after the registers are set, the code must execute an ecall to trap the system call.


OpenSBI Console Functions

Console Putchar

To put a character to the console, we will select function 0, extension 1, which can be called using inline assembly.

RegisterValue
a0 (in)8-bit character value to put to the console
a6 (in)0
a7 (in)1
Parameters to call console putchar OpenSBI function.

Console Getchar

To get a character from the console, we will use function number 0 and extension number 2. The function does not block, meaning that if no data is available, it will still return. If nothing is available it will return 0.

RegisterValue
a0 (out)8-bit character read from the console.
a6 (in)0
a7 (in)2
Parameters to call console getchar OpenSBI function.

The value -1 (255u) will be returned if a character could not be retrieved from the console.


OpenSBI Hart Functions

We will also use OpenSBI to control harts. We mainly care about three different functions: (1) starting a hart, (2) stopping a hart, and (3) getting the status of a hart.

Starting a Hart

Starting a hart will cause OpenSBI to run in the specified privilege mode and execute an mret to get there. We can do this by entering three pieces of information: (1) the hart to start, (2) the function address to set the program counter, and (3) the privilege mode. This function returns an SbiRet structure in case an error is returned.

OpenSBI can only start a stopped hart. If the hart is running, it will return an error.

The C++ prototype for this function is as follows.

struct sbiret sbi_hart_start(unsigned long hartid, unsigned long start_addr, unsigned long priv);

Here are the parameters and return for this function.

RegisterValue
a0 (in)hartId to start
a1 (in)address to set PC to
a2 (in)privilege mode to set to (1 = supervisor, 0 = user)
a6 (in)0
a7 (in)0x48534D (HSM)
a0 (lateout)sbiret.error
a1 (lateout)sbiret.value
Parameters for OpenSBI hart_start

This function will start the given hart. It is imperative that the stack and global pointer be set properly before going into high level code.

You will see that the extension ID is 'HSM' for Hart State Management.

When a hart is started, the SATP (supervisor address translation and protection) register will be set to 0, so the MMU will be turned off. So, you need to run an assembly-level function that properly sets the trap frame, loads the registers, sets the MMU, and then jumps to the given virtual memory address. This is usually given in the ELF binary as the entry point. We will discuss this later.

Finally, a0 will contain the hartid. This is useful since mhartid is only accessible in machine mode, which we will never achieve. Finally, a1 will be the privilege mode. So, we can use registers a0 and a1 to determine the state of our hart.

Stopping a Hart

Stopping a hart can only be executed on the currently running hart by calling OpenSBI.

RegisterValue
a6 (in)1
a7 (in)0x48534D (HSM)
a0 (lateout)sbiret.error
a1 (lateout)sbiret.value
Parameters for OpenSBI hart_stop

This function only stops the hart that calls this function. The prototype for this function is as follows.

struct sbiret sbi_hart_stop();

If this function is successful, it does not return and the hart stops. Otherwise, it can return the Failed error if for some reason the hart could not be stopped.

Status of a Hart

The status of the hart can be used to enumerate the number of harts in a system as well as to see if the hart is running or stopped.

The C++ prototype of start_hart is as follows.

struct sbiret sbi_hart_status(unsigned long hartid);

The following table shows the parameters for hart_status.

RegisterValue
a0 (in)The hart ID to get the status of.
a6 (in)2
a7 (in)0x48534D (HSM)
a0 (lateout)sbiret.error
a1 (lateout)sbiret.value
OpenSBI hart_status parameters.

This will return to you the status of the hart given as the parameter. sbiret.value can contain the following.

Return valuesbiret.valueDescription
Started0The hart has is running.
Stopped1The hart is parked.
Start pending2The hart has been given a start sequence, but has not done it yet.
Stop pending3The hart has been given a stop sequence, but has not yet stopped.
OpenSBI get_status return values.

OpenSBI Timer Functions

Many times we want to put off working until a certain time has elapsed. For example, we don't want to context switch until a certain amount of time has progressed. Luckily, we can use the built-in timer to do this. The timer is hooked onto the core local interrupter (CLINT). We can tell the CLINT to send an interrupt after a certain amount of time has elapsed.

The timer can be set by the C++ prototype below.

struct sbiret sbi_set_timer(uint64_t stime_value)

The timer function uses function id 0 and the extension id 'TIME', which is hex 0x5449_4D45.

The parameters required for this are as follows.

RegisterValue
a0 (in)Time to cause an interrupt. This is absolute time. You should add this to mtime (from the CLINT) to make a relative time.
a6 (in)0
a7 (in)0x5449_4D45 (TIME)
OpenSBI set_timer function parameters.

The timer will only be heard of SIE (supervisor interrupt enable) is set to 1. This can be done by setting SPIE (supervisor pending interrupt enable) to 1 and then issuing an sret.

After the timer hits the value set in the parameter, it will cause an interrupt. If we want to pause for the given time, we can issue the timer set, then issue a wfi instruction which stands for wait for interrupt. After the interrupt occurs, the instruction will unblock and the hart will resume executing instructions.