FE310G: an open source RISC-V microcontroller – Interrupt System

In this second RISC-V article I talk about its interrupt and exception system and about SiFive‘s FE310G, the first commercial silicon implementation of a RISC-V. For more information on RISC-V instructions and registers, take a look at my previous article: FE310G: an open source RISC-V microcontroller – Introduction.

Interrupts

RISC-V ISA defines two major interrupt types: global and local. Basically, global interrupts are designed for multicore environments, while local interrupts are always associated with one specific core. Local interrupts suffer less overhead as there is no need for arbitration (which is the case of global interrupts on multicore systems).

Local Interrupts

Local interrupt system is responsible for processing a limited (and usually small) number of interrupt sources. The CLINT (Coreplex Local Interrupts) module has three basic interrupt sources: software interrupt (SI), timer interrupt (TI) and external interrupt (EI). RISC-V ISA also defines sixteen other optional local interrupt sources (which are not present on E31). One important note: all global interrupts from PLIC (Platform-level Interrupt Controller) are applied to the external interrupt input within CLINT!

RISC-V interrupt system will suspend execution flow and branch to an ISR if a local interrupt source (as long as it is previously enabled) sets its pending interrupt flag. There is also a global interrupt enable bit (MIE/SIE/UIE according to the current mode) available on MSTATUS register. This register also controls interrupt nesting, memory access privileges, etc. For further information regarding take a look at the RISC-V privileged instructions manual.

Table 1 – MSTATUS register

There are two ways to deal with interrupts on RISC-V: by using a single vector or multiple vectors. On the single vector mode, register MTVEC (CSR number 0x305) points to the ISR base address, that is, MTVEC points to the single/unique entry point for all ISR code. On the multiple vector mode, on the other hand, MTVEC works as a pointer to the vector table base address and the index for that table is taken from the MCAUSE register (CSR number 0x342).

MTVEC’s two least significant bits control the operation mode: when “00”, interrupt mode is single vectored and when “01”, multiple interrupt vector mode is enabled. In the later case, program flow will divert to address given by MTVEC (with its lsb = “00”) + (Exception Code)*4, that is, the exception code works as an index to the vector table which base address is given by MTVEC (in all cases MTVEC’s two lsb are always used as “00” and in multiple vector mode, MTVEC must always point to a valid 128-byte boundary).

Table 2 – Machine mode CSRs

Tables 3 and 4 show configuration and possible values for MCAUSE. Note that bit 31 (XLEN equals to 32 on RV32, so XLEN-1 equals to 31) acts as an interrupt flag, meaning that it is set for interrupt events and cleared for other exceptions.

Table 3 – MCAUSE register
Table 4 – MCAUSE possible values

Table 5 depicts CLINT memory map (Hart 0 refers to Hardware Thread 0, as the ISA is designed for single and multicore environments, so Hart 0 is the code running on the local physical core).

Table 5 – CLINT memory map

On E31 the timer interrupt (MTIP) is sourced by the standard timer which comprises a 64-bit counter (MTIME) clocked by io_rtcToggle (usually 32,768Hz) and a 64-bit comparator (MTIMECMP). When MTIME counting is higher or equal to MTIMECMP, MTIP bit is set on MIP register. MSIP register is used for software interrupt generation and its bit 0 is propagated to MSIP bit on MIP register.

The interrupt system has also pending interrupt registers and local interrupt enable registers. In supervisor mode we have SIP register (pending interrupt) and SIE register (interrupt enable). Supervisor mode registers also include user mode bits (those beginning with U) along with supervisor mode bits (those beginning with S). WIRI bits must be ignored (both on reading and writing).

Table 6 – SIP (Supervisor Interrupt Pending) register
Table 7 – SIE (Supervisor Interrupt Enable) register

In machine mode we have MIP (pending interrupt) and MIE (interrupt enable) registers. Note that machine mode registers also include user mode bits (those beginning with U), supervisor mode bits (those beginning with S) and machine mode bits (those beginning with M).

Table 8 – MIP (Machine Interrupt Pending) register
Table 9 – MIE (Machine Interrupt Enable) register

