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.
/// Enable a given interrupt id
pub fn enable(id: u32) {
let enables = PLIC_INT_ENABLE as *mut u32;
let actual_id = 1 << id;
unsafe {
// Unlike the complete and claim registers, the plic_int_enable
// register is a bitset where the id is the bit index. The register
// is a 32-bit register, so that gives us enables for interrupts
// 31 through 1 (0 is hardwired to 0).
enables.write_volatile(enables.read_volatile() | actual_id);
}
}
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.
/// Set a given interrupt priority to the given priority.
/// The priority must be [0..7]
pub fn set_priority(id: u32, prio: u8) {
let actual_prio = prio as u32 & 7;
let prio_reg = PLIC_PRIORITY as *mut u32;
unsafe {
// The offset for the interrupt id is:
// PLIC_PRIORITY + 4 * id
// Since we're using pointer arithmetic on a u32 type,
// it will automatically multiply the id by 4.
prio_reg.add(id as usize).write_volatile(actual_prio);
}
}
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.
/// Set the global threshold. The threshold can be a value [0..7].
/// The PLIC will mask any interrupts at or below the given threshold.
/// This means that a threshold of 7 will mask ALL interrupts and
/// a threshold of 0 will allow ALL interrupts.
pub fn set_threshold(tsh: u8) {
// We do tsh because we're using a u8, but our maximum number
// is a 3-bit 0b111. So, we and with 7 (0b111) to just get the
// last three bits.
let actual_tsh = tsh & 7;
let tsh_reg = PLIC_THRESHOLD as *mut u32;
unsafe {
tsh_reg.write_volatile(actual_tsh as u32);
}
}
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.
/// Get the next available interrupt. This is the "claim" process.
/// The plic will automatically sort by priority and hand us the
/// ID of the interrupt. For example, if the UART is interrupting
/// and it's next, we will get the value 10.
pub fn next() -> Option {
let claim_reg = PLIC_CLAIM as *const u32;
let claim_no;
// The claim register is filled with the highest-priority, enabled interrupt.
unsafe {
claim_no = claim_reg.read_volatile();
}
if claim_no == 0 {
// The interrupt 0 is hardwired to 0, which tells us that there is no
// interrupt to claim, hence we return None.
None
}
else {
// If we get here, we've gotten a non-0 interrupt.
Some(claim_no)
}
}
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.
// Machine external (interrupt from Platform Interrupt Controller (PLIC))
// println!("Machine external interrupt CPU#{}", hart);
// We will check the next interrupt. If the interrupt isn't available, this will
// give us None. However, that would mean we got a spurious interrupt, unless we
// get an interrupt from a non-PLIC source. This is the main reason that the PLIC
// hardwires the id 0 to 0, so that we can use it as an error case.
if let Some(interrupt) = plic::next() {
// If we get here, we've got an interrupt from the claim register. The PLIC will
// automatically prioritize the next interrupt, so when we get it from claim, it
// will be the next in priority order.
match interrupt {
10 => { // Interrupt 10 is the UART interrupt.
// We would typically set this to be handled out of the interrupt context,
// but we're testing here! C'mon!
// We haven't yet used the singleton pattern for my_uart, but remember, this
// just simply wraps 0x1000_0000 (UART).
let mut my_uart = uart::Uart::new(0x1000_0000);
// If we get here, the UART better have something! If not, what happened??
if let Some(c) = my_uart.get() {
// If you recognize this code, it used to be in the lib.rs under kmain(). That
// was because we needed to poll for UART data. Now that we have interrupts,
// here it goes!
match c {
8 => {
// This is a backspace, so we
// essentially have to write a space and
// backup again:
print!("{} {}", 8 as char, 8 as char);
},
10 | 13 => {
// Newline or carriage-return
println!();
},
_ => {
print!("{}", c as char);
},
}
}
},
// Non-UART interrupts go here and do nothing.
_ => {
println!("Non-UART external interrupt: {}", interrupt);
}
}
// We've claimed it, so now say that we've handled it. This resets the interrupt pending
// and allows the UART to interrupt again. Otherwise, the UART will get "stuck".
plic::complete(interrupt);
}
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.
/// Complete a pending interrupt by id. The id should come
/// from the next() function above.
pub fn complete(id: u32) {
let complete_reg = PLIC_CLAIM as *mut u32;
unsafe {
// We actually write a u32 into the entire complete_register.
// This is the same register as the claim register, but it can
// differentiate based on whether we're reading or writing.
complete_reg.write_volatile(id);
}
}
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!
// Let's set up the interrupt system via the PLIC. We have to set the threshold to something
// that won't mask all interrupts.
println!("Setting up interrupts and PLIC...");
// We lower the threshold wall so our interrupts can jump over it.
plic::set_threshold(0);
// VIRTIO = [1..8]
// UART0 = 10
// PCIE = [32..35]
// Enable the UART interrupt.
plic::enable(10);
plic::set_priority(10, 1);
println!("UART interrupts have been enabled and are awaiting your command");
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