Category Archive : Rust

The RISC-V APLIC’s New Features

Contents

  1. Repository
  2. Introduction
  3. The APLIC
  4. Conclusion

Repository

This blog series refers to the code written here: https://github.com/sgmarz/riscv_msi.

The APLIC specification (still in draft) is part of the Advanced Interrupt Architecture (AIA) specification, and it is kept here: https://github.com/riscv/riscv-aia.

I am using AIA specification version 0.3.0-draft.31 to write this article.


Introduction

The advanced platform level interrupt controller (APLIC) is an advanced version of SiFive’s PLIC. The main advancement is that it supports sending interrupts via message. So, when paired with an incoming MSI controller (IMSIC), the APLIC can send messages just like any other hardware device.

The main purpose of the original PLIC was to aggregate, prioritize, and send hardware interrupt signals. It provided a method for an operating system to claim, handle, and complete an interrupt request. The PLIC sent a notification to a specific HART (hardware thread — pretty much a CPU core) through an “external interrupt” pin. That HART would then determine what interrupted it by reading from a specific PLIC register called the claim register. This would return a number identifying the device. Usually, this number is a specific wire connecting a hardware device to the PLIC.


The Advanced Platform Level Interrupt Controller (APLIC)

The new APLIC is not backwards compatible with the SiFive PLIC. Some of the concepts and features are the same, but the register file and mappings are different. Technically, a system may be implemented without a full APLIC, but the AIA documentation specifically states that “[f]ull conformance to the Advanced Interrupt Architecture requires the APLIC.”

First, the APLIC registers are laid out as follows:

struct Aplic {
    pub domaincfg: u32,           // Domain CSR that controls how this APLIC functions
    pub sourcecfg: [u32; 1023],   // Source configuration for 1023 interrupts
    _reserved1: [u8; 0xBC0],

    pub mmsiaddrcfg: u32,         // Machine-level MSI address (for APLIC to write MSIs)
    pub mmsiaddrcfgh: u32,
    pub smsiaddrcfg: u32,         // Supervisor-level MSI address
    pub smsiaddrcfgh: u32,
    _reserved2: [u8; 0x30],

    pub setip: [u32; 32],         // Bitset to set pending interrupts (32 IRQS per element)
    _reserved3: [u8; 92],

    pub setipnum: u32,            // Sets a pending interrupt by number
    _reserved4: [u8; 0x20],

    pub clrip: [u32; 32],         // Bitset to clear pending interrupts (opposite of setip)
    _reserved5: [u8; 92],

    pub clripnum: u32,            // Clears a pending interrupt by number
    _reserved6: [u8; 32],

    pub setie: [u32; 32],         // Bitset to enable interrupts
    _reserved7: [u8; 92],

    pub setienum: u32,            // Enable an interrupt by number
    _reserved8: [u8; 32],

    pub clrie: [u32; 32],         // Bitset to disable interrupts (opposite of setie)
    _reserved9: [u8; 92],

    pub clrienum: u32,            // Disable an interrupt by number
    _reserved10: [u8; 32],

    pub setipnum_le: u32,         // Set an interrupt pending by number always little end first
    pub setipnum_be: u32,         // Set an interrupt pending by number always big end first
    _reserved11: [u8; 4088],

    pub genmsi: u32,              // Used to generate MSIs
    pub target: [u32; 1023],      // Target control per interrupt
}

Domain Configuration Register (domaincfg)

Domain Configuration Register (32-bits)

There are three usable fields in this register, interrupt enable (IE, bit 8), delivery mode (DM, bit 2), and big endian (BE, bit 0).

The first 8 bits is a byte order mark for all intents and purposes. It is set to 0x80 so that we can test to see whether the register is big end first or little end first. For example, if we instruct a “load word” (lw instruction in RISC-V), and we get 0x80 in the first byte, that means the machine is in big endian. If we don’t, it is little endian, and that byte we loaded contains the delivery mode and big-endian bits.

The interrupt enable bit enables the APLIC to send interrupts (1 = enabled, 0 = disabled). This doesn’t mean that the interrupt will necessarily be heard, but instead, it only means the APLIC can send interrupts by triggering a pending bit.

The delivery mode bit allows the APLIC to be configured to send normal interrupts, like the old PLIC, or to send interrupts as messages (MSIs). If the DM bit is set to 0, it will send direct interrupts, like the old APLIC. If the DM bit is set to 1, it will send MSIs instead.

The big endian bit allows the APLIC to write in big endian (BE = 1) or to write messages in little endian (BE = 0). However, the BE bit also affects the order of the multibyte domain configuration register too. The most significant byte is set to 0x80 on purpose to act as sort of a byte order mark.

We can configure a Rust function to set the domain register. All fields in this register are Boolean (yes/no, true/false, 1/0).

impl Aplic {
    pub fn set_domaincfg(&mut self, bigendian: bool, msimode: bool, enabled: bool) {
        // Rust library assures that converting a bool into u32 will use
        // 1 for true and 0 for false
        let enabled = u32::from(enabled);
        let msimode = u32::from(msimode);
        let bigendian = u32::from(bigendian);
        self.domaincfg = (enabled << 8) | (msimode << 2) | bigendian;
    }
}

