This is chapter 5 of a multi-part series on writing a RISC-V OS in Rust.
Table of Contents → Chapter 4 → (Chapter 5) → Chapter 6
https://www.youtube.com/watch?v=99KMubPgDIU
In the last chapter, we talked about interrupts to the CPU and thus to the kernel. In this chapter, we will discuss one category of interrupts, external interrupts. These interrupts signal that some external, or platform interrupt has occurred. For example, the UART device could've just filled its buffer.
The platform-level interrupt controller (PLIC) routes all signals through one pin on the CPU--the EI (external interrupt) pin. This pin can be enabled via the machine external interrupt enable (meie
) bit in the mie
register.
Whenever we see that this pin has been triggered (an external interrupt is pending), we can query the PLIC to see what caused it. Furthermore, we can configure the PLIC to prioritize interrupt sources or to completely disable some sources, while enabling others.
The PLIC is an interrupt controller controlled via MMIO. There are several registers relevant to the PLIC:
Register | Address | Description |
Priority | 0x0c00_0000 | Sets the priority of a particular interrupt source |
Pending | 0x0c00_1000 | Contains a list of interrupts that have been triggered (are pending) |
Enable | 0x0c00_2000 | Enable/disable certain interrupt sources |
Threshold | 0x0c20_0000 | Sets the threshold that interrupts must meet before being able to trigger. |
Claim (read) | 0x0c20_0004 | Returns the next interrupt in priority order. |
Complete (write) | 0x0c20_0004 | Completes handling of a particular interrupt. |
The PLIC is connected to the CPU through the external interrupt pin. The following architecture diagram comes from SiFive's Freedom Unleashed Manual.
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). That means that we as the programmer can control the priorities of each interrupt, whether we see it, whether we've handled it, and so forth.
The diagram above might get a little confusing, but the PLIC programmed in QEMU is much simpler.
The system will have wires connecting the PLIC to the external device. We are typically given the wire number in a technical reference document or something similar. However, for us, I looked at qemu/include/hw/riscv/virt.h
to see that the UART is connected to pin 10, the VIRTIO devices are 1 through 8, and the PCI express devices are 32 through 35. You may be wondering why in our Rust code, we only have a single interrupt enable register--well because we won't go above interrupt 10 (UART) for our purposes.
Now that we know the device connected through which interrupt, we enable that interrupt by writing 1 << id
into the interrupt enable register. The interrupt ID for this example will be 10 for the UART.
Now that we've enabled the interrupt source, we need to give it a priority from 0 to 7. 7 is the highest priority and 0 is the "scum" class (h/t Top Gear)--however a priority of 0 cannot meet any threshold, so it essentially disables the interrupt (see PLIC threshold below). We can set each interrupt source's priority through the priority register.
The PLIC itself has a global threshold that all interrupts must hurdle before it is "enabled". This is controlled through the threshold register, which we can write values 0 through 7 into it. Any interrupt priority less than or equal to this threshold cannot meet the hurdle and is masked, which effectively means that the interrupt is disabled.
The PLIC will signal our OS through the asynchronous cause 11 (Machine External Interrupt). When we handle this interrupt, we won't know what actually caused the interrupt--just that the PLIC caused it. This is where the claim/complete process begins.
The claim/complete process is described by SiFive as the following:
The claim
register will return the next interrupt sorted in priority order. If the register returns 0, no interrupt is pending. This shouldn't happen since we will be handling the claim process via the interrupt handler.
We can then ask the PLIC to give us the number of whomever interrupted us through this claim register. In Rust, I run it through a match to drive it to the correct handler. As of now, we're handling it directly in an interrupt context--which is a bad idea, but we don't have any deferred tasking systems, yet.
When we claim an interrupt, we're telling the PLIC that it is going to be handled or is in the process of being handled. During this time the PLIC won't listen to any more interrupts from the same device. This is where the complete
process begins. When we write (instead of read) to the claim register, we give the value of an interrupt to tell the PLIC that we're done with the interrupt it gave us. The PLIC can then reset the interrupt trigger and wait for that device to interrupt again in the future. This resets the system and lets us cycle back to the claim/complete over and over again.
There you have it. The PLIC is programmed and for now, it just handles our UART. Notice in lib.rs
that I removed the UART polling code. Now that we have interrupts, we can handle the UART only when it signals us. Since we put the HART in a waiting loop using wait-for-interrupt (wfi), we can save power and CPU cycles.
Let's add this to our kmain
function and see what happens!
When we make run
this code, we get the following. I typed the bottom line, but it shows that we're now handling interrupts via the PLIC!
Table of Contents → Chapter 4 → (Chapter 5) → Chapter 6