This post is a draft for Medium.

The basics of RISC-V interrupt handling, and C++ lambda functions.

What are the basics of interrupt handing in RISC-V? Can we utilize modern C++ to simplify the interrupt handling?

RISC-V 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.

What do we have in the base ISA? A timer, an external interrupt and a software interrupt (mti, mei, msi). These are defined in machine level ISA standard machine mode interrupt registers.

For the example used in these posts we just need the timer interrupt, mti, which is included in the core ISA.

For a simple embedded application platform-specific machine-level interrupt sources can extend the mip and mie registers 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.

C++ Callbacks

An interrupt is an asynchronous event. What do do in C++ when an asynchronous event occurs? In modern C++ it is common to associate a lambda function with an event and execute the function when the event occurs. There are several advantages of lambda functions. The advantages I consider important are code locality, and the ability to store state easily.

Can we register a C++ lambda function as an interrupt handier?

In Part1 we saw a lambda function declared such as this:

    static const auto handler = [&] (void) 
        { 
            auto this_cause = riscv::csrs.mcause.read();
            // ...more code...
        } 

This is a bit of syntactic sugar. The compiler will define an object to capture the caller’s context and a callback method to execute the lambda function.

So can we take a pointer to this callback method and save it as an interrupt vector? No, for a few reasons:

  1. A RISC-V interrupt handler must have a specific prologue to save context to the stack, and epilogue to restore the stack and return via mret, unlike ARM Cortex-M, but like most ISAs, interrupts are not standard C functions.
  2. A RISC-V interrupt handler has alignment requirements.
  3. The C++ callback from the lambda function needs to be called as a method of a C++ object.

Installing an Interrupt Handler with GCC

We need to use some GCC compiler extensions when declaring functions used as interrupt handlers. These use attributes and pragmas to achieve the first two requirements above.

namespace irq {
    static void entry(void) __attribute__ ((interrupt ("machine")));
#pragma GCC push_options
// Force the alignment for mtvec.BASE.
#pragma GCC optimize ("align-functions=4")
    static void entry(void)  {
        // Jump into the function defined within the irq::handler class.
        handler::handler_entry();
    }
#pragma GCC pop_options
}

That is not pretty, but we now have a function address we can load to the interrupt vector register. On RISC-V that’s a machine mode register mtvec.

riscv::csrs.mtvec.write(reinterpret_cast<std::uintptr_t>(irq::entry) ); 

Trampoline into C++

But how does this reach our lambda function? There is some need for C++ and RISC-V tricks. We need to extract the function object context of the lambda function, we need to store it somewhere for the raw interrupt handler to use, and we need to call the lambda as a method of that object.

  • To store context for an the IRQ handler, we can use another machine mode register mscratch.
  • To extract the lambda function object context, a templated function is used access the generated lambda functor type. It’s important not to use std::function here, std::function makes use of the heap. The flow is:
    1. Create a static function irq::handler_entry() that calls an inlined static member _execute_handler.
    2. Assign _execute_handler to an intermediate void(*)(void) lambda function declared in the irq::handler constructor.
    3. Save a pointer to the handler’s functor object in the mscratch register.
    4. The intermediate _execute_handler lambda reads back the functor object pointer from mscratch and calls operator() on the pointer to the lambda functor object.
    5. The lambda function is invoked.
  • After doing all that we hope the optimizer has reduced this to a simple trampoline function.
namespace irq {
    class handler {
    public:
        /** Create an IRQ handler class to install a 
            function as the machine mode irq handler */
        template<class T> handler(T const &isr_handler);
        inline static void (*_execute_handler)(void); 
        // Trampoline function is required to bridge from the entry point
        // function declared with specific attributes and alignments to this class member.
        friend void entry(void);
        /* Step 1 */
        static inline void handler_entry(void) {
            _execute_handler();
        }
    }

    template<class T> handler::handler(T const &isr_handler) {
        // This will call the C++ function object method that represents the lambda function above.
        // This is required to provide the context of the function call that is captured by the lambda.
        // A RISC-V optimization uses the MSCRATCH register to hold the function object context pointer.
        /* Step 2 */
        _execute_handler = [](void)
            {
                // Read the context from the interrupt scratch register.
                /* Step 4 */
                uintptr_t isr_context = riscv::csrs.mscratch.read();
                // Call into the lambda function.
                /* Step 5 */
                return ((T *)isr_context)->operator()();
            };
        // Get a pointer to the IRQ context and save in the interrupt scratch register.
        uintptr_t isr_context = (uintptr_t)&isr_handler;
        /* Step 3 */
        riscv::csrs.mscratch.write( reinterpret_cast<std::uintptr_t>(isr_context) );
        // Write the entry() function to the mtvec register to install our IRQ handler.
        riscv::csrs.mtvec.write( reinterpret_cast<std::uintptr_t>(entry) );
    }      
}

Conclusion

Could we implement this in pure C++. No, but it’s close. Does C++ add value here? It’s questionable.

For the caller, we’ve now reduced an interrupt handler to a standard C++ event driven programming model. This will allow global state to be removed and possibly extended to other models such as promise/future.

But for implementation, a traditional C callback would be much simpler to implement and understand. The C++ code to insert the handler is obfuscated and offers no abstraction.

This is the last part in the series…