Crate r3_port_riscv
source ·Expand description
The RISC-V port for the R3 kernel.
Startup code
use_rt!
hooks up the entry points (EntryPoint
) using #[
[::riscv_rt::entry
]]
(requires the riscv-rt
Cargo feature). If this is not desirable for some reason, you can opt not to use it and call the entry points in other ways.
Interrupts
This port supports the basic interrupt handling model from the RISC-V specification.
Other interrupt handling models such as RISC-V Core-Local Interrupt Controller are not supported.
Local Interrupts
The first few interrupt numbers are allocated for interrupts defined by the RISC-V privileged architecture (Machine software interrupts, timer interrupts, and external interrupts), which we collectively call local interrupts. The second-level interrupt handlers for these interrupt numbers are called with their respective interrupts disabled (mie.M[STE]IE = 0
) and global interrupts enabled (mstatus.MIE = 1
).
Interrupt Type | Interrupt Number | Can Pend? |
---|---|---|
Software | INTERRUPT_SOFTWARE | Yes |
Timer | INTERRUPT_TIMER | No |
External | INTERRUPT_EXTERNAL | No |
The local interrupts follow a fixed priority scheme in which they are handled in the following decreasing priority order: External, Software, Timer. Interrupts with a higher priority can preempt lower ones, but not the other way. This is realized by carefully controlling the enable bits in the top-level interrupt handler.
The interrupt handler of a particular interrupt number can re-enable the interrupts of the said interrupt number to allow re-entry by preemption (this is useful for external interrupts, which usually have multiple interrupt sources with varying priorities).
The local interrupts are always enabled from an API point of view. InterruptLine::disable
will always return NotSupported
.
Rationale: Because their enable bits are toggled frequently in the top-level interrupt handler, removing the ability to disable these interrupts simplifies the implementation and reduces interupt latency. This should pose no problems in most cases.
The local interrupts are always managed. This is because CPU Lock is currently mapped to mstatus.MIE
(global interrupt-enable).
Interrupt Controller
The remaining interrupt numbers (≥ INTERRUPT_PLATFORM_START
) are controlled by an interrupt controller driver.
Usually, there are more than one interrupt source connected to the external interrupt pin of a hart through an interrupt controller. An interrupt controller driver is responsible for determining the source of an external interrupt and dispatching the appropriate handler. At configuration time, it attaches an interrupt handler to INTERRUPT_EXTERNAL
. The interrupt handler, when called, queries the currently pending interrupt (let’s assume the interrupt number is n
). It may call InterruptControllerToPort::enable_external_interrupts
to allow nested interrupts (assuming the underlying hardware supports that). Then it fetches the corresponding interrupt handler by indexing INTERRUPT_HANDLERS
by n + INTERRUPT_PLATFORM_START
and calls that.
The PortInterrupts
implementation generated by use_port!
delegates method calls to an interrupt controller driver through InterruptController
for these interrupt numbers.
Your kernel trait type should be combined with an interrupt controller driver by implementing InterruptController
. Most systems are equipped with Platform-Level Interrupt Controller (PLIC), whose driver is provided by use_plic!
. PLIC does not support pending or clearing interrupt lines.
Emulation
LR
/SC
Emulation
The emulate-lr-sc
Cargo feature enables the software emulation of the lr
(load-reserved) and sc
(store-conditional) instructions. This is useful for a target that supports atomic memory operations but doesn’t support these particular instructions, such as FE310. The following limitations should be kept in mind when using this feature:
- The software emulation is slow and non-preemptive (increases the worst-case interrupt latency).
- The addition of the software emulation code introduces a non-negligible code size overhead.
- It doesn’t do actual bus snooping and therefore it will behave incorrectly if there’s another bus master controlling the same memory address.
- Instructions with
rd = sp
are not supported and will behave incorrectly. This shouldn’t be a problem in practice. - It doesn’t do actual bus snooping and can’t detect a conflicting memory write that doesn’t modify the memory contents. This shouldn’t be a problem for the atomic operations currently provided by the standard library.
lr
and sc
instructions are generated when the program uses atomic operations that aren’t covered by AMO instructions (e.g., Atomic*::compare_and_swap
).
mstatus.MPIE
Maintenance
The maintain-pie
Cargo feature enables the work-around for the hardware quirk where the mret
instruction clears mstatus.MPIE
in violation of the specification. This quirk is found in QEMU 4.2 and K210. The common symptom is methods returning Err(BadContext)
.
Implementation
The CPU Lock state is mapped to mstatus.MIE
(global interrupt-enable). Unmanaged interrupts aren’t supported.
Context State
The state of an interrupted thread is stored to the interrupted thread’s stack in the following form:
#[repr(C)]
struct ContextState {
// Second-level state (SLS)
// ------------------------
//
// Includes everything that is not included in the first-level state. These
// are moved between memory and registers only when switching tasks.
// SLS.HDR: Second-level state, header
//
// The `mstatus` field preserves the state of `mstatus.FS[1]`.
// `mstatus.FS[0]` is assumed to `1`. This means `mstatus.FS` can only take
// one of the following states: Initial and Dirty.
// Irrelevant bits are don't-care (hence `_part`).
#[cfg(target_feature = "f")]
mstatus_part: usize,
// SLS.F: Second-level state, FP registers
//
// This portion exists only if `mstatus.FS[1] != 0`.
#[cfg(target_feature = "f")]
f8: [FReg; 2], // fs0-fs1
#[cfg(target_feature = "f")]
f18: [FReg; 10], // fs2-fs11
// SLS.X: Second-level state, X registers
x8: usize, // s0/fp
x9: usize, // s1
#[cfg(not(target_feature = "e"))]
x18: usize, // s2
#[cfg(not(target_feature = "e"))]
x19: usize, // s3
#[cfg(not(target_feature = "e"))]
x20: usize, // s4
#[cfg(not(target_feature = "e"))]
x21: usize, // s5
#[cfg(not(target_feature = "e"))]
x22: usize, // s6
#[cfg(not(target_feature = "e"))]
x23: usize, // s7
#[cfg(not(target_feature = "e"))]
x24: usize, // s8
#[cfg(not(target_feature = "e"))]
x25: usize, // s9
#[cfg(not(target_feature = "e"))]
x26: usize, // s10
#[cfg(not(target_feature = "e"))]
x27: usize, // s11
// First-level state (FLS)
// -----------------------
//
// This section is comprised of caller-saved registers. In an exception
// handler, saving/restoring this set of registers at entry and exit allows
// it to call Rust functions.
//
// The registers are ordered in the encoding order (rather than grouping
// them by their purposes, as done by Linux and FreeBSD) to improve the
// compression ratio very slightly when transmitting the code over a
// network.
// FLS.F: First-level state, FP registers
//
// This portion exists only if `mstatus.FS[1] != 0`.
#[cfg(target_feature = "f")]
f0: [FReg; 8], // ft0-ft7
#[cfg(target_feature = "f")]
f10: [FReg; 8], // fa0-fa7
#[cfg(target_feature = "f")]
f28: [FReg; 4], // ft8-ft11
fcsr: usize,
_pad: [u8; (max(FLEN, XLEN) - XLEN) / 8],
// FLS.X: First-level state, X registers
x1: usize, // ra
x5: usize, // t0
x6: usize, // t1
x7: usize, // t2
x10: usize, // a0
x11: usize, // a1
x12: usize, // a2
x13: usize, // a3
x14: usize, // a4
x15: usize, // a5
#[cfg(not(e))]
x16: usize, // a6
#[cfg(not(e))]
x17: usize, // a7
#[cfg(not(e))]
x28: usize, // t3
#[cfg(not(e))]
x29: usize, // t4
#[cfg(not(e))]
x30: usize, // t5
#[cfg(not(e))]
x31: usize, // t6
pc: usize, // original program counter
}
x2
(sp
) is stored in TaskCb::port_task_state
. The stored stack pointer is only aligned to word boundaries.
The idle task (the implicit task that runs when *
running_task_ptr
().is_none()
) always execute with sp == 0
. For the idle task, saving and restoring the context store is essentially replaced with no-op or loads of hard-coded values. In particular, pc
is always “restored” with the entry point of the idle task.
When a task is activated, a new context state is created inside the task’s stack. By default, only essential registers are preloaded with known values. The preload-registers
Cargo feature enables preloading for all x
registers, which might help in debugging at the cost of performance and code size.
The trap handler stores a first-level state directly below the current stack pointer. This means the stack pointer must be aligned to a max(XLEN, FLEN)
-bit boundary all the time. This requirement is weaker than the standard ABI’s requirement, so it shouldn’t pose a problem in most cases.
Processor Modes
All code executes in Machine mode by default. The value of mstatus.MPP
is always M
(0b11
). Other modes can be selected by ThreadingOptions::PRIVILEGE_LEVEL
, which changes all CSRs and CSR values accordingly.
Modules
- Changelog
Macros
- Attach the implementation of
PortTimer
based on the RISC-V machine-mode timer (mtime
/mtimecfg
) to a given kernel trait type. This macro also implementsTimer
on the kernel trait type. RequiresMtimeOptions
. - Implement
InterruptController
andPlic
on the given kernel trait type using the Platform-Level Interrupt Controller (PLIC) on the target. RequiresPlicOptions
andInterruptControllerToPort
. - Define a kernel trait type implementing
PortThreading
,PortInterrupts
,InterruptControllerToPort
, andEntryPoint
. RequiresThreadingOptions
,Timer
, andInterruptController
. - use_rt
riscv-rt
Generate entry points using [::riscv_rt
]. RequiresEntryPoint
to be implemented. - Attach the implementation of
PortTimer
based on the RISC-V Supervisor Binary Interface Timer Extension (EID #0x54494D45 “TIME”) andtime[h]
CSR to a given kernel trait type. This macro also implementsTimer
on the kernel trait type. RequiresSbiTimerOptions
.
Constants
- The interrupt number for external interrupts.
- The first interrupt number allocated for use by an interrupt controller driver.
- The interrupt number for software interrupts.
- The interrupt number for timer interrupts.
- The RISC-V privilege level encoding for the machine level.
- The RISC-V privilege level encoding for the supervisor level.
- The RISC-V privilege level encoding for the user/application level.
Traits
- Defines the entry points of a port instantiation. Implemented by
use_port!
. - An abstract interface to an interrupt controller. Implemented by
use_plic!
. - An API intended to be used by an interrupt controller driver. Implemented by
use_port!
. - The options for
use_mtime!
. - Provides access to a system-global PLIC instance. Implemented by
use_plic!
. - The options for
use_plic!
. - The options for
use_sbi_timer!
. - The configuration of the port.
- An abstract inferface to a port timer driver. Implemented by
use_mtime!
anduse_sbi_timer!
.