In the previous post, we’ve established how we can tell the GD32VF103 apart from the CH32V103. In this post we’ll take a look on how are are going to initialise them. We want different initialisation because these chips require different interrupt handlers. Primary because the interrupt numbering does not match.

First, let’s look at interrupts in general. In RISC-V there are two ways of handling interrupts defined. Vectored and non-vectored. In non-vectored mode, there is a single interrupt handler for all interrupts. You set a pointer to the interrupt handler, which is called when an interrupt occurs. In this interrupt handler you would determine which interrupt is active, and handle it appropriately. In vectored mode, one sets a pointer to interrupt vector. When an interrupt occurs, it will call the address you set + 4 times the interrupt number. At each location, you have a jump instruction to the appropriate handler. This will be a relative jump, such instruction is 4 bytes in length. The CLIC interrupt handler, which is a proposal at the time of writing, also sets a pointer to the interrupt vector, however, in stead of jump instructions, it contains the addresses of the interrupt handler. This is like the way a Cortex-M style vector table works.

The CH32V103 will use the standard RISC-V vectored interrupt vector, and the GD32VF103 will use the CLIC interrupt vector. Upon startup we will detect what MCU we are running on and configure the interrupt accordingly.

In the code snippets below I only look at configuring the interrupts. Parts of the initialisation that will be the same for any RISC-V MCU, such as initialising the memory, are skipped for this discussion.

This initialisation code is based on the vendor supplied libraries.
The initialisation of the CH32V103 is straight forwards, set the address of the vector table
and set the interrupt mode to vector. Then interrupts are enabled, and we jump to main

la t0, ch32_vector_base   # Load the address of the vector base into register t0
ori t0, t0, 1             # t0 |= 1 (set lowest bit = enable vector interrupts)
csrw mtvec, t0            # write value t0 to mtvec
li t0, 0x08               # load value 0x08 into register t0 
csrs mstatus, t0          # mstatus |= 0x08 (MIE), enable interrupts
j main                    # jump to main

For the GD32VF103, we have a slightly more complex initialisation, where, for the time being, is partly is assembly, and partly in C
The assembly part loads the addresses of the vector table, and two more handlers.

la t0, gd32_vector_base // Load address for
csrw CSR_MTVT, t0       // the vector table 

la t0, gd32_irq_entry // Load address for
csrw CSR_MTVT2, t0    // Unvectored interrutps
csrs CSR_MTVT2, 0x1   // and enable them 

la t0, gd32_exc_entry // Load address for
csrw CSR_MTVEC, t0    // exceptions
csrs CSR_MTVEC, 0x03  // enable   CLIC mode

j init_gd32

The C part calls into the NMSIS ECLIC library. NMSIS is the CMSIS equivalent for Nuclei RISC-V cores.
We configure the minimum interrupt level, and enable all 4 priority bits that are present in the GD32VF103
Finally, we loop through all interrupts and set them to ECLIC vectored mode.

void init_gd32(void) {
	ECLIC_SetMth(0);        // Minimum IRQ prority
	ECLIC_SetCfgNlbits(4);  // Number of bits in priority field
	for (int i = 0; i < 86 ; i++ ) {  // There are 86 IRQs
		ECLIC_SetShvIRQ(i, 1);    // Enable vectored mode

So far, we have initialised our interrupt handlers with the vector tables. However, this is only the code part. We also have the vector tables themselves, and the linker script that dictates where in flash those vector tables will be located. Those topic I will discuss in the next part.