Source Configuration Registers (sourcecfg[u32; 1023])

If bit 10 (delegate) is 0, SM is in bits 2:0. If D=1, bits 9:0 mark the child index.

There is one sourcecfg register for every interrupt possible. Recall that interrupt 0 is not possible, so interrupt 1’s source configuration register is in sourcecfg[0]. This is why the example Rust code I provided subtracts 1 from the interrupt source’s value.

The delegate bit (bit 10) can be read to determine if the given interrupt has been delegated. This bit is read/write. If we write a 1 into this field, it delegates it to a child domain. If that particular source does not have a child domain, this bit will always be read 0.

The source mode bits (bits 2:0) control how the interrupt is triggered for those interrupts not delegated. If an interrupt is delegated (D=1), the bits 9:0 describe the index of the child it was delegated to. If the interrupt is not delegated (D=0), then each interrupt source may be configured to trigger a “pending” interrupt by one of the following.

3-bit “SM” valueRegister NameDescription
0InactiveThe interrupt source cannot generate an interrupt and is inactive.
1DetachedThe interrupt can only be generated by writing directly to the APLIC.
2, 3Reserved
4Edge1The interrupt is asserted on a rising edge (from 0 to 1).
5Edge0The interrupt is asserted on a falling edge (from 1 to 0).
6Level1The interrupt is asserted when high (device IRQ pin is asserted).
7Level0The interrupt is asserted when low (device IRQ pin is deasserted).

An inactive interrupt source cannot generate an interrupt through the APLIC, and we cannot manually assert an interrupt by writing to the interrupt pending register. A detached interrupt source cannot be triggered by the device, however we can manually write to the MMIO APLIC registers to assert the interrupt.

#[repr(u32)]
enum SourceModes {
    Inactive = 0,
    Detached = 1,
    RisingEdge = 4,
    FallingEdge = 5,
    LevelHigh = 6,
    LevelLow = 7,
}
impl Aplic {
    pub fn set_sourcecfg(&mut self, irq: u32, mode: SourceModes) {
        assert!(irq > 0 && irq < 1024);
        self.sourcecfg[irq as usize - 1] = mode as u32;
    }

    pub fn sourcecfg_delegate(&mut self, irq: u32, child: u32) {
        assert!(irq > 0 && irq < 1024);
        self.sourcecfg[irq as usize - 1] = 1 << 10 | (child & 0x3ff);
    }
}

Set/Clear Pending/Enable Interrupt (setip/clrip) Registers

These registers control whether an interrupt is pending. The APLIC will set these bits itself whenever an enabled interrupt is triggered; however, we can manually set an interrupt as a pending interrupt.

These registers are unlike the IMSIC registers. There is a set of registers to set a pending interrupt and a set of registers to clear a pending interrupt.

The setipnum and clripnum registers function much the same way as the following Rust functions that use the setip/clrip registers.

impl Aplic
    pub fn set_ip(&mut self, irq: u32, pending: bool) {
        assert!(irq > 0 && irq < 1024);
        let irqidx = irq as usize / 32;
        let irqbit = irq as usize % 32;
        if pending {
            // self.setipnum = irq;
            self.setip[irqidx] = 1 << irqbit;
        } else {
            // self.clripnum = irq;
            self.clrip[irqidx] = 1 << irqbit;
        }
    }
}

The interrupt enable registers act much like the interrupt pending registers, except they allow the interrupts to be signaled. If an interrupt is NOT enabled, then that interrupt is masked and cannot be triggered.

impl Aplic {
    pub fn set_ie(&mut self, irq: u32, enabled: bool) {
        assert!(irq > 0 && irq < 1024);
        let irqidx = irq as usize / 32;
        let irqbit = irq as usize % 32;
        if enabled {
            // self.setienum = irq;
            self.setie[irqidx] = 1 << irqbit;
        } else {
            // self.clrienum = irq;
            self.clrie[irqidx] = 1 << irqbit;
        }
    }
}

Generate MSI Register (genmsi)

There are two read/write fields and one read-only field in the genmsi register. The genmsi register can be used to trigger an MSI write, even though writing directly to the IMSIC is more efficient.

The hart index is the HART that you want to send an MSI and the EIID (external interrupt identifier) is the value to write to the IMSIC. Usually the EIID is the same number as the interrupt you want to trigger.


Target Control Registers (target[u32; 1032])

The target control registers have two different forms based on how the APLIC is configured. If the APLIC is configured in direct delivery mode, then the register contains a HART index and a priority. Smaller priorities are higher, so 10 has a higher priority than 20.

impl Aplic {
    pub fn set_target_direct(&mut self, irq: u32, hart: u32, prio: u32) {
        assert!(irq > 0 && irq < 1024);
        self.target[irq as usize - 1] = (hart << 18) | (prio & 0xFF);
    }
}

In MSI delivery mode, the register contains a HART index, guest index, and external interrupt identifier (EIID).

impl Aplic {
    pub fn set_target_msi(&mut self, irq: u32, hart: u32, guest: u32, eiid: u32) {
        assert!(irq > 0 && irq < 1024);
        self.target[irq as usize - 1] = (hart << 18) | (guest << 12) | eiid;
    }
}

Interrupt Delivery Control

As I mentioned previously, the APLIC may be configured in MSI or direct mode. In MSI mode, the messages are handled by the incoming MSI controller (IMSIC); however, in direct mode, the APLIC itself will control interrupts. This is done through the interrupt delivery control (IDC) section of the APLIC.

For the virt machine on QEMU, the interrupt delivery control registers are memory mapped to 0x. The registers are laid out as follows.

OffsetSize (bytes)Register Name
04idelivery
44iforce
84ithreshold
244topi
284claimi

Each IDC is for an individual HART and is 32 bytes. Therefore, HART #0’s registers start at offset 0, HART #1’s registers start at offset 32, and so forth.

The idelivery register enables the APLIC’s direct delivery mode. If this register is set to 1, then the APLIC may deliver interrupts, otherwise, if this register is set to 0, then the APLIC does not deliver interrupts.

The idelivery register.

The iforce register forces the APLIC to deliver an interrupt #0. If we write 1 to this register and the APLIC’s interrupts are enabled, then this will signal an interrupt.

The iforce register

The ithreshold register sets the interrupt priority threshold for an interrupt to be heard. A threshold of 0 means that all interrupts can be heard. If the value is non-zero, then any interrupt priority of that value or higher are masked, and hence not heard. In this case, as with everything else, the lower the number, the higher the priority. Don’t let 0 fool you, it’s just a special value that unmasks all priorities. However, a threshold of 1 means all interrupts will be masked, whereas a threshold of 2 means that only priority 1 can be heard.

The ithreshold register.

The topi register holds the interrupt number that is the highest priority and is enabled. The register is split into two pieces: bits 25:16 hold the interrupt number and bits 7:0 hold the interrupt priority.

The topi and claimi registers

The claimi register is the same as topi except that it signals that we are claiming the top interrupt. When we read from this register, the pending bit for the given register will be cleared to 0.

The OS on the repo that I made only uses the MSI delivery mode, since this delivery control is much like the old PLIC’s.


Conclusion

QEMU’s virt machine connects the UART to external IRQ #10, so we can use the APLIC to send messages whenever the UART receiver has data. This requires us to set up the APLIC using the registers above.

In the code below, I split between the machine and supervisor mode and delegate the UART to the supervisor APLIC (index 0).

pub fn aplic_init() {
    // The root APLIC
    let mplic = Aplic::as_mut(AplicMode::Machine);
    // The delgated child APLIC
    let splic = Aplic::as_mut(AplicMode::Supervisor);

    // Enable both the machine and supervisor PLICS
    mplic.set_domaincfg(false, true, true);
    splic.set_domaincfg(false, true, true);

    // Write messages to IMSIC_S
    mplic.set_msiaddr(AplicMode::Supervisor, crate::imsic::IMSIC_S);

    // Delegate interrupt 10 to child 0, which is APLIC_S
    // Interrupt 10 is the UART. So, whenever the UART receives something
    // into its receiver buffer register, it triggers an IRQ #10 to the APLIC.
    mplic.sourcecfg_delegate(10, 0);

    // The EIID is the value that is written to the MSI address
    // When we read TOPEI in IMSIC, it will give us the EIID if it
    // has been enabled.
    splic.set_target_msi(10, 0, 0, 10);

    // Level high means to trigger the message delivery when the IRQ is
    // asserted (high).
    splic.set_sourcecfg(10, SourceModes::LevelHigh);

    // The order is important. QEMU will not allow enabling of the IRQ
    // unless the source configuration is set properly.
    // mplic.set_irq(10, true);
    splic.set_ie(10, true);
}

We can now write the UART handler in the IMSIC whenever the UART sends interrupts.

pub fn imsic_handle(pm: PrivMode) {
    let msgnum = imsic_pop(pm);
    match msgnum {
        0 => println!("Spurious 'no' message."),
        2 => println!("First test triggered by MMIO write successful!"),
        4 => println!("Second test triggered by EIP successful!"),
        10 => console_irq(),
        _ => println!("Unknown msi #{}", msgnum),
    }
}

The code above forwards any message #10 to console_irq, which in turn pops a value off of the receiver buffer register of the UART device.

When all is said and done, we can start getting UART messages:

RISC-V Is Getting MSIs!

Contents

  1. Overview
  2. Repository
  3. Message Signaled Interrupts (MSI)
  4. Incoming MSI Controller (IMSIC)
  5. Conclusion
  6. What’s Next

Overview

Message signaled interrupts or MSIs describe a way to signal an interrupt without a dedicated interrupt request pin (IRQ). One of the most prevalent uses for MSIs is the PCI bus, and the PCI specification defines the MSI and MSI-X standards. The potential benefits may include: (1) reduced number of direct wires from the device to the CPU or interrupt controller, (2) improve signaling performance–mainly by forcing in-band signals by design, and (3) improving guest/host signaling for virtualized environments.

Wikipedia’s article on MSIs can be found here: https://en.wikipedia.org/wiki/Message_Signaled_Interrupts


Repository

All of the code for this post can be found here: https://github.com/sgmarz/riscv_msi. Use tag “msi”.

