Cheap chips from China – A brief look at the CH32V003 microcontroller

I admit I love AliExpress specifically for low-end electronics components. I often shop around for stuff like IC’s and microcontrollers. And while you can get certain Microchip (Atmel), Texas Instruments and STM devices attractively from China, this has been starting to feel a bit odd to me. After all – a nation that retrieves rocks from the dark side of the moon is surely capable of making a decent microcontroller by itself. So why purchase Western parts from China, and not dip into the actual Chinese-made (and, in particular, engineered) stuff? So that’s what I did!

…and I’m not the first in doing so, I’ll admit happily. Still, the use of microcontrollers of (more or less) Chinese origins is relatively new here in the West, so I figure maybe my hobbyist-perspective experiences can be of help to others trying the same.

The appeal of these Chinese controllers is clear: they’re cheap. Well, in principle. Indeed, if you buy large quantities and you’re OK with only being able to program the device once, you could probably end up shopping in the <$0.10 category. As an amateur, my typical order will be in the order of 10 or 20 devices only, and I’d like to be able to reprogram the device plenty of times as I develop and debug my application.

There are hurdles, of course, too. Distance, is one, and the associated shipping costs. Fortunately, today we can pay somewhat realistic rates for shipping when shopping from AliExpress (it used to be free, which sounded appealing, but evidently wasn’t sustainable in any way, and ethically dubious, too), pay VAT as we order and receive the items within a week or so. That’s fine, for me.

A bigger hurdle is language. Using a chip and in particular a microcontroller requires decent insight into what it can do, how it should be implemented and how to program it. So I really need decent quality English language documentation. There’s one company in particular that seems to have jumped into this gap: Nanjing Qinheng Microelectronics, or, in short…WCH (which apparently stands for WinChipHead, their ‘major brand’).

Now, in finding these guys, I was being pragmatic. I could have started drooling over whatever alternatives there are out there and then try to source them. In fact, I did look into the product lines offered by Padauk (they’re big in one-time-programmable controllers), but quickly found that they’re not really being offered on AliExpress. So I approached it the other way around and looked for what was available to me. And that turned out to be the entry-level 32-bit CH32V003 series by WCH. And, yes, it does come with a very decent summary datasheet as well as a more in-depth reference manual.

I ordered 20 pcs, which cost me a little shy of €8, including postage, bringing them down to €0.40 a piece. For this puny amount of money, I get a 32-bit controller with 2k SRAM, 16k flash, all the regular peripherals and it’ll run at up to 48MHz. It’s a bit like the heart of an Arduino Nano (which is/was an 8-bit device), but updated to the 2010s or thereabouts. O yeah, it has an opamp on board as well – neat! It’ll come in nicely if I need to buffer a high-impedance signal or just need an amplifier or comparator. I went for the QFN20-packaged F4U6 variant, because I intend to use these for devices that may need to be small. Well, this tiny 3x3mm QFN20 package definitely isn’t going to be a bottleneck in that sense!

I threw together a small test board that pretty much only breaks out most of the pins to some headers on the sides.

The board has a power LED (lit in the image above) and another LED hooked up to pin PC4 to be able to do a blink test with. There’s also space for a reset button, but I admit that I didn’t bother soldering it on there. I figured I’d add it when needed, and that moment simply hasn’t come yet. I still might, since the ‘reset’ pin as such isn’t really needed for e.g. programming the chip, so I could use the ‘reset’ button as a regular input button instead.

OK, so…how do I turn this thing on…? Or rather: how do I program this? What kind of toolchain can I use, and how do I physically connect it to a computer to download a hex file to it?

Fortunately, WCH have got all this covered. Some Googling later, I figured I would need the following:

  • A WCH-LinkE programmer. This is basically a USB dongle that can program a range of WCH-based chips, as well as (apparently) some others, like STM32F1 devices. Oh, it also has a USB-UART interface – how convenient! The CH32V003 is programmed with a single SWDIO data line (and +VCC and GND, obviously).
  • Some kind of integrated development environment (IDE) and a compiler and linker that will produce code for this particular device. WCH recommend the use of MounRiver Studio, which looks like a more or less direct clone of STM ‘Cube’ IDE’s (kind of a mix between STM8Cube and STM32CubeMX), and it comes with the compilers and linker that allow the creation of C and C++ (and Assembler) projects. Apparently, you could use PlatformIO as well, but I’ve not tried that approach, yet.

