- 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.
To load the vector table riscv_mtvec_table
address is written to
mtvec with the mode set to 1.
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.)
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
.
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.