PLIC

From FriendOS Wiki
Revision as of 02:53, 8 February 2023 by Veloya (talk | contribs) (Updated priorities and threshold section, I suggest someone double check just in case)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The Platform Interrupt Controller (PLIC) is the standardized device for receiving, routing, and completing external interrupts with configuration per-hart and per-privilege mode. Originally, the PLIC design is based on early SiFive designs which were then slightly refined and standardized and only supports basic interrupt enabling/disabling, prioritization and routing. As part of the RISC-V Advanced Interrupt Architecture (AIA), the original PLIC design is extended to support more types of interrupts, namely Message Signaled Interrupts (MSIs), which are important for technologies like PCI. The PLIC specification can be found at https://github.com/riscv/riscv-plic-spec while the APLIC specification is available at https://github.com/riscv/riscv-aia.

Original PLIC Design

The original PLIC design consist of a set of: interrupt source priorities, interrupt pending bits, per-context interrupt enable bits, and then per-context priority threshold & claim/complete regions. A context is the combination of a hart ID and a privilege mode, though the specific ordering of contexts is platform-specific and cannot be generalized between them. A small table below has been added for quick reference, but likely will not contain a formula for calculating the context for every platform. The memory layout for the PLIC is as follows (taken from the PLIC spec):

base + 0x000000: Reserved (interrupt source 0 does not exist)
base + 0x000004: Interrupt source 1 priority
base + 0x000008: Interrupt source 2 priority
...
base + 0x000FFC: Interrupt source 1023 priority
base + 0x001000: Interrupt Pending bit 0-31
base + 0x00107C: Interrupt Pending bit 992-1023
...
base + 0x002000: Enable bits for sources 0-31 on context 0
base + 0x002004: Enable bits for sources 32-63 on context 0
...
base + 0x00207C: Enable bits for sources 992-1023 on context 0
base + 0x002080: Enable bits for sources 0-31 on context 1
base + 0x002084: Enable bits for sources 32-63 on context 1
...
base + 0x0020FC: Enable bits for sources 992-1023 on context 1
base + 0x002100: Enable bits for sources 0-31 on context 2
base + 0x002104: Enable bits for sources 32-63 on context 2
...
base + 0x00217C: Enable bits for sources 992-1023 on context 2
...
base + 0x1F1F80: Enable bits for sources 0-31 on context 15871
base + 0x1F1F84: Enable bits for sources 32-63 on context 15871
base + 0x1F1FFC: Enable bits for sources 992-1023 on context 15871
...
base + 0x1FFFFC: Reserved
base + 0x200000: Priority threshold for context 0
base + 0x200004: Claim/complete for context 0
base + 0x200008: Reserved
...
base + 0x200FFC: Reserved
base + 0x201000: Priority threshold for context 1
base + 0x201004: Claim/complete for context 1
...
base + 0x3FFF000: Priority threshold for context 15871
base + 0x3FFF004: Claim/complete for context 15871
base + 0x3FFF008: Reserved
...
base + 0x3FFFFFC: Reserved

Or, more usefully:

#[repr(C)]
struct Plic {
    source_priorities: [u32; 1024],
    interrupt_pending: [u32; 32],
    _padding1: [u8; 3968],
    interrupt_enable: [[u32; 32]; 15872],
    _padding2: [u8; 57344],
    threshold_and_claim: [[u32; 1024]; 15872],
}

Each u32 inside of the source_priorities is a priority level from 0..=7 (on most platforms, see your platform specification for more complete information) of increasing priority level where priority 0 is considered "never interrupt" and will cause the interrupt to effectively be disabled, with the exception of interrupt #0, which does not exist and is reserved. interrupt_pending is a read-only bitmap of the pending status for priorities 1..=1024 (with the interrupt #0 bit hardwired to zero). Both interrupt_enable and threshold_and_claim work on the aforementioned contexts, and need to be indexed at a platform-specific index for a given hart and privilege mode combination. The interrupt_enable consists of another bitmap of interrupt sources (again with interrupt #0 for each context being hardwired to zero) which allow enabling and disabling specific interrupt sources at the context level, but also means that if you wish for a given interrupt to trigger across any hart, it will need to be enabled for each context for the privilege mode you are executing in. threshold_and_claim is made up of a threshold u32 value and a claim u32 value, with the rest of the u32s being reserved and should not be written to.

Hart & Privilege Context Mappings

Platform M-mode Context S-mode Context
QEMU virt hart_id * 2
hart_id * 2 + 1
QEMU sifive_u Monitor hart: 0