By looking at MIP register we can identify the three local interrupt sources:

  • SIP (USIP, SSIP e MSIP): pending software interrupt flag. This interrupt source is enabled by bits USIE, SSIE and MSIE on MIE register;
  • TIP (UTIP, STIP e MTIP): pending timer interrupt flag. This interrupt source is enabled by bits UTIE, STIE and MTIE on MIE register;
  • EIP (UEIP, SEIP e MEIP): pending external interrupt flag (from PLIC). This interrupt source is enabled by bits UEIE, SEIE e MEIE on MIE register;

Individual enable and pending bits for each operation mode (user, supervisor and machine) enable specific interrupts to be enabled only in specific modes. On E31 core, only machine mode interrupts should be used!

Global Interrupts

RISC-V global interrupts are managed by PLIC (Platform-level Interrupt Controller) module and up to 255 global interrupt sources are allowed. On E31 core all peripheral interrupts (except the standard timer) are considered global and thus under PLIC management. As said before, the PLIC output is applied to the external local interrupt (MEIP/SEIP/UEIP).

Each individual interrupt can have its own priority (address 0x0C000004 to 0x0C000400) selected among 8 possible levels (0= never interrupt, 1 is the lowest priority and 7 the highest priority). Only the three least significant bits of each address are used for priority select, the remaining bits should be kept in “0”.

There is also an 8-register set (address 0x0C001000 to 0x0C00101C) which store pending interrupt flags for all 255 possible interrupt sources. Address 0x0C001000 stores interrupt 0 (bit 0) up to interrupt 31 (bit 31) flags, address 0x0C001004 stores interrupt 32 (bit 0) to interrupt 63 (bit 31) flags and so on.

Addresses 0x0C002000 to 0x0C00201C store individual interrupt enable bits for all 255 possible interrupt sources, each register controls 32 sources in the same way as pending interrupt flag registers.

Table 10 – PLIC memory map

There is an interrupt threshold register (address 0x0C200000) which controls current PLIC interrupt priority level, ignoring interrupts with priority lower than specified.

PLIC’s interrupt mechanism is completed by a key element which is the claim/complete register (address 0x0C200004): this register identifies interrupt currently being serviced (interrupt ID) or zero if no interrupt is being serviced. The ISR must then claim interrupt, signaling PLIC it is servicing that interrupt. Claim process occurs by simply reading the claim/complete register. PLIC then automatically clears the corresponding interrupt pending flag.

Once interrupt servicing is complete, ISR code must write the interrupt ID onto the claim/complete register, that indicates the interrupt was serviced, completing the process and also enables creating a software tail-chaining mechanism similar to the one found on Cortex NVIC: once interrupt processing is completed, if claim/complete register is not zero, another interrupt is pending and the code must restart interrupt servicing.

Talking specifically about E31 core, there are 52 PLIC global interrupt sources as shown on table 11.

Table 11 – FE310G PLIC interrupt sources

Hands-on Interrupts

Talking about registers, memory maps, vectors, etc may look a lot complicated so, what about taking a look at some code to show how to deal with interrupts on RISC-V? The following code snippet could be used for servicing the standard timer interrupt:

void handle_m_time_interrupt(){
  // set next timer compare to half a second from its previous value
  *mtimecmp += RTC_FREQ/2;
  // toggle blue LED state
  GPIO_REG(GPIO_OUTPUT_VAL) ^= 1 << BLUE_LED_OFFSET;
}

Interrupt enabling can be done with the following:

set_csr(mie, MIP_MTIP);		// enable timer interrupt in machine mode
set_csr(mstatus, MSTATUS_MIE);	// enable global interrupts

SiFive’s BSP code is responsible for initializing interrupts properly (file bsp/env/freedom-e300-hifive1/init.c):

#ifdef USE_M_TIME
extern void handle_m_time_interrupt();
#endif