The code is written for RV32I in Rust. I originally wrote it for RV64GC, but everything else I wrote is also for RV64GC, so I figured I should branch out and broaden my horizons.

The AIA draft manual, written by John Hauser, can be found here: https://github.com/riscv/riscv-aia. This blog post uses version 0.3.0-draft.31, tagged on 13-Jun-2022.


Message Signaled Interrupts (MSI)

An MSI is triggered by a “message”, which is a fancy term for a “memory write”. In fact, we can trigger a message by simply dereferencing an MMIO pointer.

// Note that 0xdeadbeef is not really a valid message. 
// The AIA specifies messages 1 through 2047 are valid due to the number of registers
// available. But, the following is just an example.
let message = 0xdeadbeef;
// QEMU's 'virt' machine attaches the M-mode IMSIC for HART 0 to 0x2400_0000
// The AIA specifies that this must be a word, a double word will cause a trap.
write_volatile(0x2400_0000 as *mut u32, message);

Memory Mapped IO Addresses for Interrupt Files

The code above writes to the MMIO address 0x2400_0000, which is where QEMU’s virt machine connects the M-mode IMSIC for HART 0. The S-mode IMSIC for HART 0 is connected to 0x2800_0000. Each HART is a page away from each other, meaning the M-mode IMSIC for HART 1 is at 0x2400_1000, and the S-mode IMSIC for HART 1 is 0x2800_1000.

For many embedded systems, these values would come from a specification or from an open firmware (OF) package that contained a flattened device tree (FDT) that specifies an MMIO address and what’s connected to it. Here’s an example of QEMU’s virt FDT as plain text. For the repo, I hard coded the MMIO addresses instead of reading a device tree.

imsics@24000000 {
   phandle = <0x09>;
   riscv,ipi-id = <0x01>;
   riscv,num-ids = <0xff>;
   reg = <0x00 0x24000000 0x00 0x4000>;
   interrupts-extended = <0x08 0x0b 0x06 0x0b 0x04 0x0b 0x02 0x0b>;
   msi-controller;
   interrupt-controller;
   compatible = "riscv,imsics";
};

The organization that standardizes device tree formats can be found here: https://www.devicetree.org/

More information about device trees in Linux can be found here: https://www.kernel.org/doc/html/latest/devicetree/usage-model.html

IMSICs were added to QEMU’s virt machine fairly recently, so you may need to clone and build your own QEMU. QEMU’s repository can be found here: https://github.com/qemu.

Triggering an Interrupt by Sending a Message

After a device writes a word to a specific MMIO address, the interrupt is triggered. This means that devices do not need a wire connecting it to an IRQ controller, such as RISC-V’s platform-level interrupt controller, or PLIC. Instead, as long as the device can make a memory write, it can trigger an interrupt.

Even though triggering a message is that simple, we need a mechanism to enable and prioritize these messages. There might be some circumstances where we don’t want to hear certain messages. This is where the incoming MSI controller or IMSIC comes into play.


Incoming MSI Controller (IMSIC)

To be able to support MSIs, some device needs to be able to take memory writes and turn them into interrupts. Furthermore, the device needs to provide a mechanism to enable/disable and to prioritize interrupts just like a regular interrupt controller. This is done by the incoming MSI controller (IMSIC) device.

WARNING: The Advanced Interrupt Architecture (AIA) manual is still a work-in-progress, and already, there have been major changes that removed or added CSRs or other pertinent information. So, some of the code and tables might be outdated.

The register mechanism for the IMSIC consists of several control and status registers (CSRs) as well as internal registers accessibly through a selection mechanism.

Newly Added Registers

The AIA defines several new CSRs separated between the machine and supervisor modes.

Register NameRegister NumberDescription
MISELECT0x350Machine register select
SISELECT0x150Supervisor register select
MIREG0x351A R/W view of the selected register in MISELECT
SIREG0x151A R/W view of the selected register in SISELECT
MTOPI0xFB0Machine top-level interrupt
STOPI0xDB0Supervisor top-level interrupt
MTOPEI0x35CMachine top-level external interrupt (requires IMSIC)
STOPEI0x15CSupervisor top-level external interrupt (requires IMSIC)
New CSRs defined for the AIA.

The registers MISELECT and MIREG allow us to select a register by writing its number into the MISELECT register. Then the MIREG will represent the selected register. For example, if we read from MIREG, we read from the selected register, and if we write to MIREG, we write to the selected register.

There are four selectable registers. There are machine and supervisor versions of these registers. For example, if we write to SISELECT, we will view the supervisor version of the register.

Register NameMISELECT/SISELECTDescription
EIDELIVERY0x70External Interrupt Delivery Register
EITHRESHOLD0x72External Interrupt Threshold Register
EIP0 through EIP630x80 through 0xBFExternal Interrupt Pending Registers
EIE0 through EIE630xC0 through 0xFFExternal Interrupt Enable Registers
Registers selectable by MISELECT/SISELECT and readable/writeable via MIREG/SIREG.

The first thing we need to do is enable the IMSIC itself. This is done through a register called EIDELIVERY for “enable interrupt delivery” register. This register may contain one of three values:

ValueDescription
0Interrupt delivery is disabled
1Interrupt delivery is enabled
0x4000_0000Optional interrupt delivery via a PLIC or APLIC
Value written to EIDELIVERY register