Other harts: hart_id * 2 + 1

hart_id * 2

This platform contains a Monitor Hart which does not support S-mode and does not have the same extension availability as the general-purpose harts

Setting Interrupt Source Priorities & Thresholds

TODO: Explain better

Every interrupt source has a priority, these priorities effect which interrupts get dealt with first, and which don't get dealt with at all. An interrupt won't be dealt with at all if it is below the threshold of the context handling it. To set the interrupt source priority, index into the `source_priorities` field of the PLIC, and then write the desired priority(typically from 0-7). To set the threshold of a context, index into the `threshold_and_claim` field of the PLIC based off the context, and then index to 0 in that, then write the desired threshold(typically 0-7 like priority).

Example in rust:

unsafe fn set_priority(plic: *mut Plic, interrupt_source: usize, priority: u32) {
    // Sanity check
    if interrupt_source >= 1024 {
        return;
    }

    let src_priority = unsafe {&mut (*plic).source_priorities[interrupt_source]};

    *src_priority = priority;
}

unsafe fn set_threshold(plic: *mut Plic, context: usize, thresh: u32) {
    // Sanity check
    if context >= 15872 {
        return;
    }

    let threshold = unsafe {&mut (*plic).threshold_and_claim[context][0]};

    *threshold = thresh;
}

Enabling and Disabling Interrupts

To enable or disable any given interrupt source, simply toggle the specific bit that corresponds to the interrupt source in the interrupt_enable for the context for which you wish to enable or disable the interrupt. Example Rust code:

unsafe fn enable_interrupt(plic: *mut Plic, context: usize, interrupt_source: usize) {
    // Sanity checks, neither values would be valid
    if context >= 15872 || interrupt_source >= 1024 {
        return;
    }
    
    // Calculate the bit-group index and the bit index for that bit-group
    let (u32_index, bit) = (interrupt_source / 32, interrupt_source % 32);
    
    // Calculate the address of the bit-group
    let bit_group_ptr = unsafe { core::ptr::addr_of_mut!((*plic).interrupt_enable[context][u32_index]) };
    
    // Read the current value
    let previous = unsafe { bit_group_ptr.read_volatile() };
    
    // Set the bit for the interrupt source
    let new = previous | (1 << bit);
    
    // Write the new value
    unsafe { bit_group_ptr.write_volatile(new) };
}

unsafe fn disable_interrupt(plic: *mut Plic, context: usize, interrupt_source: usize) {
    // Sanity checks, neither values would be valid
    if context >= 15872 || interrupt_source >= 1024 {
        return;
    }
    
    // Calculate the bit-group index and the bit index for that bit-group
    let (u32_index, bit) = (interrupt_source / 32, interrupt_source % 32);
    
    // Calculate the address of the bit-group
    let bit_group_ptr = unsafe { core::ptr::addr_of_mut!((*plic).interrupt_enable[context][u32_index]) };
    
    // Read the current value
    let previous = unsafe { bit_group_ptr.read_volatile() };
    
    // Clear the bit for the interrupt source
    let new = previous & !(1 << bit);
    
    // Write the new value
    unsafe { bit_group_ptr.write_volatile(new) };
}

Acknowledging Interrupts — Claim/Complete Mechanism

TODO: Explain it better

When receiving an interrupt before you can return from it you need to acknowledge that you finished the interrupt, this is the Claim/Complete mechanism. As the name implies, first you must "claim" an interrupt, which is tells the PLIC "I am working on handling this interrupt, do not try to tell other CPU's about it while I'm working on it". Then once it is handled you must "complete" it, which tells the PLIC "this interrupt is finished and the hardware device can signal again if it wants". It is perfectly normal for claiming an interrupt to sometimes fail if you have multiple CPU's, essentially another CPU has beaten you in the race. The race is mediated by the hardware so it all works out in the end.

In order to claim it you must take the interrupt number, and write that to the `threshold_and_claim` register, first index it based off the context, and then index to 1, and write the interrupt number.

Example rust code:

unsafe fn claim_interrupt(plic: *mut Plic, context: usize, interrupt_source: usize) {
    // Sanity checks, neither values would be valid
    if context >= 15872 || interrupt_source >= 1024 {
        return;
    }

    let threshold_and_claim = unsafe {&mut (*plic).threshold_and_claim};
    
    // Write the interrupt
    threshold_and_claim[context][1] = interrupt_source;
}