References :

We had previously defined the framework for handling interrupts all interrupts. But External interrupts need some special attention. Why? becaus they come from an external environment. We need to choose how to interact with outside things. THink of it like creating an API for commuicating with external devices

An external interrupt in our case is an interupt that was not generated by the code running in the subject CPU. In our case, the external interrut is either:

  1. An interrupt coming from an external device such as a UART through the PLIC
  2. A platform level internal interrupt
insert Image

(PLIC and CLINT)interraction with CPU

The PLIC interfaces with the CPU via a physical Interrupt pin. The interrupt pin is enabled, disabled and configured using the MIE register (the machine interrpt Enable register)

The meie bit is found within the MIE register. It enables the acceptance of external interrupts.

Whenever we see that this pin has been triggered (an external interrupt is pending), we can query the PLIC to see what caused it.

Configuring the PLIC means : 1. Prioritizing certain interrupt sources in certain conditions. 2. Ignoring certain interupt sources under certain conditions.

The PLIC is not part of the core CPU. It is just a bunch of external circuits that interact with the CPU via an interrupt pin.
We need to program the PLIC in order to : - filters out some external interrupts - prioritize some external interrupts.

The PLIC can be programmed via MMIO programmning... meaning we can use Rust Code to control the PLIC. The PLIC has a bunch of registers exposed in the I/O dedicated memory.
Here are the Registers :

RegisterAddressDescription
Priority0x0c00_0000 - 32bits longSets the priority of a particular interrupt source
Pending0x0c00_1000 - 32bits longContains a list of interrupts that have been triggered (are pending)
Enable0x0c00_2000 - 32bits longEnable/disable certain interrupt sources
Threshold0x0c20_0000 - 32bits longSets the threshold that interrupts must meet before being able to trigger.
Claim (read)0x0c20_0004 - 32bits longReturns the next interrupt in priority order.
Complete (write)0x0c20_0004 - 32bits longCompletes handling of a particular interrupt.

0x0c00_1000 minus 0x0c00_2000 == 4096 bytes difference. But the registers are 32 bits long. Don't let the 4096 byte difference fool you.... I don't know why there are 4096 gaps or any gaps at all.
The PLIC is connected to the external devices and controls their interrupts through a programmable interface at the PLIC base address (registers shown in the table above).
The PLIC is connected to multiple external devices. So we do not have to burden the CPU to have multiple pins. This makes our motherboard more modular. You can insert different PLICs without having to change the core CPUs.

We will use this file to see the virtual structure of Riscv in Qemu. Inside it we can see this rust enumeration that shows which PLIC pin each external device is attached to. :

#![allow(unused)]
fn main() {
enum {
    UART0_IRQ = 10,
    RTC_IRQ = 11,
    VIRTIO_IRQ = 1, /* 1 to 8 */
    VIRTIO_COUNT = 8,
    PCIE_IRQ = 0x20, /* 32 to 35 */
    VIRT_PLATFORM_BUS_IRQ = 64, /* 64 to 95 */
};

}

Our basic external interrupt will come from the UART. According to the enumeration above, The UART is attached to pin 10 of the PLIC.
This means that Interrupt 10 is the UART external interrupt

Claim/Complete Rgister

The claim process gets us the next interrupt after prioritization has already happened. For example, if the UART is interrupting and it's next, we will get the value 10 if we claim.
Interrupt 0 is a "null" interrupt and is hardwired to 0. So if we read 0 after performing a claim... it means that there are currently no interrupts in the buffer.

The Complete register is used by the processor to inform the PLIC that it has finished handling a specific interrupt. When the processor writes the number of the handled interrupt to the Complete register, the PLIC removes the interrupt from the pending list and updates its internal state. If you write a wrong value, the PLIC does nothing... it just ignores your garbage value.

The PLIC can differenciate when we either write or read the Claim/Complete register.

the plic_int_enable

The plic_int_enable register is a bit-encoding register that controls the interrupt enable status of each interrupt source in the PLIC. The register has a bit for each interrupt source, where each bit corresponds to a specific interrupt source number. If a bit is set to 1, the corresponding interrupt source is enabled, and interrupts from that source can be forwarded to the processor for handling. If a bit is set to 0, the interrupt source is disabled, and interrupts from that source will not be forwarded to the processor.

The plic_int_enable register is typically set by the operating system during system initialization to enable or disable specific interrupt sources based on the system's requirements. The register can also be modified by an interrupt handler to dynamically enable or disable specific interrupt sources during runtime.

For example, if you have a system with multiple devices that generate interrupts, you can use the plic_int_enable register to enable interrupts only from the devices that are required for a specific task. By enabling only the necessary interrupt sources, you can reduce the system's interrupt load and improve overall performance.

Overall, the plic_int_enable register is an important part of the PLIC's operation, as it allows the operating system to control which interrupt sources are enabled or disabled, and which interrupts are forwarded to the processor for handling.

The Threshold register

The Threshold register is used to configure the interrupt priority threshold for the PLIC. The PLIC assigns a priority level to each interrupt request it receives from different devices, and the Threshold register determines which interrupts are sent to the processor for handling.

The Threshold register contains a priority threshold level, which is used to filter the pending interrupts in the PLIC. Interrupt requests with a priority level equal to or higher than the threshold level will be sent to the processor, while interrupts with a lower priority level will be blocked until the threshold level is changed.

The threshold register is typically set by the operating system during system initialization or by an interrupt handler to dynamically adjust the interrupt priority level. By setting the threshold level, the operating system or the interrupt handler can control the priority of the interrupts and avoid overwhelming the processor with a large number of lower priority interrupts.

Interrupt Source Priority registers

The priority levels of different interrupt sources can be set during system initialization by configuring the PLIC's registers, including the Interrupt Source Priority registers, which assign a specific priority level to each interrupt source.

It is upto the kernel programmer to define :

  • which interrupts they are willing to handle
  • The priority of each interrupt
  • The threshhold under different conditions

So after defining all the above three things, you can pass those definitions to interact with the PLIC interface.
For example you can just send a threshold value to the PLIC when the CPU gets overwhelmed.

plic.rs
  • Abstract the plic starting addresses to the registers (Each register is 4-bytes (u32) regardless of the differences between the starting addresses)
  • functions include :
    • claim the next pending interrupt
    • inform the plic of complete interrupt handling (return correct id)
    • inform the PLIC of the threshold required by kernel
    • Check if interrupt X is pending
    • Enable a specific an external interrupt
    • Set a given interrupt priority to the given priority.

initialization:

  • inform the PLIC of the threshold required by kernel
  • enable interrupts - plic_int_enable
  • set priority of interrupts (0-7) - Interrupt Source Priority register

The PLIC will signal Our OS through the asynchronous cause 11 (Machine External Interrupt). That is how the mcause will record an interrupt from the PLIC -- 11 asynchronous.

Now that the mcause just says "11", this info does not specify which specific interrupt happened... it jus says a Machine External Interrupt happened. For us to know which specific External Interrupt happened, we read the claim/complete register.

We need to use a singleton design to implement every module that gets shared between more than one module. Modules need to be thread safe