uintptr_t handle_trap(uintptr_t mcause, uintptr_t epc)
{
  if (0){
#ifdef USE_PLIC
    // External Machine-Level interrupt from PLIC
  } else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_EXT)) {
    handle_m_ext_interrupt();
#endif
#ifdef USE_M_TIME
    // External Machine-Level interrupt from PLIC
  } else if ((mcause & MCAUSE_INT) && ((mcause & MCAUSE_CAUSE) == IRQ_M_TIMER)){
    handle_m_time_interrupt();
#endif
  }
  else {
    write(1, "trap\n", 5);
    _exit(1 + mcause);
  }
  return epc;
}

void _init()
{
  #ifndef NO_INIT
  use_default_clocks();
  use_pll(0, 0, 1, 31, 1);
  uart_init(115200);
  printf("core freq at %d Hz\n", get_cpu_freq());
  write_csr(mtvec, &trap_entry);
  if (read_csr(misa) & (1 << ('F' - 'A'))) { // if F extension is present
    write_csr(mstatus, MSTATUS_FS); // allow FPU instructions without trapping
    write_csr(fcsr, 0); // initialize rounding mode, undefined at reset
  }
  #endif
}

More details on how to use interrupts will be seen in this series coming articles.

SiFive FE310G

The FE310G is the first RISC-V commercial silicon implementation and includes a 32-bit RV32IMAC core which includes a 32-bit integer instructions, compressed instructions, hardware multiply and division and atomic operations. E31 core inside the chip includes a 5-stage pipeline, branch prediction system with 40-entry target branch buffer, 128-entry branch history table and a 2-level return address stack. The branch prediction system avoid (when possible) pipeline (and cache eventually) flush due to a branch in code-flow. Once a branch prediction fails, branch instructions take 3 clock-cycles (instead of 1). Most instructions execute in 1 clock-cycle (as long as they are cached), except for load instructions such as load word (LW) which takes 2 cycles, load byte and half-word (LB, LBU, LH and LHU) take 3 cycles, CSR instructions which take 3 cycles (reading) or 5 cycles (writing), multiplies take 5 cycles and divides and remaining take from 2 up to 33 cycles (according to operand values).

Figure 1 – FE310G block diagram

Chip technical characteristics are the following:

  • 320MHz RISC-V RV32IMAC CPU, with 16Kb instruction cache, 16Kb RAM, 8Kb BOOT ROM, 8Kb OTP configuration memory and external program memory (128Mb QSPI FLASH on HiFive board);
  • Embedded debug module with JTAG connection;
  • Supply voltage of 1.8V (core and PLL) and 3.3V (I/O ports);
  • Program Memory Protection (PMP) with 8 regions (write, read and execute);
  • Internal oscillators (72MHz calibrated HFROSC and 32,768Hz LFROSC), external clock inputs (16MHz and 32,768Hz) and internal PLL;
  • 19 I/O pins with individual interrupt;
  • Always On module with 16 32-bit registers, RTC and power management (this is the only module which operates while in sleep mode);
  • 2 UARTs;
  • 3 SPI (one QSPI);
  • Watchdog;
  • 3 4-channel PWM timers (one 8-bit and two 16-bit).
Figure 2 – FE310G packaged in QFN 48 pins

It is clear that the peripheral set is pretty humble considering it is a 32-bit microcontroller released in 2016. There are no analog interfacing peripherals (such as ADC, DAC, analog comparator, etc.), timers are mostly focused on PWM, there are no high-end communication interfaces (such as USB) and even RAM amount is not enough for slightly complex applications.

Nonetheless, we must relieve it as this is just the very first version of a possible new generation of 32-bit open core microcontrollers. Moreover, some other big semiconductor players are already part of RISC-V (such as Qualcomm, Samsung, AMD, etc.) and can release future devices with their own peripheral libraries and manufacturing process (FE310G is manufactured by TSMC using a 180nm process).

In the next part of this article we will take a look at Freedom Studio, an Eclipse-based IDE which integrates GCC, GDB and SiFive SDK, we will also take a look at some simple code examples using FE310G, see you!

References

Note: tables and images shown here were taken from SiFive’s documents referred above.

Leave a Reply