Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

The Machine Mode Interrupts

The RISC-V ISA is not specialized for embedded applications (compared to, say, the ARM Cortex-M). Keeping this in mind, the core ISA interrupt handing is limited - an interrupt controller is not in the core ISA specification.

These are defined in machine level ISA standard machine mode interrupt registers. In a given privilege level there is a timer, an external interrupt and a software interrupt, for machine mode those are mti, mei, msi.

For a simple embedded application platform-specific machine-level interrupt sources can extend the mip and mie registers to use the bits above 16 to gain more interrupts without an external interrupt controller.

However, it is expected most cores would be integrated with an external interrupt controller. The SiFive target device uses the PLIC (Platform Local Interrupt Controller) which connects to the mei external interrupt.

Installing an Interrupt Handler with GCC

We need to use some GCC compiler extensions when declaring functions used as interrupt handlers.

Firstly, the function needs to be declared an interrupt handler. This will enable the correct register context saving and use of the mret instruction on return.

static void irq_entry(void) __attribute__ ((interrupt ("machine")));

Secondly, we should ensure the function is aligned correctly, as the mtvec register will clip the 2 lsb.

#pragma GCC push_options
// Force the alignment for mtvec.BASE.
#pragma GCC optimize ("align-functions=4")
void irq_entry(void)  {
     ...
}   
#pragma GCC pop_options

Enable and Service an Interrupt in C++

The example for this post is on github baremetal-startup-cxx/src/startup.cpp.

In the example below the timer interrupt is enabled. We need to ensure 4 things to get a timer interrupt:

    mtimer.set_time_cmp(std::chrono::seconds{1});
    // Setup the IRQ handler entry point
    riscv::csrs.mtvec.write( reinterpret_cast<std::uintptr_t>(irq_entry));

    // Timer interrupt enable
    riscv::csrs.mie.mti.set();
    // Global interrupt enable
    riscv::csrs.mstatus.mie.set();

In direct mode servicing the interrupt requires us to read the cause from mcause. We need to clear the interrupt at the source, but pending bits and interrupt enables of the core are automatically managed by hardware.

    auto this_cause = riscv::csrs.mcause.read();
    if (this_cause &  riscv::csr::mcause_data::interrupt::BIT_MASK) {
        this_cause &= 0xFF;
        // Known exceptions
        switch (this_cause) {
        case riscv::interrupts::mti :
            // Reset the timer to keep up the one second tick.
            mtimer.set_time_cmp(std::chrono::seconds{1});
            break;
        }
    }

Pre-emption

The mie bit controls pre-emption. The action on interrupt as explained here, however in summary: