RISC-V: A Baremetal Introduction using C++. Interrupt Handling.

RISC-V Machine Mode Interrupts

The standard RISC-V ISA does not specify how to wire up a tangle of system interrupts.

C++ Callbacks

        static const auto handler = [&] (void) 
auto this_cause = riscv::csrs.mcause.read();
// ...more code...
  1. A RISC-V interrupt handler must have a specific prologue to save context to the stack, and an 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

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.
#pragma GCC pop_options
riscv::csrs.mtvec.write(reinterpret_cast<std::uintptr_t>(irq::entry) );

Trampoline into C++

  • To store context for an the IRQ handler, we can use another machine mode register mscratch, although a global variable could also have been used.
  • To extract the lambda function object context, a templated function is used to access the generated lambda functor type. It’s important not to use std::function here, as std::function makes use of the heap.
  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.
namespace irq {
class handler {
/** 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) {
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 */
reinterpret_cast<std::uintptr_t>(isr_context) );
// Write the entry() function to the mtvec register
/// to install our IRQ handler.
reinterpret_cast<std::uintptr_t>(entry) );


  • For the caller, we’ve now reduced an interrupt handler to a standard C++ event driven programming model. This will allow the global state to be removed and possibly extended to other programming models such as promise/future.
  • But for the 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.

The Disassembly

  • It loads the _execute_handler function address from 0x8000_0000into a5.
  • Saves the register context on the stack.
  • Calls _execute_handler by jumping (jalr) to the address in a5.
  • Restores the context from the stack.
  • Executes mretto exit the interrupt.
  • The context can be seen to be read from mscratch at 0x2001_0128 .
  • It’s clear that the handler lambda function has been inlined from when 0x2001_012c reads the mcause register.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Phil Mulholland

Phil Mulholland

Experienced in Distributed Systems, Event-Driven Systems, Firmware for SoC/MCU, Systems Simulation, Network Monitoring and Analysis, Automated Testing and RTL.