The WCH-LinkE programmer was another AliExpress purchase that set me back €8 including shipping. I got it from the “W Official Store” on AliExpress, which seems to be WCH’s consumer-oriented outlet. I’ve had a quick peek at what else they’re selling and I have to admit that it feels a bit like entering a candy store to me. Toys!

So when I had received the parts and put the little test board together, the moment of truth arrived. Getting a new-to-me controller to work is always a bit like heading out into the dark – after all, what to do if it doesn’t work?

The first thing I did was to try and get the programmer to do something. That was easy enough; plugging it into my Windows 10 machine, its USB-UART bridge was automatically installed correctly. However, Windows didn’t manage to automatically find a driver for the actual programmer part. Fortunately, there’s a rather complete manual for the WCH-Link programmer series as well. From this, I gleaned that the driver issue would settle itself if I just installed MounRiver Studio, so I downloaded that, installed it and hey presto – the WCH-LinkE enumerated just fine that way.

A feature of the WCH-LinkE is that it can program different types of controllers – specifically ARM and RISC-V types. My WCH-LinkE came set up in ARM mode initially, so I had to switch it into RISC-V mode for programming the CH32V003, which happens to be a RISC architecture core. Well, there’s a very nice switch with ‘ModeS’ silk-screened next to it, but there’s a bit of a snag….

…the programmer is housed in a very nice little protective case – without any holes. I didn’t really feel like prying it open just yet, so I tried a soft approach first. I went into MounRiver Studio and exported the WCH Link Utility from it.

I then fired up that utility and fortunately, it seemed to recognize the programmer just fine. At the bottom of the Utility’s screen, I found some functionality that seemed to do the mode switching stuff:

I clicked “Get” and it told me the Mode was “LinkDAP” (as shown above). I selected WCH-LinkRV instead (which is the RISC-V mode) and clicked “Set”, which seemed to work. I then verified whether it worked by going to Target>Query Chip Info and guess what…

…it lives! The little status screen came up with some info on memory size and a device ID:

I was pretty chuffed with that, because it showed me that pretty much everything worked!

I then went back into MounRiver Studio to try and get a simple LED flash project going. When making a new project, I automatically was presented with a test sketch that reads the UART port, inverts the data and sends it back out onto UART. Nice, because UART is convenient for debugging purposes and it’s very kind of the good people at WCH to throw a (more or less) working example straight into my lap. Still, I wanted to do the blink thing first, and sure enough, with a little help from the Reference Manual, I had that going in a jiffy.

Not just the Reference Manual is quite convenient – the vendor libraries are quite nicely done as well. I had been working with the STM32 CMSIS drivers for my recent densitometer project and the WCH equivalent is so similar that the transition was really smooth and I could make a flying start – impressive! I was expecting much more of a struggle with Chinese commented code, a lack of documentation or other kinds of “terra incognita” roving around. None of that, really; it’s really not much different from working with a low-end STM32 controller. In fact, some things seem to be even easier to manage, such as setting the UART baud rate, for which there’s a convenient function (yes, STM’s HAL also is convenient, and kudos for them for offering it, but it’s also godawfully bloated to the point of being literally dysfunctional on lightweight platforms….)

So, no snags, whatsoever…?

Overall, it’s been pretty smooth sailing Monday – as it took me literally a single day to build the hardware and get it to work (layout the PCB, etch it, populate it, figure out the programmer etc.) But there were two snags alright.

