Five EmbedDev logo Five EmbedDev

An Embedded RISC-V Blog

Code Samples

These code samples relate to this article:

Vectored Machine Mode Interrupts

This article describes vectored machine mode interrupts for the base ISA core local interrupts (CLINT), and it follows on from the basics in interrupts. It does not cover CLIC local vectored interrupts or PLIC platform vectored interrupts. A more general desciption is included in SiFive’s Interrupt Cookbook. This article focuses on building a small example in C to implement vectored interrupt handling for any RISC-V core.

I’ve used this technique for deeply embedded firmware on minimal RISC-V cores.

The mtvec register selects the vectored interrupt mode. The LSB is a mode bit selects the mode.

Constructing a Vector Table

The simplest way to construct a vector table is to use a jump instruction at each entry.

The code below implements the table as inline assembler in C. Only machine and supervisor privilege entries are added. Entries above index 16 are platform specific and are useful for small embedded cores.

The table is implemented as a naked function (to ensure no pre-amble) and aligned to a four byte boundary using gcc function attributes. It’s also given a custom text section so it could be located at a specific address using the linker file.

void riscv_mtvec_table(void)  __attribute__ ((naked, section(".text.mtvec_table") ,aligned(16)));

void riscv_mtvec_table(void) {
    __asm__ volatile (
        ".org  riscv_mtvec_table + 0*4;"
        "jal   zero,riscv_mtvec_exception;"  /* 0  */   
        ".org  riscv_mtvec_table + 1*4;"
        "jal   zero,riscv_mtvec_ssi;"  /* 1  */   
        ".org  riscv_mtvec_table + 3*4;"
        "jal   zero,riscv_mtvec_msi;"  /* 3  */   
        ".org  riscv_mtvec_table + 5*4;"
        "jal   zero,riscv_mtvec_sti;"  /* 5  */   
        ".org  riscv_mtvec_table + 7*4;"
        "jal   zero,riscv_mtvec_mti;"  /* 7  */   
        ".org  riscv_mtvec_table + 9*4;"
        "jal   zero,riscv_mtvec_sei;"  /* 9  */   
        ".org  riscv_mtvec_table + 11*4;"
        "jal   zero,riscv_mtvec_mei;"  /* 11 */   
#ifndef VECTOR_TABLE_MTVEC_PLATFORM_INTS
        ".org  riscv_mtvec_table + 16*4;"
        "jal   riscv_mtvec_platform_irq0;"
        "jal   riscv_mtvec_platform_irq1;"
        "jal   riscv_mtvec_platform_irq2;"
        "jal   riscv_mtvec_platform_irq3;"
        "jal   riscv_mtvec_platform_irq4;"
        "jal   riscv_mtvec_platform_irq5;"
        "jal   riscv_mtvec_platform_irq6;"
        "jal   riscv_mtvec_platform_irq7;"
        "jal   riscv_mtvec_platform_irq8;"
        "jal   riscv_mtvec_platform_irq9;"
        "jal   riscv_mtvec_platform_irq10;"
        "jal   riscv_mtvec_platform_irq11;"
        "jal   riscv_mtvec_platform_irq12;"
        "jal   riscv_mtvec_platform_irq13;"
        "jal   riscv_mtvec_platform_irq14;"
        "jal   riscv_mtvec_platform_irq15;"
#endif
        : /* output: none */                    
        : /* input : immediate */               
        : /* clobbers: none */
        );
}

To load the vector table riscv_mtvec_table address is written to mtvec with the mode set to 1.

#define RISCV_MTVEC_MODE_VECTORED 1
...
    // Setup the IRQ handler entry point, set the mode to vectored
    csr_write_mtvec((uint_xlen_t) riscv_mtvec_table | RISCV_MTVEC_MODE_VECTORED);

Linking the Interrupt Handlers

The vector_table.h file defines the interrupt handler functions. These functions should be implemented to process individual interrupts. The main.c file implements riscv_mtvec_mti and riscv_mtvec_exception.

The handler definitions are marked with gcc interrupt function attributes. This will ensure the stack is saved at entry and restored on return, and the mret, sret or uret instruction is used to return. The handler implementation does not need this attribute. (Assuming the declarations in vector_table.h are included.)

#pragma GCC push_options
// Force the alignment for mtvec.BASE. A 'C' extension program could be aligned to to bytes.
#pragma GCC optimize ("align-functions=4")
// The 'riscv_mtvec_mti' function is added to the vector table by the vector_table.c
void riscv_mtvec_mti(void)  {
    // Timer exception, re-program the timer for a one second tick.
    mtimer_set_raw_time_cmp(MTIMER_SECONDS_TO_CLOCKS(1));
    timestamp = mtimer_get_raw_time();
}
// The 'riscv_mtvec_exception' function is added to the vector table by the vector_table.c
// This function looks at the cause of the exception, if it is an 'ecall' instruction then increment a global counter.
void riscv_mtvec_exception(void)  {
    uint_xlen_t this_cause = csr_read_mcause();
    uint_xlen_t this_pc    = csr_read_mepc();
    //uint_xlen_t this_value = csr_read_mtval();
    switch (this_cause) {
        case RISCV_EXCP_ENVIRONMENT_CALL_FROM_M_MODE:
            ecall_count++;
            // Make sure the return address is the instruction AFTER ecall
            csr_write_mepc(this_pc+4);
            break;
    }
}
#pragma GCC pop_options

Unimplemented Interrupt Handlers

The vector_table.c file implements default handlers for any handler not implemented elsewhere. The default handlers are mapped to each vectored interrupt handler via weak linking.

e.g. In our example main.c program msi and mei are not implemented. These are linked to the default nop handler, riscv_nop_machine.

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

void riscv_mtvec_msi(void) __attribute__ ((interrupt ("machine")     , weak, alias("riscv_nop_machine") )); 
void riscv_mtvec_mei(void) __attribute__ ((interrupt ("machine")     , weak, alias("riscv_nop_machine") ));


#pragma GCC push_options
#pragma GCC optimize ("align-functions=4")
static void riscv_nop_machine(void)  {
    // Nop machine mode interrupt.
}
#pragma GCC pop_options

Running the Example

The 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 riscv_mtvec_mti saves the timestamp value to a global variable timestamp. On each return from interrupt ecall is executed and the this increments the global ecall_count. You should see this increment approximately once per second.