- Code Samples
- Vectored Machine Mode Interrupts
- Constructing a Vector Table
- Linking the Interrupt Handlers
- Unimplemented Interrupt Handlers
- Running the Example
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.
- Settings the LSB to 1 enables vectored mode.
- In this mode mtvec will point to a table of instructions. (not a single routine)
- Only interrupts are vectored. Synchronous exceptions are NOT vectored.
- Each entry in the table is an instruction, NOT an address.
- The offset in the table is 4 x the numbered cause in the mcause register (this is also the bit position from the mip register).
- Entry 0 is always the synchronous exception vector.
- Each entry is allocated 4 bytes.
- However, if the following entry is not used a routine longer than 4 bytes could be implemented.
- As user/supervisor/machine mode interrupts are interleaved, if only one privilege is used a small 12 byte routine can be implemented.
- There are separate tables for supervisor (stvec) and user ( utvec ) privileges.
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.