Enabling the IMSIC

So, we need to write 1 (Interrupt delivery is enabled) into the EIDELIVERY register:

// First, enable the interrupt file
// 0 = disabled
// 1 = enabled
// 0x4000_0000 = use PLIC instead
imsic_write(MISELECT, EIDELIVERY);
imsic_write(MIREG, 1);

The EITHRESHOLD register creates a threshold that interrupt priorities must be before it can be heard. If an interrupt has a priority less than the value in EITHRESHOLD, it will be “heard” or unmasked. Otherwise, it will be masked and will not be heard. For example, an EITHRESHOLD of 5 would only permit messages 1, 2, 3, and 4 to be heard. The message 0 is reserved to mean “no message”.

Since a higher threshold opens more messages, messages with a lower number have a higher priority.

// Set the interrupt threshold.
// 0 = enable all interrupts
// P = enable < P only
imsic_write(MISELECT, EITHRESHOLD);
// Only hear 1, 2, 3, and 4
imsic_write(MIREG, 5);

Interrupt Priorities

The AIA documentation uses the message itself as the priority. So, a message of 1 has a priority of 1, whereas a message of 1234 has a priority of 1234. This is more convenient since we can control messages directly. However, since each message number has associated enable and pending bits, there is a limit to the highest numbered interrupt. The specification has a maximum of \(32\times64 – 1 = 2,047\) total messages (we subtract one to remove 0 as a valid message).

Enabling Messages

The EIE register controls whether a message is enabled or disabled. For RV64, these registers are 64-bits wide, but still take up two adjacent register numbers. So, for RV64, only even numbered registers are selectable (e.g., EIE0, EIE2, EIE4, …, EIE62). If you try to select an odd numbered EIE, you will get an invalid instruction trap. This took me many hours to figure out even though the documentation states this is the desired behavior. For RV32, the EIE registers are only 32-bits, and EIE0 through EIE63 are all selectable.

The EIE register is a bitset. If the bit for a corresponding message is 1, then it is unmasked and enabled, otherwise it is masked and is disabled. For RV64, messages 0 through 63 are all in EIE0[63:0]. The bit is the message. We can use the following formulas to determine which register to select for RV64:

// Enable a message number for machine mode (RV64)
fn imsic_m_enable(which: usize) {
    let eiebyte = EIE0 + 2 * which / 64;
    let bit = which % 64;

    imsic_write(MISELECT, eiebyte);
    let reg = imsic_read(MIREG);
    imsic_write(MIREG, reg | 1 << bit);
}

RV32 behaves much the same, except we don’t have to scale it by 2.

// Enable a message number for machine mode (RV32)
fn imsic_m_enable(which: usize) {
    let eiebyte = EIE0 + which / 32;
    let bit = which % 32;

    imsic_write(MISELECT, eiebyte);
    let reg = imsic_read(MIREG);
    imsic_write(MIREG, reg | 1 << bit);
}

With the code above, we can now enable the messages we want to hear. The following example enables messages 2, 4, and 10.

imsic_m_enable(2);
imsic_m_enable(4);
imsic_m_enable(10);

Pending Messages

The EIP registers behave in the exact same way as the EIE registers except that a bit of 1 means that that particular message is pending, meaning a write to the IMSIC with that message number was sent. The EIP register is read/write. If we read from it, we can determine which messages are pending. If we write to it, we can manually trigger an interrupt message by writing a 1 to the corresponding message bit.

// Trigger a message by writing to EIP for Machine mode in RV64
fn imsic_m_trigger(which: usize) {
    let eipbyte = EIP0 + 2 * which / 64;
    let bit = which % 64;

    imsic_write(MISELECT, eipbyte);
    let reg = imsic_read(MIREG);
    imsic_write(MIREG, reg | 1 << bit);
}

Testing

Now that we can enable the delivery as well as indiviual messages, we can trigger them one of two ways: (1) write the message directly to the MMIO address or (2) set the interrupt pending bit of the corresponding message to 1.

unsafe {
    // We are required to write only 32 bits.
    // Write the message directly to MMIO to trigger
    write_volatile(0x2400_0000 as *mut u32, 2);
}
// Set the EIP bit to trigger
imsic_m_trigger(2);

Message Traps

Whenever an unmasked message is sent to an enabled IMSIC, it will come to the specified HART as an external interrupt. For the machine-mode IMSIC, this will come as asynchronous cause 11, and for the supervisor-mode IMSIC, this will come as asynchronous cause 9.

When we receive an interrupt due to a message being delivered, we will need to “pop” off the top level pending interrupt by reading from the MTOPEI or STOPEI registers depending on the privilege mode. This will give us a value where bits 26:16 contain the message number and bits 10:0 contain the interrupt priority. Yes, the message number and message priority are the same number, so we can choose either.

// Pop the top pending message
fn imsic_m_pop() -> u32 {
    let ret: u32;
    unsafe {
        // 0x35C is the MTOPEI CSR.
        asm!("csrrw {retval}, 0x35C, zero", retval = out(reg) ret),
    }
    // Message number starts at bit 16
    ret >> 16
}

My compiler does not support the names of the CSRs in this specification, so I used the CSR number instead. That is why you see 0x35C instead of mtopei, but they mean the same thing.

When we read from the MTOPEI register (0x35C), it will give us the message number of the highest priority message. The instruction csrrw in the code snippet above will atomically read the value of the CSR into the return register and then store the value zero into the CSR.

When we write zero into the MTOPEI register (0x35C), we are telling the IMSIC that we are “claiming” that we are handling the topmost message, which clears the EIP bit for the corresponding message number.

/// Handle an IMSIC trap. Called from `trap::rust_trap`
pub fn imsic_m_handle() {
    let msgnum = imsic_m_pop();
    match msgnum {
        0 => println!("Spurious message (MTOPEI returned 0)"),
        2 => println!("First test triggered by MMIO write successful!"),
        4 => println!("Second test triggered by EIP successful!"),
        _ => println!("Unknown msi #{}", v),
    }
}

Message 0 is not a valid message since when we pop from the MTOPEI, a 0 signals “no interrupt”.

Example Output

If you run the repo with a new QEMU, you should see the following after a successful test.


Conclusion

In this post, we saw the new registers added to support the incoming MSI controller (IMSIC) for RISC-V. We also enabled the IMSIC delivery, the individual messages, and handled two ways of sending a message: via MMIO directly or by setting the corresponding EIP bit. Finally, we handled the interrupt from the trap.


What’s Next?

The second part of the AIA manual includes the new Advanced Platform-Level Interrupt Controller or APLIC. We will examine this system as well as write drivers to start looking at how this new APLIC can signal using wires or messages.

After the APLIC, we will write a driver for a PCI device and use it to send MSI-X messages.

Writing Pong in Rust for my OS Written in Rust

This post is part of a larger effort you can view over here: https://osblog.stephenmarz.com.

Pong being played on my custom, RISC-V OS in Rust!

Video

Contents

  1. Overview
  2. Application Programmer’s Interface (API)
  3. Starting Routines
  4. System Calls
  5. Drawing Primitives
  6. Event Handling
  7. Start Our Game
  8. Game Loop
  9. PLAY

Overview

We last left off writing a graphics driver and an event driver for our operating system. We also added several system calls to handle drawing primitives as well as handling keyboard and mouse inputs. We are now going to use those to animate the simple game of pong. Just like hello world is the test of every programming language, pong is a test of all of our graphics and event systems.


Application Programmer’s Interface (API)

Since we’re writing in user space, we need to use the system calls we developed to (1) enumerate the framebuffer, (2) invalidate sections of the framebuffer, (3) read keyboard/click events, and (4) read motion events–mouse or tablet.

For Rust, we are still using the riscv64gc-unknown-none-elf target, which doesn’t contain a runtime. Now, riscv64gc-unknown-linux-gnu does exist, however it uses Linux system calls, which we haven’t implemented, yet. So, we are required to create our own system call interface as well as runtime!


Starting Routines

Many programmers know that ELF allows our entry point to start anywhere. However, most linkers will look for a symbol called _start. So, in Rust, we have to create this symbol. Unfortunately, after many attempts, I could not get it to work fully in Rust. So, instead, I used the global_asm! macro to import an assembly file. All it does is call main and invokes the exit system call when main returns.

.section .text.init
.global _start
_start:
.option push
.option norelax
	la	gp, __global_pointer$
.option pop
	li		a0, 0
	li		a1, 0
	call	main
	# Exit system call after main
	li	a7, 93
	ecall
.type _start, function
.size _start, .-_start

I did fail to mention about the global pointer. Since global variables can be stored far away, we use a register to store the top of this location. Luckily, most linkers have a symbol called __global_pointer$ (yes, even with the $) that we put into the gp (global pointer) register. I’m not going to cover relaxation, but this is necessary.

This is all that we need to get into Rust. However, since we’re still in baremetal Rust, we have to define the same symbols we did for our OS, including the panic and abort handlers.

#![no_std]
#![feature(asm, panic_info_message, lang_items, start, global_asm)]

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
	print!("Aborting: ");
	if let Some(p) = info.location() {
		println!("line {}, file {}: {}", p.line(), p.file(), info.message().unwrap());
	} else {
		println!("no information available.");
	}
	abort();
}
#[no_mangle]
extern "C" fn abort() -> ! {
	loop {
		unsafe {
			asm!("wfi");
		}
	}
}

This allows our code to at least compile, but as you can see we need to import the assembly file as well as define a main.

global_asm!(include_str!("start.S"));
#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
    0
}

Now we have an entry point for Rust. Now, we can create the println and print macros to make Rust be more Rusty for us.

#[macro_export]
macro_rules! print
{
	($($args:tt)+) => ({
			use core::fmt::Write;
			let _ = write!(crate::syscall::Writer, $($args)+);
			});
}
#[macro_export]
macro_rules! println
{
	() => ({
		   print!("\r\n")
		   });
	($fmt:expr) => ({
			print!(concat!($fmt, "\r\n"))
			});
	($fmt:expr, $($args:tt)+) => ({
			print!(concat!($fmt, "\r\n"), $($args)+)
			});
}

System Calls

