5 March 2025
Now I am going to write a bare-metal diagnostic for this bizarre SPI timeout behavior. This will eliminate the possibility of some odd malfunction in the vendor-supplied SDK. It will also introduce the possibility of some odd malfunction as a result of my own programming.
As I mentioned yesterday, I have had some limited success with writing bare-metal code for these chips, both in the C programming language as well as native RISC-V assembly code. Both of these approaches rely heavily on the vendor-supplied SVD file for these chips. SVD files are ‘system view descriptor’ files containing machine-readable descriptions of the chip’s on-board resources. In the case of the CH32V003 SVD file, this is limited to the peripheral registers and their respective bit-field contents. Alas, no ‘enumerated values’ are included, so I am forced to supply those myself.
Since the release of MounRiver Studio 2, we have an updated version of the SVD for the -003 family of chips. It is contained within the MRS2 app itself, here:
/Applications
/MounRiver Studio 2.app
/Contents
/Resources
/app
/resources
/darwin
/components
/WCH
/SDK
/default
/RISC-V
/CH32V003
/NoneOS/
CH32V003xx.svd
Now that was a deep dive!
The file, 321 KB in length, as distributed by the vendor, is dated 23 December 2024 at 4:59 AM. Within it is a version number of 1.2. The previous version, labelled “1.1” was what I used when I was first starting to get to know these chips.
We’ll need both the register addresses and their bit field information in order to manipulate the chip into doing what we want. What I originally did was use a Python script to examine the SVD file and emit a C header file that created typedef’d structs that encapsulated the needed register information. I then included that header file in a makefile project that used the custom version of GCC supplied by the original MRS toolset. This would allow me to reference individual bits within a given peripheral without having to know which exact register was indicated, like this:
RCC->SW = RCC_SW_HSI; // select HSI as system clock source
Where the “RCC” is the pointer to the base address of the “Reset and Clock Control” peripheral, “SW” is the “system clock source selection” bitfield within the “Clock Configuration Register 0” and “RCC_SW_HSI” was an enumerated value (constant) that I created and #define’d elsewhere. Notice that I didn’t have to keep track of which register it was in. The data structure keeps all that information for me. Now I don’t have to check the reference manual for register addresses or bit positions. I still have to look up specific bitfield values because the manufacturer decided to omit those from the SVD file as defined enumerated values.
I also created a handful of boilerplate source files that coordinated some of the other, lower-level necessities of the project. These are sometimes referred to as the “C runtime support” files.
I eventually started wanting use the same technique with RISC-V assembly language projects. I modified the original Python conversion script to emit an assembly language header file with the peripheral register addresses and a bit mask representing the bitfield assignments.
In both cases, I created a ‘Makefile’ that allowed me to compiler or assemble the project from the command line. I also created my own linker script to link the variously-transmogrified source files to be coalesced into an executable binary image. The makefile also added ‘phony’ targets to perform such actions as erase, program or launch the debugger.
Since the resulting projects had multiple but formulaic folder structures as well as project-unique headers and footers where appropriate, I wrote a console application that would create a new project and populate the required files for me. This only worked for the -003 devices, however. Well, in truth, it “worked” for the simplest of 203 or 307 projects, as well, but not as comprehensively as I would have liked.
Most of this effort was spurred by the fact that version 1 of the MounRiver Studio was only supported on Windows or Linux/x86 hosts. A set of command line tools was provided for macOS, and that’s what I used.
Now I’d like to review the process in light of the MRS2 native support of macOS, which includes Apple Silicon. Let’s start with the RISC-V assembly language version first, as that is a little more straight-forward in that it will need to do less for us than the C language version.
First let’s see what my Python script thinks of the new SVD file. I recall that there were a few issues with the original version 1.1 SVD file, but the details escape me.
Well, it didn’t burp. It created a new file called ‘ch32v003xx.svd.inc’, which is simply the input filename with ‘.inc’ appended to the end. It also generated this report to the console:
svd2inc.py - SVD to RISC-V ASM header file converter
SVD filename: ch32v003xx.svd
Parsing ch32v003xx.svd... done
Filename 'ch32v003xx.svd.inc' already exists. Overwrite? (Y/N) y
Note: Overwriting existing file 'ch32v003xx.svd.inc'
Creating 'ch32v003xx.svd.inc'
Peripherals
PWR/PWR, 0x40007000, Power control
RCC/RCC, 0x40021000, Reset and clock control
EXTEN/EXTEN, 0x40023800, Extend configuration
GPIO/GPIOA, 0x40010800, General purpose I/O
GPIO/GPIOC, 0x40011000, derived from GPIOA
GPIO/GPIOD, 0x40011400, derived from GPIOA
AFIO/AFIO, 0x40010000, Alternate function I/O
EXTI/EXTI, 0x40010400, EXTI
DMA1/DMA1, 0x40020000, DMA1 controller
IWDG/IWDG, 0x40003000, Independent watchdog
WWDG/WWDG, 0x40002C00, Window watchdog
TIM/TIM1, 0x40012C00, Advanced timer
TIM/TIM2, 0x40000000, General purpose timer
I2C/I2C1, 0x40005400, Inter integrated circuit
SPI/SPI1, 0x40013000, Serial peripheral interface
USART/USART1, 0x40013800, Universal synchronous asynchronous receiver transmitter
ADC1/ADC1, 0x40012400, Analog to digital converter
DBG/DBG, 0xE000D000, Debug support
ESIG/ESIG, 0x1FFFF7E0, Device electronic signature
FLASH/FLASH, 0x40022000, FLASH
PFIC/PFIC, 0xE000E000, Programmable Fast Interrupt Controller
Interrupts
2: NMI - Non-maskable interrupt
3: HardFault - Exception interrupt
5: Ecall_M - Callback interrupt in machine mode
8: Ecall_U - Callback interrupt in user mode
9: BreakPoint - Breakpoint callback interrupt
12: STK - System timer interrupt
14: SW - Software interrupt
16: WWDG - Window Watchdog interrupt
17: PVD - PVD through EXTI line detection interrupt
18: FLASH - Flash global interrupt
19: RCC - Reset and clock control interrupt
20: EXTI7_0 - EXTI Line[7:0] interrupt
21: AWU - AWU global interrupt
22: DMA1_Channel1 - DMA1 Channel 1 global interrupt
23: DMA1_Channel2 - DMA1 Channel 2 global interrupt
24: DMA1_Channel3 - DMA1 Channel 3 global interrupt
25: DMA1_Channel4 - DMA1 Channel 4 global interrupt
26: DMA1_Channel5 - DMA1 Channel 5 global interrupt
27: DMA1_Channel6 - DMA1 Channel 6 global interrupt
28: DMA1_Channel7 - DMA1 Channel 7 global interrupt
29: ADC - ADC global interrupt
30: I2C1_EV - I2C1 event interrupt
31: I2C1_ER - I2C1 error interrupt
32: USART1 - USART1 global interrupt
33: SPI1 - SPI1 global interrupt
34: TIM1BRK - TIM1 Break interrupt
35: TIM1UP - TIM1 Update interrupt
36: TIM1RG - TIM1 Trigger and Commutation interrupts
37: TIM1CC - TIM1 Capture Compare interrupt
38: TIM2 - TIM2 global interrupt
Creating interrupt vectors
2: NMI_handler
3: HardFault_handler
5: Ecall_M_handler
8: Ecall_U_handler
9: BreakPoint_handler
12: STK_handler
14: SW_handler
Created 7 system vectors
16: WWDG_handler
17: PVD_handler
18: FLASH_handler
19: RCC_handler
20: EXTI7_0_handler
21: AWU_handler
22: DMA1_Channel1_handler
23: DMA1_Channel2_handler
24: DMA1_Channel3_handler
25: DMA1_Channel4_handler
26: DMA1_Channel5_handler
27: DMA1_Channel6_handler
28: DMA1_Channel7_handler
29: ADC_handler
30: I2C1_EV_handler
31: I2C1_ER_handler
32: USART1_handler
33: SPI1_handler
34: TIM1BRK_handler
35: TIM1UP_handler
36: TIM1RG_handler
37: TIM1CC_handler
38: TIM2_handler
Created 23 device vectors
Created 30 vectors in total
So it actually saw that there was already a file with the proposed new filename, and very politely asked permission to over-write it. How courteous!
The new file is 103 KB long. There are still some rough edges in the script, as it tends to emit duplicate definitions for some of the repeated registers, such as the various DMA channel configuration registers. But I think they are “true duplicates” in that they all just redefine the same symbol with the same value, which wastes file space and assembly compute cycles but will still “work”.
Instead of adding newly-minted enumerated values directly to each new source file that needed them, I decided to collect them in a more generic include file for each device, and have that include file subsequently include the generated register definition include file. This file I will creatively and bravely name, “ch32v003.inc”. You can’t stop me!
Here is the as-yet empty generic include file:
# filename: ch32v003.inc
# register definitions for WCH CH32V003 devices
# 5 March 2025 - Dale Wheat
.ifndef CH32V003_INC # prevent recursive inclusion
CH32V003_INC = 0 # arbitrary but required value
.include "ch32v003xx.svd.inc"
# hand-crafted enumerated values go here
.endif # end of include guard conditional CH32V003_INC
# ch32v003.inc [end-of-file]
Now each new assembly source file that we create need only add this line to become fully (or mostly-enoughly) aware of the inner workings of the -003 family:
.include "ch32v003.inc"
This is assuming that your makefile knows where we’ve stashed this master record of all -003 knowledge.
I looked through the archive for a suitably simple project to use as a template, and I found a likely candidate, “J4-blink-asm”. Buy why is this blinky project source file over 20 K-bytes long?
Ah, it seems that once I got the basic blinky goodness developed, I just kept adding on to it, one little bit at a time. It’s got a lot of stuff in there that I’m not going to immediately need. Here’s what I’m going to start with for this bare-metal diagnostic:
# filename: F4-WS2812B-SPI-asm.S
# Diagnostic for WS2812B via SPI
# 5 March 2025 - Dale Wheat
.include "CH32V003xx.svd.inc"
.global start:
start:
# F4-WS2812B-SPI-asm.S [end-of-file]
Notice that the filename ends with an upper-case “.S”, telling the assembler to go ahead and expand any macros that it finds contained within the assembler source file.
Now we just need a makefile for the project. I will again borrow this from the J4-blink-asm project. The linker script for the -003 devices is already in place.
I need to update the “CC_PATH” variable in the makefile to reflect the newest version of the GCC compiler suite, as provided by the MRS2 application. I also had to put single quotes around the path because it now contains spaces. How modern!
Additionally, they also changed the names of all the GCC utilities from the ‘risk-none-elf’ triple to ‘riscv-wch-elf’, so that must be updated in the new makefile, as well.
Now since this very simple example needed no interrupt support, I failed to define a symbol to indicate what I wanted. This is a scenario I hadn’t tested before, because it most definitely does not work. I changed the generated include file by hand from:
.if INTERRUPT_VECTOR_TABLE # use vector table interrupts
to:
.ifdef INTERRUPT_VECTOR_TABLE # use vector table interrupts
and now I have to go back and update the Python script, re-run it and copy the resulting output to the distribution folder. Now I can successfully assemble my little program into a file that has exactly nothing in it. But that’s OK, because that is, after all, what I told it to do.
So like with any other new framework, we have to blink that LED. I’ll start with my own -F4P6 development board and attach a jumper from PA1 to the built-in green LED. It’s set up to be ‘active high’, so writing a 1 to PA1 should turn it on and a zero would turn it off.
But before we can do that, we have to enable the GPIOA peripheral clock and then configure PA1 as an output. Neither of those things are the way we want them to be when the chip first wakes up.
In RISC-V assembly language, to write to a memory location, you first have to load the address into one of the registers and then the value that you want to write into another register. That is, unless you want to write a zero, and you can just use the “zero register”, x0, which already has a zero in it. But we want to write a 1, so we’ll use something else.
I won’t bore you with yet another blinking LED code example, but here’s a fun snippet for about a 1/2 second delay, if your clock is running at ~8 MHz, as the CH32V003 does if not otherwise configured:
li a2, 1000000
1: addi a2, a2, -1 # decrement a2
bnez a2, 1b
This sample uses register a2, one of the ‘function argument’ registers, to count down from one million. It could be any other register available on the RV32EC platform. Don’t try, like I did, to use registers like s2, as they are not present here and will just have the chip restart unless you have a HardFault handler set up to catch them.
I will share with you some working code that spits out a 12 MHz square wave on PC6. It really won’t help you much without the not-supplied header file, but you’ll see some of what I’ve been talking about:
# filename: F4-WS2812B-SPI-asm.S
# Diagnostic for WS2812B via SPI
# 5 March 2025 - Dale Wheat
.include "CH32V003.inc"
.global start
start:
# set up system clock for HSI * 2 via PLL = 48 MHz
la a0, RCC_BASE
lw a1, RCC_CTLR(a0)
li a2, RCC_PLLON
or a1, a1, a2
sw a1, RCC_CTLR(a0) # enable PLL
li a1, RCC_SW_PLL
sw a1, RCC_CFGR0(a0) # select PLL as system clock
# enable required peripheral clocks
li a1, RCC_SPI1EN | RCC_IOPCEN | RCC_IOPAEN
sw a1, RCC_APB2PCENR(a0)
# initialize GPIO
# GPIOA
# PA1 - LED, active high
la a0, GPIOA_BASE
li a1, 0x88888828
sw a1, GPIO_CFGLR(a0)
sw zero, GPIO_OUTDR(a0) # LED off
# GPIOC
# PC6 - SPI data out
la a0, GPIOC_BASE
li a1, 0x89888888
sw a1, GPIO_CFGLR(a0)
# initialize SPI
la a0, SPI1_BASE
li a1, SPI_BIDIMODE | SPI_BIDIOE | SPI_SSM | SPI_SSI # 0xC300
sw a1, SPI_CTLR1(a0)
ori a1, a1, SPI_SPE | SPI_MSTR # 0xC344 enable SPI1 as coordinator
sw a1, SPI_CTLR1(a0)
# set up for blinking LED, sending SPI data
la a0, GPIOA_BASE
li a1, (1 << 1) # PA1
la a3, SPI1_BASE
li a4, 0x55
# endless loop
main:
sw a1, GPIO_OUTDR(a0) # LED on
sw zero, GPIO_OUTDR(a0) # LED off
1: lb a5, SPI_STATR(a3) # read SPI status register
andi a5, a5, SPI_TXE # check for transmit register empty
beqz a5, 1b # wait for TXE to be set
sw a4, SPI_DATAR(a3) # SPI data out
j main # endlessly looping
# F4-WS2812B-SPI-asm.S [end-of-file]
There’s a trick to setting up the SPI peripheral. You have to configure all the communications parameters first, then and only then enable the ‘MSTR’ and ‘SPE’ bits in the control register. Otherwise, it just doesn’t work.
Now the funny thing is that this code executes flawlessly on both the -F4P6 and -A4P6 packages. I’m going to let it run overnight on the -F4U6 prototype and see if it has managed to hang up at any point. As you can see, there’s no timeout checking or restarting of the peripheral should a timeout occur.