Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

Code Samples

These code samples relate to this article:

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 base ISA core local interrupt (CLINT) handing is limited - an interrupt controller is not in the core ISA specification.

A more general desciption is included in SiFive’s Interrupt Cookbook. This article focuses on building a small example in C and C++ to implement CLINT machine mode interrupt handling for any RISC-V core.

The machine level ISA standard machine mode interrupt registers define the available interrupts. 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.

The CLIC defines a new core local interrupt controller more appropriate for embedded applications.

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, pending bits are automatically cleared. The interrupt enables of the core are automatically managed by hardware at entry/exit of the handler.

For the timer interrupt below mip.mti is cleared by updating mtimecmp.

    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:

Running the Example

The C example program and C++ example program can be compiled with CMake and a RISC-V cross compiler, such as xPack GNU RISC-V Embedded GCC. It has been tested with a SiFive HiFive RevB, but is designed to be a generic 32 bit RISC-V example.

It enables a period 1 second machine timer, and the ISR irq_entry demultiplexes the interrupt cause and saves the timestamp value to a global variable timestamp on an mti interrupt.