FreeRTOS on CH32V307

Porting and running mainline FreeRTOS on WCH CH32V307 RISC-V MCU

FreeRTOS on CH32V307

01 - Preface

During my recent quarantine days, I began working on something I ordered a while ago: WCH CH32V307 RISC-V microcontroller board. The board arrived in April, at first I made a few modifications to the SDK, composed a hello world template project and literally forgot it.

CH32V307 Dev Kit

After being quarantined and transferred to a hotel, I finally got some time playing with it, and the first thing came across my mind is porting some RTOSes to it (since I don't have other gadgets with me).

02 - RISC-V and WCH


The CH32V307 is one of a series of Gerenal-Purpose MCUs produced by Nanjing Qinheng Microelectronics (a.k.a. WCH). WCH also produces CH32F20x series, which has nearly exact the same peripherals, but replaces the RISC-V core with a Cortex-M3 core. This may be another option for customers who are already familiar with Cortex-M processors.

The only difference between CH32F2xx and CH32V3xx

Running at 144MHz and implemented RV32IMAFC ISA, with Gigabit Ethernet, High-Speed USB and tons of low speed periperals, making this MCU fairly powerful among the budget market.


Instead of building and maintaining two seperate SDKs for different CPU architectures, WCH decided to make things easier, by making an RISC-V core that much alike an ARM core, so here we are, the RISC-V4 series cores.

These cores have the following unique features which make them more ARM-ish:

  • A vectored interrupt controller designed by WCH, called PFIC
  • Hardware exception context preservation and restoration (a.k.a. hardware stack push/pop)
  • A memory-mapped SysTick timer backward compatible with the same peripheral in ARM cores (counters and match registers are extended to 64bits), but not wired to mtime and mtimecmp CSRs.
  • Some non-standard features by utilizing CSRs' reserved bits
  • Private peripherals located at the same PPB region(0xE000_0000)

Interrupts or Exceptions?

The vectored interrupt controller WCH has implemented in their cores replaced both CLINT and PLIC, which means it handles both internal exceptions and external interrupts. Here are some highlighted features I found from the processor's manual:

  • Absolute vector table addresses utilizing a reserved MODE 0b11 in mtvec CSR. In this mode, instead of setting PC to BASE + mcause * 4, the absolute address of the exception handler will be looked up from the vector table defined in BASE and loaded to the PC, just like NVIC does.
  • Vector table free(VTF) interrupts, which speeds up interrupt handling by writing up to 4 handler addresses to PFIC memory mapped registers.
  • Hardware accelerated ISR context saving and restoring, pushes 16 caller-saved register to a hidden dedicated hardware stack in one cycle (up to 3 level nested interrupts), and pops them out automatically when mret is executed. This requires proprietary toolchain from WCH, which has a special attribute for hardware-accelerated ISR generation.

One of the unique (and non-standard) things this PFIC does is, some processor exceptions are treated the same way as platform interrupts:


It means some selected synchronous exceptions (like ecall exceptions) will be vectored to BASE + mcause * 4 as other asynchronous interrupts does, with pre-defined priorities listed below.

RISC-V4 Vector Table

03 - FreeRTOS on RISC-V

FreeRTOS has basic support for RISC-V since v10.3.0, with default configuration for NXP RV32M1 Vega along with some other processors. This default port also supports custom chips with additional registers needes to be saved on stack during exception handling.

The porting process is fairly simple, the official guide requires the following functions as exception entries, depends on the current mtvec mode:

  • freertos_risc_v_trap_handler(): For non-vectored mode
  • freertos_risc_v_exception_handler(): For vectored mode (ecall)
  • freertos_risc_v_mtimer_interrupt_handler(): For timer interrupts

I personally prefer not touching the startup file(although I have my re-written version available), so here are the approaches I made:

#define __IRQ_NAKED __attribute__((naked))

__IRQ_NAKED void Ecall_M_Handler(void) {
    /* Use naked function to generate a short call */
    asm("j freertos_risc_v_exception_handler");

__IRQ_NAKED void SysTick_Handler(void) {
    /* Careful: Avoid stack unbalance. */
    asm volatile(
        "addi sp, sp, -4\n"     /* Push */
        "sw t0, 4(sp)\n"        /* Save t0 on stack */
        "li t0, 0xE000F004\n"   /* SysTick->SR */
        "sw zero, 0(t0)\n"      /* Write 0 to clear */
        "lw t0, 4(sp)\n"        /* Restore t0 from stack */
        "addi sp, sp, 4\n"      /* Pop */
        "j freertos_risc_v_mtimer_interrupt_handler"
By using naked functions, no additional instructions are generated by the compiler.

FreeRTOS also needs to know where the exception stack top is located, this is typically defined in linker script at the end of RAM region:

/* Place initial SP to the end of SRAM */
__stack_top = ORIGIN(RAM) + LENGTH(RAM);
PROVIDE(_eusrstack = __stack_top);
PROVIDE(__freertos_irq_stack_top = __stack_top);

Also, as we have additional float-point registers needs to be saved during context switches, we need to add our own chip-specific header:

	/* Push 32 FP registers to stack. */
	addi sp, sp, -(portasmADDITIONAL_CONTEXT_SIZE * portWORD_SIZE)
	fsw f0,   1 * portWORD_SIZE( sp )
	/* ...Add More FP registers here... */
	fsw f31, 32 * portWORD_SIZE( sp )

	/* Pop 32 FP registers from stack. */
	flw f0,   1 * portWORD_SIZE( sp )
	/* ...Add More FP registers here... */
	flw f31, 32 * portWORD_SIZE( sp )
	addi sp, sp, (portasmADDITIONAL_CONTEXT_SIZE * portWORD_SIZE)
Only show the first and last FP register here.

04 - Compile and Run


The FreeRTOS Demo is based on my template project, which using CMake as build system and can be compiled with RISC-V baremetal toolchain. FreeRTOS has supported CMake for a while, so adding kernel to the project is fairly simple:

# Shared libraries linked with application


# Include sub directories here

Flash Download

WCH provides their toolchain and OpenOCD along with their MounRiver IDE, but standalone toolchain archives for Linux can be downloaded seperately at

The on-board WCH-Link may requires firmware update, however I did't find a way to do it without the full Eclipse-based IDE and a working Windows OS.

cd MRS_Toolchain_Linux_x64_V1.40/OpenOCD/bin # Extracted toolchain
./openocd -f wch-riscv.cfg # The only config needed
Run OpenOCD

load the ELF file with GDB, the demo should work as intended.

Demo download and running

05 - Fun Facts

  • FreeRTOS only supports M mode only at this time, however WCH's stock startup file jump to main() using mret, causing the processor switch to U mode. Modifications are required if stock startup files are used.
  • Somehow WCH decides to use a special way running their own FreeRTOS port, by replacing YIELD with an NVIC function, which pends a software interrupt instead of a ecall instruction...
  • The official Dev kit has some LEDs and buttons, however they are not actually connected to the MCU, the only way is using some...jumpers.
  • This MCU has full SRAM backed program flash, and some part of it can be used as additional SRAM by setting the corresponding bits in option bytes.
  • The flash is read as 0xe339e339 after sucessfully erased, I have no idea why they implemented it this way..
  • The debug interface is SWD only, even for RISC-V MCUs. WCH-Link is definitely filled with some evil black magic...
  • The custom CSR corecfgr (0xBC0) is described as "Mainly used for configuring processor pipelining and branch prediction features, and should not be operated by the user", the default value set by startup file is 0x1F.

06 - References: