I’ve been working on a small embedded project for a customer and have been keeping copious notes on my progress. It’s using (for the moment) a WCH CH32X035 RISC-V microcontroller. I have some experience with other members of the CH32V family, including the -003, -203, -208 and -307. The CH32X035 is new to me and I wanted to get to know it a little better.
I started these notes back on 25 January 2025. They are quite detailed and if you’re not especially interested in embedded software and hardware development, it just might not be very interesting to you. You have been warned.
For the brave or bored amongst you that continue on, I welcome your feedback, questions and comments. Please note that as these are literally ‘lab notes’, I will sometimes commit the twin composition sins of using incomplete sentences and ending sentences with prepositions. Please forgive me.
24 January 2025
Currently using a WCH “official” development board, the CH32X035G8U6-EVT-R0, marked on the silkscreen as CH32X035G8U6-R0-1v2. An unfortunately placed via takes out a critical segment of the ‘8’, making it look like a ‘6’. There is no -G6 variant offered at this time.
The device is a CH32X035G8U6, which is no surprise, in a QFN28 quad flat no-leads 28 pin package. It’s very small and has a 4mm x 4mm x 0.75 mm outline. I believe there is a solder pad on the bottom connected to ground.
The board has no reset switch. It does have a pushbutton marked ‘Download’ to select the bootloader when power is applied. This pushbutton, identified as S4 on the schematic, is connected to PC17 on one side and to Vcc via a 4.7KΩ resistor, R5.
Researching the ‘boot’ mode raises more questions than it answers:
Q: Can the boot code region be over-written with user code?
Q: What interfaces are supported by the boot mode?
Q: Where is the boot mode documented?
Q: What is TURBO mode (see FLASH status register)?
I have also seemingly arbitrarily attached the RST line from the WCH-LinkE device programmer to pin PB4. PB4 happened to be between the transmit and receive pins of USART1 on the board headers, and having a ‘solid’ three pin connector tends to hold on better than two individual pins. My thinking at the time I built the programming cable for this board was that I would use an external IO interrupt to trigger a software device reset. I could do that as well with the PC17/Download button, while retaining whatever bootloader capabilitiesof the chip during the power-on sequence.
I have not yet read the RM in detail, but have just ‘accidentally’ learned that the device boots with the internal 48 MHz RC oscillator divided by 6, for a system clock of 8 MHz. What’s very interesting to me at the moment is how can this clock with a specified frequency accuracy of ~0.8% be used as the clock for a proper USB host or device, as USB has a much tighter allowable timing tolerance.
The immediate goal of this investigation is to see if it is possible to program the device to operate as a USB host and take input from a standard USB keyboard and possibly a mouse. This is in furtherance of a bespoke project involving an OLED with the ability to edit custom messages in situ, without requiring connection to a proper computer or other complexity.
This will also further a separate goal of a completely self-hosted development system based on these devices.
So the first steps will be attempting to use MounRiver Studio 2 (MRS2) to compose, assemble, program and debug an application written entirely in RISC-V assembly language.
The next big step will be to extract the device register information from the vendor-supplied SVD file and format it as an assembly language ‘include’ file.
Due to the limited aspect of the initial project requirements, I think that the vector-table-free (VTF) mechanism for assigning interrupt vectors will suffice:
HardFault
USB
EXTI
I2C
The final list might necessitate the use of a traditional vector table for interrupts.
I have created a new MRS2 project called G8-asm, which is actually a C language-based project, as that is the MRS2 default. The supplied code prints the system clock frequency and chip ID code to the debug console on USART1, then sets up GPIO pin A0 as an output, then goes into an endless loop toggling the GPIO pin. I have jumpered pin A0 to the provided LED1 connector on the board, and it does, indeed, flash the red LED at ~1 Hz.
I will, for now, preserve the supplied Startup/startup_ch32x035.S file and dispose of the remaining source code from the project. This leaves in place the vendor-supplied linker script in Ld/Link.ld and whatever project settings were created when I asked for this project to be created.
Deleting the following project folders and their contents:
/Core
/Debug
/Peripheral
/User
Let’s see how well MRS2 likes my changes. The linker complains of undefined references to:
SystemInit
main
Which is fair enough, as they certainly no longer exist within the project.
Note: The internal comment in the provided startup file lists the filename as startup_ch32x035.s with a lower case ‘.s’ as the file extension when in actuality it is startup_ch32x035.S with an upper case ‘.S’ file extension, which is correct. The GNU assembler treats the two differently, with the ‘.s’ extension omitting any macro substitutions. We certainly want the ‘.S’ because a lot of the heavy lifting in this project is done with macro definitions.
Commenting out the final four lines of the startup_ch32x035.S eliminates the linker’s complaints and a small but effectively useless application is created.
# jal SystemInit
# la t0, main
# csrw mepc, t0
# mret
text data bss dec hex filename
380 0 2048 2428 97c G8-asm.elf
I’ll add an infinte loop at the end and then try to debug it.
1: j 1b # loop
And it does work, and allows me to step through individual instructions in the source code.
Reminder: the GNU assembler supports both /* comments */ and # single line comments.
Adding a new source file, G8-asm.S, to the project directory does not automatically add it to the list of source files to be assembled. Moving G8-asm.S to the Startup folder doesn’t help.
The file itself is quite humble at this point:
# G8-asm.S
# part of G8-asm project
# 24 January 2025 - Dale Wheat
# G8-asm.S [end-of-file]
OK, after flailing about and even some Stack Overflow browsing, I accidentally discovered that if you remove/delete the ‘obj’ folder from the project, then add the file to the project, it finds it and incorporates it into the project correctly. Using the project ‘clean’ target does the same thing. So there’s two ways out of this particular pickle.
So now I begin harvesting the useful bits of startup_ch32x035.S and transplanting them into my new G8-asm.S file, beginning with the ‘_start’ symbol, required by the GNU linker. I suppose it can be called something else, and the linker of course needs to know where to ‘_start’. The ‘-e [name]’ command line option for the linker lets you rename it.
I also ‘dis-included’ the startup_ch32x035.S file from the project until I’ve extracted what I need from it.
You also have to declare the _start label as global, which is done like this:
.global _start
Interestingly, it need not precede the actual label. I don’t know how to combine the two things into a single statement, however. That’s the dream, isn’t it?
So the first actual thing I want the code to do is to set up the stack pointer to the end of SRAM. The MRS2 basic linker script has a complex formula that allows for a designated memory ‘heap’ for dynamic allocation as well as a specified stack size. I’m just going to use the physical end of SRAM, which on this part is 0x20005000. Technically, it’s one byte before that, 0x20004FFF, but the customs of the ABI are to decrement the stack pointer first, then store values (a stack ‘push’) then take off values and adjust the stack pointer back up afterward (a stack ‘pop’).
The MRS2 linker script defines a variable ‘_eusrstack’ (end of user stack) that happens to be the end of physical SRAM on this chip, i.e., 0x20005000. So this statement:
la sp, _eusrstack # initialize the stack pointer
will do the trick, but ‘la’ (load address) is actually a pseudoinstruction and breaks down
into two ‘real’ instructions:
la sp, _eusrstack # initialize the stack pointer
0: 20005117 auipc sp,0x20005
4: 00010113 mv sp,sp
The ‘mov sp,sp’ is only there because it thinks it needs to load the lower 12 bits as well, even when they are zero and the auipc instruction has already set them to zero.
This works:
lui sp, %hi(_eusrstack)
where the %hi() notation is called a RISC-V assembler modifier. It extracts the upper 20 bits of the constant, which happens to be all we need, as the lower 12 bits are all zero.
So now that I can debug these programs, I need to figure out how to see the contents of the registers while the program is executing.