The first is the interrupt thing. If you Google a bit on “CH32V003 interrupt” you’ll notice it has a particular way of handling entering and exiting interrupt service routines. It took me some effort to figure this out, and yet again earlier work on the STM8 and STM32 saved me some time, since I knew where to look for a list of ISR handles (it’s in file startup_ch32v00x.S):


    .word   0
    .word   NMI_Handler                  /* NMI Handler */
    .word   HardFault_Handler            /* Hard Fault Handler */
    .word   0
    .word   0
    .word   0
    .word   0
    .word   0
    .word   0
    .word   0
    .word   0
    .word   SysTick_Handler             /* SysTick Handler */
    .word   0
    .word   SW_Handler                  /* SW Handler */
    .word   0
    /* External Interrupts */
    .word   WWDG_IRQHandler         	/* Window Watchdog */
    .word   PVD_IRQHandler          	/* PVD through EXTI Line detect */
    .word   FLASH_IRQHandler        	/* Flash */
    .word   RCC_IRQHandler          	/* RCC */
    .word   EXTI7_0_IRQHandler       	/* EXTI Line 7..0 */
    .word   AWU_IRQHandler              /* AWU */
    .word   DMA1_Channel1_IRQHandler   	/* DMA1 Channel 1 */
    .word   DMA1_Channel2_IRQHandler   	/* DMA1 Channel 2 */
    .word   DMA1_Channel3_IRQHandler   	/* DMA1 Channel 3 */
    .word   DMA1_Channel4_IRQHandler   	/* DMA1 Channel 4 */
    .word   DMA1_Channel5_IRQHandler   	/* DMA1 Channel 5 */
    .word   DMA1_Channel6_IRQHandler   	/* DMA1 Channel 6 */
    .word   DMA1_Channel7_IRQHandler   	/* DMA1 Channel 7 */
    .word   ADC1_IRQHandler          	/* ADC1 */
    .word   I2C1_EV_IRQHandler         	/* I2C1 Event */
    .word   I2C1_ER_IRQHandler         	/* I2C1 Error */
    .word   USART1_IRQHandler          	/* USART1 */
	.word   SPI1_IRQHandler            	/* SPI1 */
	.word   TIM1_BRK_IRQHandler        	/* TIM1 Break */
    .word   TIM1_UP_IRQHandler         	/* TIM1 Update */
    .word   TIM1_TRG_COM_IRQHandler    	/* TIM1 Trigger and Commutation */
    .word   TIM1_CC_IRQHandler         	/* TIM1 Capture Compare */
    .word   TIM2_IRQHandler            	/* TIM2 */

Handling the entering/exiting of the ISR in a way that works well for this controller is done with a manufacturer-specific attribute. For C++, I ended up with this implementation, showing an “Arduino millis” inspired millisecond counter based on the SysTick timer:

extern "C" void SysTick_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

extern "C" void SysTick_Handler(void) {
    SysTick->CTLR &= ~(0x01 << 31);     //Disable software interrupt CTRL->SWIE = 1 (b31)
    millis ++;
    SysTick->CTLR |= (0x01 << 31);      //Re-enable software interrupt
    SysTick->SR = 0;                    //Clear comparison reached flag
}

Note the particular __attribute__ in the declaration.

The other snag was to actually get this thing to work in C++…By default, making a project results in a C-only project. Which was fine for doing my blink thing. But I’ll want to use object oriented approaches at some point, so figuring how to kick this into C++ gear was an important step as well. Unfortunately, things aren’t quite as nicely streamlined at WCH as they are at, say, STM.

Chance favors the prepared, and while I was waiting for the chips to arrive, I had been Googling a bit on my phone and I just knew I had read something about this. Sure enough, WCH maintains a GitHub page and there’s a guide to convert a MounRiver Studio project from C to C++ (it fortunately comes with a perfectly nice GCC compiler). You can find it here. The process is somewhat involved, as you will need to create (or rename) a main.cpp file, convert the MRS project to C++, add the header directories to the C++ compiler path and then also add a few lines of code to the startup.S file and add some (empty) functions to a .c file somewhere in your project.

Other than these two minor issues (which, admittedly, did cost me a couple of hours of head-scratching and experimentation), it was relatively smooth sailing. The thing works, basically – it’s now happily blinking on the basis of a SysTick timer, merrily merrily chatting to the desktop over UART about how many seconds it’s been doing this stilly thing. Which means that from here, the sky is the limit. Or, well, you know – simple things with I2C devices and whatnot.

I’m pretty darn impressed with what the Chinese will sell you for €0.40, postage included!

2 thoughts on “Cheap chips from China – A brief look at the CH32V003 microcontroller”

  1. I have been using this CH32V003 IC for a year now, but recently decided to use Arduino IDE as my editor and compiler as I find it easier to develop small projects.
    The tricky part I had been struggling with was the use of a TIMER interrupt, which I couldn’t get to work, at least not until I read this article and saw the use of the __attribute__ on the declaration, which fixed the problem I was having, all my code was correct in the C version for Arduino, but this part was missing, which was a way to tell the compiler the address of the routine bound to the interrupt handler.
    Thanks a Lot!

    1. Great to hear! I haven’t tried this controller in Arduino IDE yet, but it makes good sense that you need to take the same measures to signal to the compiler that a function is indeed an ISR. I spent quite some time digging into this issue, so I’m glad someone found it useful!

Leave a Reply

Your email address will not be published. Required fields are marked *