FreeRTOS on CH32V307
Porting and running mainline FreeRTOS on WCH CH32V307 RISC-V MCU
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.
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
Summary
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.
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.
ARM or RISC-V?
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
andmtimecmp
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
inmtvec
CSR. In this mode, instead of setting PC toBASE + mcause * 4
, the absolute address of the exception handler will be looked up from the vector table defined inBASE
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.
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 modefreertos_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:
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:
04 - Compile and Run
Compile
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
set(TARGET_LIBS
"freertos_kernel"
)
#...
# Include sub directories here
set(FREERTOS_PORT "GCC_RISC_V_WCH_RISC_V4A_FPU" CACHE STRING "")
set(FREERTOS_CONFIG_FILE_DIRECTORY "${CMAKE_SOURCE_DIR}/include" CACHE STRING "")
set(FREERTOS_HEAP "4" CACHE STRING "")
add_subdirectory(lib/FreeRTOS-Kernel)
Flash Download
WCH provides their toolchain and OpenOCD along with their MounRiver IDE, but standalone toolchain archives for Linux can be downloaded seperately at MounRiver.com.
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.
load
the ELF file with GDB, the demo should work as intended.
05 - Fun Facts
- FreeRTOS only supports M mode only at this time, however WCH's stock startup file jump to
main()
usingmret
, 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 is0x1F
.