[][src]Crate r3_port_riscv

The RISC-V port for the R3 kernel.

Startup code

use_rt! hooks up the entry points (EntryPoint) using #[[::riscv_rt::entry]]. If this is not desirable for some reason, you can opt not to use it and call the entry points in other ways.


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 TypeInterrupt NumberCan Pend?

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 for 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 can set mie.MEIE 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 system 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.


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).


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:

This example is not tested
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
    x16: usize, // a6
    x17: usize, // a7
    x28: usize, // t3
    x29: usize, // t4
    x30: usize, // t5
    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 for most cases.

Processor Modes

All code executes in Machine mode. The value of mstatus.MPP is always M (0b11).



Implement InterruptController and Plic on the given system type using the Platform-Level Interrupt Controller (PLIC) on the target. Requires PlicOptions.


Define a system type implementing PortThreading, PortInterrupts, and EntryPoint. Requires ThreadingOptions and InterruptController.


Generate entry points using ::riscv_rt. Requires EntryPoint to be implemented.


Attach the implementation of PortTimer that is based on the RISC-V timer (mtime/mtimecfg) to a given system type. This macro also implements Timer on the system type. Requires TimerOptions.



The interrupt number for external interrupts.


The first interrupt numbers allocated for use by an interrupt controller driver.


The interrupt number for software interrupts.


The interrupt number for timer interrupts.



Defines the entry points of a port instantiation. Implemented by use_port!.


An abstract interface to an interrupt controller. Implemented by use_plic!.


Provides access to a system-global PLIC instance. Implemented by use_plic!.


The options for use_plic!.


The configuration of the port.


An abstract inferface to a port timer driver. Implemented by use_timer!.


The options for use_timer!.