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.
Error | Error Value |
---|---|
Success | 0 |
Failed | -1 |
Not supported | -2 |
Invalid parameter | -3 |
Denied | -4 |
Invalid address | -5 |
Already available | -6 |
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.
Register | Value |
---|---|
a0 (in) | 8-bit character value to put to the console |
a6 (in) | 0 |
a7 (in) | 1 |
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.
Register | Value |
---|---|
a0 (out) | 8-bit character read from the console. |
a6 (in) | 0 |
a7 (in) | 2 |
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.
Register | Value |
---|---|
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 |
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.
Register | Value |
---|---|
a6 (in) | 1 |
a7 (in) | 0x48534D (HSM) |
a0 (lateout) | sbiret.error |
a1 (lateout) | sbiret.value |
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.
Register | Value |
---|---|
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 |
This will return to you the status of the hart given as the parameter. sbiret.value can contain the following.
Return value | sbiret.value | Description |
---|---|---|
Started | 0 | The hart has is running. |
Stopped | 1 | The hart is parked. |
Start pending | 2 | The hart has been given a start sequence, but has not done it yet. |
Stop pending | 3 | The hart has been given a stop sequence, but has not yet stopped. |
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.
Register | Value |
---|---|
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) |
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.