We need to create an API for system calls, since that will be how we get and put certain data to our operating system. Generally, the runtime will establish this for us, but again, we’re in baremetal Rust.

use core::fmt::{Write, Error};
use crate::event::Event;

pub struct Writer;

impl Write for Writer {
	fn write_str(&mut self, out: &str) -> Result<(), Error> {
		for c in out.bytes() {
			putchar(c);
		}
		Ok(())
	}
}

pub fn putchar(c: u8) -> usize {
    syscall(2, c as usize, 0, 0, 0, 0, 0, 0)
}

pub fn sleep(tm: usize) {
    let _ = syscall(10, tm, 0, 0, 0, 0, 0, 0);
}

pub fn get_fb(which_fb: usize) -> usize {
    syscall(1000, which_fb, 0, 0, 0, 0, 0, 0)
}

pub fn inv_rect(d: usize, x: usize, y: usize, w: usize, h: usize) {
    let _ = syscall(1001, d, x, y, w, h, 0, 0);
} 

pub fn get_keys(x: *mut Event, y: usize) -> usize {	
    syscall(1002, x as usize, y, 0, 0, 0, 0, 0)
}

pub fn get_abs(x: *mut Event, y: usize) -> usize {	
    syscall(1004, x as usize, y, 0, 0, 0, 0, 0)
}

pub fn get_time() -> usize {
     syscall(1062, 0, 0, 0, 0, 0, 0, 0)
}

pub fn syscall(sysno: usize, a0: usize, a1: usize, a2: usize, a3: usize, a4: usize, a5: usize, a6: usize) -> usize {
    let ret;
    unsafe {
    asm!("ecall",
        in ("a7") sysno,
        in ("a0") a0,
        in ("a1") a1,
        in ("a2") a2,
        in ("a3") a3,
        in ("a4") a4,
        in ("a5") a5,
        in ("a6") a6,
        lateout("a0") ret);
    }
    ret
}

MAN, I love the new Rust asm! It is very clear what’s happening when you look at it. Usually when I use inline assembly, register selection is extremely important. This makes it rather foolproof–dare I actually say that?

Just like we did for our operating system, we use the Write trait to hook into the format macro that is given to us by Rust.


Drawing Primitives

Ok, with the system calls and the startup code in start.S, we now have everything we need to start programming the game. So, let’s make some drawing primitives. Since this is pong, we only really care about drawing rectangles.

#[repr(C)]
#[derive(Clone,Copy)]
pub struct Pixel {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

pub type Color = Pixel;

impl Pixel {
    pub fn new(r: u8, g: u8, b: u8) -> Self {
        Self {
            r, g, b, a: 255
        }
    }
}

pub struct Vector {
    pub x: i32,
    pub y: i32
}

impl Vector {
    pub fn new(x: i32, y: i32) -> Self {
        Self {
            x,  y
        }
    }
}

pub struct Rectangle {
    pub x: i32,
    pub y: i32,
    pub width: i32,
    pub height: i32,
}

impl Rectangle {
    pub fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
        Self {
            x, y, width, height
        }
    }
}
pub struct Framebuffer {
    pixels: *mut Pixel
}

impl Framebuffer {
    pub fn new(pixels: *mut Pixel) -> Self {
        Self { pixels }
    }
    pub fn set(&mut self, x: i32, y: i32, pixel: &Pixel) {
        unsafe {
            if x < 640 && y < 480 {
                let v = (y * 640 + x) as isize;
                self.pixels.offset(v).write(*pixel);
            }
        }
    }
    pub fn fill_rect(&mut self, rect: &Rectangle, color: &Pixel) {
        let row_start = rect.y;
        let row_finish = row_start + rect.height;
        let col_start = rect.x;
        let col_finish = col_start + rect.width;
        for row in row_start..row_finish {
            for col in col_start..col_finish {
                self.set(col, row, color);
            }
        }
    }
}

pub fn lerp(value: i32, mx1: i32, mx2: i32) -> i32 {
    let r = (value as f64) / (mx1 as f64);
	return r as i32 * mx2;
}

Now we have a pixel structure, a vector, a rectangle, and a framebuffer. The pixel comes from the operating system, so it is important that we control that structure, which necessitates #[repr(C)].


Event Handling

We can draw, so now we need to be able to handle input. We can create an event structure to handle this.

#[repr(C)]
#[derive(Copy, Clone)]
pub struct Event {
    pub event_type: u16,
	pub code: u16,
	pub value: u32
} 

impl Event {
	pub fn empty() -> Self {
		Self {
			event_type: 0,
			code: 0,
			value: 0
		}
	}
	pub fn new(event_type: u16, code: u16, value: u32) -> Self {
		Self {
			event_type,
			code,
			value
		}
	}
}

// Key codes
pub const KEY_RESERVED: u16 = 0;
pub const KEY_ESC: u16 = 1;
pub const KEY_1: u16 = 2;
pub const KEY_2: u16 = 3;
// ... CLIP ...
pub const KEY_END: u16 = 107;
pub const KEY_DOWN: u16 = 108;

// mouse buttons

pub const BTN_MOUSE: u16 = 0x110;
pub const BTN_LEFT: u16 = 0x110;
pub const BTN_RIGHT: u16 = 0x111;
pub const BTN_MIDDLE: u16 = 0x112;


