21 February 2025
I was setting up a small CH32V003 demo project to see which GPIO pin got toggled in the default MounRiver Studio 2 application. On the CH32X035 default application, it’s PA0 (GPIO port A, pin 0). But there’s no PA0 on the CH32V003, even on the largest package. So which pin is it?
The answer surprised me.
It turns out that the default application generated by MRS2 does not blink an LED or toggle a GPIO pin at all. It sets up the USART to receive and echo characters via the virtual serial port on the WCH-LinkE programming adapter. Technically, it doesn’t faithfully echo the character; it inverts all the bits of any received character then transmits that back to the console.
So as not to feel entirely shut down, I plowed ahead and made it blink an LED. No PA0, you say? No problem, I answer. I see that there is a PA1, which is pin 2 on the CH32V003F4U6 I’m using, and that should do just as well.
I added the requisite code to enable GPIOA and set up PA1 as a push-pull output of modest speed:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // enable GPIOA peripheral clock
GPIO_InitTypeDef GPIO_InitStructure = { 0 };
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // PA1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // output, push-pull
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; // doesn't have to be fast
GPIO_Init(GPIOA, &GPIO_InitStructure);
Actually, I cut and pasted that code from the existing, pre-generated code from the project that sets up the USART. Now since the default mapping of the USART’s transmit (PD5) and receive (PD6) pins belong entirely to GPIOD, not GPIOA, it was a while before I noticed that my initialization code was wrong. This only took an embarrassingly long time to find by single-stepping the code and looking at the bits in the configuration register for the GPIOA peripheral.
So once I had discovered that I was, in fact, re-initializing GPIOD, at least pin 1 in any case, I assumed it would just start working. I had attached one of my very favorite LEDs, a 5mm blue LED from waaay back. Indeed, it might be one of the first blue LEDs I ever obtained. I still remember the moment that Billy Gage of BG Micro showed these to me. It was an amazing experience.
So I’ve attached the blue LED with its requisite 270Ω resistor between PA1 and ground. You know the steps: save the file, recompile and download. Blinky blue goodness?
Goodness, no. Still no blinkage. I’m getting just a little exasperated at this point. Blinking an LED is the “Hello, world!” of the embedded development world. It is both a rite of passage and a trivial accomplishment at the same time. I would have assumed at this point in my career and at this particular point on my learning curve of these devices that I would be seeing a blinking blue LED. I had done it before, countless times. I would do it again!
The only other thing I could think of was that the PA1 pin had been re-mapped to a different function. These new microcontrollers have so many internal peripherals that not all of them get their very own pins. Scarce resources must be thoughtfully allocated. I looked up the re-mapping options for this pin in the CH32V003 data sheet. Yes, it can be re-purposed as the external crystal input, OSCI. But I hadn’t asked it to do that.
Cranking up the debugger again (how would I even survive without this tool?), I look at the remap register PCFR1 in the AFIO peripheral, and there is PA12_RM. Now that’s not the best possible name for it, is it? It’s the ‘remap option bit for PA1 and PA2’, but it sure looks like they are referring to a mysterious PA12, which totally does not exist on this chip.
And yes, the bit is set, meaning that the function of PA1 (and PA2) has been shifted over to quartz crystal oscillator duty, and not GPIO function, as I intended.
Now this is not the default state of this re-mapping option. Someone, somewhere, was sneaking in during the night, replacing everything with an exact duplicate and setting that bit in blatant contradiction to my wishes.
Something told me to review the clock options located in the “system_ch32v00x.c” source file, which is created by the MRS2 new project wizard. Sure enough, it had selected “#define SYSCLK_FREQ_48MHz_HSE 48000000” as the default clock for the system. The HSE is the “High Speed External” oscillator. My circuit has no quartz crystal attached to PA1 and PA2. You might remember that I have a very special blue LED attached to PA1. PA2 happens to have a WS2812 programmable LED attached to it, but I wasn’t even going to play with that (yet).
Changing the selection to “#define SYSCLK_FREQ_8MHz_HSI 8000000”, saving, recompiling and downloading finally gave me the blinky blue triumph I felt that I deserved at this point. Whew!
Now you may be asking, “How could the system even run at all with no crystal attached, if that was how it was configured to run?” And that would be an excellent question. The answer is that it goes through a sequence of steps to get to that point, and when any of those steps fail, it just continues on. There is an internal variant of the high speed oscillator, properly named the HSI oscillator, that is always present and is on by default when the chip first powers up. It runs at a nominal 24 MHz, but can be divided a selection of integer prescalers (1, 2, 3, 4, 5, 6, 7, 8, 16, 32, 64, 128 and 256). It divides the 24 MHz signal by 3 to give me my selected 8 MHz clock, once I specified it correctly. So it was previously running at 24 MHz, clocked from the HSI oscillator, since the HSE failed to start, and so it never even tried enabled the built-in phase locked loop to double the frequency to 48 MHz. Additionally, the mechanism to switch system clocks will just silently ignore your request if the required signal is not available and stable.
So now I have my blinking blue LED and all is well with the world. I should stop here, right? Always quit a winner, they say.
Well, of course not. Now is the time to answer all the other nagging questions I have had about certain aspects of this chip, and specifically some of the functions of the pins.
Having first been introduced to this family of chips by the smaller, eight pin packaged CH32V003J4, I had struggled to understand the availability of pins and functions. That particular beastie has multiple GPIO pins tied to each physical pin – but not all of them! PD7, GPIO port D, pin 7, which can double as the external reset signal NRST, was not pinned out at all. On the expansive F4U6 package (QFN20, quad flat no leads 20 pins) sitting before me, PD7 is brought out to pin 1. Now what will it take to actually be able to use this pin as a chip reset?
The answer might surprise you.
Nothing, actually. It’s already set up from the factory to be the reset input signal. In fact, you would have to go into the ‘user option bytes’ and change the configuration of the RST_MODE field to allow PD7 to be used as a GPIO pin. Then you would have to reset the chip for the new setting to take place.
I set out to confirm this theory by connecting a momentary push button switch between PD7 and ground. When I press the button, the chip resets. If I hold the button down, the chip does nothing at all.
Now a clever sort of developer could enable PD7 as a GPIO pin, then connect an external interrupt to it, so that it could be an ‘intelligent’ reset input, while still being completely asynchronous. The interrupt handler would consider the ‘request for reset’ and decide, based on what was important at the time, whether to reset or not. Resetting the chip from code can actually be done in a number of ways. How many do you know? Share your favorites in the comments.
So that experiment was quick and satisfying for me. PD7, which is available on every package except the -J4 SOP8, is a perfectly cromulent nRST input, and works exactly as one would expect it to work.
So does that wrap up all the experimentation for today? Can you not see the scroll bar on the right side of this screen? Of course it doesn’t!
Reviewing the pinout of the F4U6 package, I see that GPIO port A has only two pins present, PA1 and PA2, while ports C and D both have eight pins each. Now still thinking that these devices are bigger on the inside than they are on the outside, as far as available peripheral connections to available physical pins is concerned, it seems to me that it was odd that GPIOA only had two pins to it. It probably wasn’t even going to have any pins, as those two pins would or could be allocated to an external crystal for system clocking purposes. But it would seem to be a waste of two perfectly good pins if the end-application did not require the exquisitely precise timing that a quartz-based oscillator can provide. So they wisely put in another GPIO port on the chip.
But does it really only have two pins in it? Or is it, and this was my suspicion, actually an exact copy of the other two ports, GPIOC and GPIOD, with a total of eight ‘pins’ internally and only two of those pins brought out to physical pins on the package?
Now without decapsulating the device and taking some pictures through a microscope, which is completely a reasonable thing to do in someone else’s laboratory (not mine), how could we determine if those phantom pins exist or not?
One way would be to write various bit patterns to the output register, OUTDR, then read them back in and see if they all toggled on and off in unison. So just to be exhaustive, I wrote a short loop that wrote all 256 combination of ones and zeros to GPIOA->OUTDR, then read them back in and compared the results. If they all matched, it meant that all eight bits were realized internally and just not pinned out. If there were mismatches, it would indicate that some or all of the other bits were, in fact, unimplemented.
So I got tons and tons of mismatches. But since I was writing them out one line at a time on the serial terminal, the first results scrolled past too fast to examine.
I added a dummy ‘getchar()’ call to wait for the user (me) to hit a key on the keyboard after every 16 lines, for a very simple sort of pagination of the output.
For some reason that I have yet to investigate, the getchar() function simply returns without ‘getting’ any ‘chars’ at all. It probably has something to do with the fact that I have not provided a low-level read() function for the stdio library to use to let it know whence the aforementioned characters. An experiment for a future day.
Since I already had the USART initialized for the console output using the printf() function, et al., I just called the SDK-provided function to wait for a character to arrive, then read and discard said character. Pagination accomplished.
Now while my progression of output test patterns went from 0x00 to 0xFF in the expected order, the returned values that were read back in consisted only of 0x00, 0x02, 0x04 and 0x06. These values represent the four possible states in binary of bit positions 1 and 2, or PA1 and PA2 as we know them.
The conclusion I reach at this point is that only the two published GPIO pins, PA1 and PA2, are actually implemented on this chip. Do you agree or disagree with my conclusion? What other testing methodology should I apply to dive deeper into this Important Scientific Investigation?
Just to help me feel better about the testing I had done on GPIOA, I repeated the same test on both GPIOC and GPIOD. In all 256 cases, each port read back the exact expected value as had been written to it. All eight bits of GPIOC and GPIOD are implemented, which is not surprising at all as they have all been routed to different pins on the package. But it does give me a positive result to help me have a little confidence in my testing strategy.
What I found especially interesting about the testing on GPIOD was that it ‘succeeded’ even when some of the pins were being used for other function, such as the USART (PD5 and PD6) and the nRST input (PD7).
But you may be asking, “Wait a minute… what happened to GPIO port B?” And that would be an excellent question. So I set out to try to discover if there was any vestige of a GPIO port B on the chip.
The first thing I did was to try to set the ‘peripheral reset’ bit for GPIOB in the RCC peripheral. There are bits defined to reset the GPIO ports A, C and D, as well as the AFIO (alternate function input output controller) peripheral. There is a suspiciously ‘reserved’ spot between IOPARST and IOPCRST bits in the APB2PRSTR register within RCC. I fudged my own definition for this missing bit, as well as the upcoming IOPBEN peripheral clock enable bits, like this:
#define RCC_IOPBRST (1 << 3) // not defined in device header (for a reason)
#define RCC_IOPBEN (1 << 3) // not defined in device header (for a reason)
If you write all ones into this peripheral reset register (there are two of them, actually), then read back that register, you will find that only some of the bits still have ones in them. Those ones represent peripherals that are 1) implemented and 2) able to be reset. GPIOB, as represented by my completely fake RCC_IOPBRST bit, was a zero.
Now remember to release all those peripherals that you just reset or they will remain in a reset state in perpetuity.
You can do the same thing with the peripheral clock enable registers (there are three of these in total). Again, GPIOB fails to stick when writing a one to the RCC_IOPBEN bit.
So there really is no GPIOB implemented on this chip.
Now we know.