// mouse movement

pub const ABS_X: u16 = 0x00;
pub const ABS_Y: u16 = 0x01;
pub const ABS_Z: u16 = 0x02;

Many of the constants came from libevdev’s input-event-codes.h. Unfortunately, that’s in C, so a little Python script could make it Rust.

Just like the Pixel structure, the Event structure is defined by our operating system, so we are required to control it ourselves (hence #[repr(C)]).


Start Our Game

const MAX_EVENTS: usize = 25;
const GAME_FRAME_TIMER: usize = 1000;

#[start]
fn main(_argc: isize, _argv: *const *const u8) -> isize {
	use drawing::Framebuffer;
	use drawing::Pixel;

	let ufb = syscall::get_fb(6) as *mut Pixel;
	let mut fb = Framebuffer::new(ufb);
	let background_color = drawing::Pixel::new(25, 36, 100);
	let mut event_list = [event::Event::empty(); MAX_EVENTS];
	let event_list_ptr = event_list.as_mut_ptr();

	let player_color = drawing::Pixel::new(255, 0, 0);
	let npc_color = drawing::Pixel::new(0, 255, 0);
	let ball_color = drawing::Pixel::new(255, 255, 255);

	let mut game = pong::Pong::new(player_color, npc_color, ball_color, background_color);
	// GAME LOOP HERE
	println!("Goodbye :)");
	0
}

You can see the first thing we do is grab a framebuffer. If you recall from the operating system tutorial, our operating system will map the pixels into our application’s memory space. We can update the pixels as we see fit, but to actually realize it on the screen, we must invalidate the given pixels, which is a separate system call in our OS.

You can see we have a structure called Pong. This structure contains the routines we need to make this somewhat a game. We have a timing function called advance_frame, and we have a way to get the game onto the screen using the draw function.

This is pretty gross, but here’s the Pong structure’s advance frame and draw routines (the rest can be found on GitHub):

impl Pong {
	pub fn advance_frame(&mut self) {
		if !self.paused {
            self.move_ball(self.ball_direction.x, self.ball_direction.y);
			let miss = 
			if self.ball.location.x < 40 {
				// This means we're in the player's paddle location. Let's
				// see if this is a hit or a miss!
				let paddle = (self.player.location.y, self.player.location.y + self.player.location.height);
				let ball = (self.ball.location.y, self.ball.location.y + self.ball.location.height);

				if paddle.0 <= ball.0 && paddle.1 >= ball.0 {
					false
				}
				else if paddle.0 <= ball.1 && paddle.1 >= ball.1 {
					false
				}
				else {
					true
				}
			}
			else {
				false
			};
			if miss {
				self.reset();
				self.paused = true;
			}
			else {
				if self.ball.location.x < 40 || self.ball.location.x > 580 {
					self.ball_direction.x = -self.ball_direction.x;
				}
				if self.ball.location.y < 20 || self.ball.location.y > 430 {
					self.ball_direction.y = -self.ball_direction.y;
				}
				let new_loc = self.ball.location.y - self.npc.location.height / 2;
				self.npc.location.y = if new_loc > 0 { new_loc } else { 0 };
			}
		}
	}
	pub fn draw(&self, fb: &mut Framebuffer) {
		fb.fill_rect(&Rectangle::new(0, 0, 640, 480), &self.bgcolor);
		fb.fill_rect(&self.player.location, &self.player.color);
		fb.fill_rect(&self.npc.location, &self.npc.color);
		fb.fill_rect(&self.ball.location, &self.ball.color);
	}
}

So our game will move the ball given the direction vector every frame due to advance_frame being called from our game loop. We also have the ability to pause the game (or unpause). Finally, we have very basic collision detection for the player’s paddle.


Game Loop

Now we need to put all of this together using a game loop, which will handle input events as well as advance the animation.

  // GAME LOOP
  gameloop: loop {
		// handle mouse buttons and keyboard inputs
		// println!("Try get keys");
		let num_events = syscall::get_keys(event_list_ptr, MAX_EVENTS);
		for e in 0..num_events {
			let ref ev = event_list[e];
			// println!("Key {}  Value {}", ev.code, ev.value);
			// Value = 1 if key is PRESSED or 0 if RELEASED
			match ev.code {
				event::KEY_Q => break 'gameloop,
				event::KEY_R => game.reset(),
				event::KEY_W | event::KEY_UP => game.move_player(-20),
				event::KEY_S | event::KEY_DOWN => game.move_player(20),
				event::KEY_SPACE => if ev.value == 1 { 
					game.toggle_pause();
					if game.is_paused() {
						println!("GAME PAUSED");
					}
					else {
						println!("GAME UNPAUSED")
					}
				},
				_ => {}
			}
		}
		game.advance_frame();
		game.draw(&mut fb);
		syscall::inv_rect(6, 0, 0, 640, 480);
		syscall::sleep(GAME_FRAME_TIMER);
	}

PLAY

Our event loop uses W to move the paddle up or D to move the paddle down. Space toggles the pause and R resets the game.

Hmmm. When I started writing this, it seemed more impressive in my mind. Oh well, have fun!