Posted on Leave a comment

CH32V003 driving WS2812B LEDs with SPI – Part 7

4 March 2025

Today I see that my overnight testing of the -F4P6 packaged CH32V003 chips shows no errors for over 144 million loops on the WCH board and over 91 million loops for my own development board. While I was able to induce some “power glitches” in my own board (enhanced wiggling), the WCH board proves to be more robust.

Part of me thinks that this issue of the SPI locking up has to be a software issue. But how could that be? Am I not running the exact same software across all the various chips?

Well, maybe I am and maybe I’m not. If the factory can program in a different “device identifier” in the Vendor Bytes area of the flash memory, depending on package type, could they also change some other memory contents? There’s a boot loader in there, somewhere, and who knows what else.

A quick search for “0x1FFF” through the code created by the MRS2 new project minion reveals some interesting items. This is the first part of the address for the “System FLASH” section of the memory map.

The device description header file, ch32v00x.h, contains these #define’d values:

#define OB_BASE          ((uint32_t)0x1FFFF800)    /* Flash Option Bytes base address */
#define VENDOR_CFG0_BASE ((uint32_t)0x1FFFF7D4)

We also see the reference to address 0x1FFFF7C4 in the DBGMCU_Get…ID() functions mentioned previously.

A surprising find is the GPIO_IPD_Unused() function in the /Peripheral/src/ch32v00x_gpio.c file. It specifically configures GPIOC and GPIOD’s unused pins as inputs with pull-up resistors, but only for the less-than-twenty-pin packages, the -A4M6 and -J4M6. I can’t find where this function is actually being called in the supplied source code, but that doesn’t mean that it’s not being called from a pre-compiled library.

So what exactly is “VENDOR_CFG0_BASE” used for? It’s only reference by the very next line in the device definition file, like this:

#define CFG0_PLL_TRIM (VENDOR_CFG0_BASE)

This address is referenced by the RCC_SYSCLKConfig() and SetSysClockTo_48MHZ_HSI() functions, which I assume help to trim the HSI when it is being used as the system clock.

So there’s another “magic number” being stored in the flash, as set up by the factory.

But what about those boot loaders? Let’s grab all those and hold them up to the light.

To get the contents of a memory region from inside the chip and into a file for our examination, I will use the ‘wlink’ utility, available from:

https://github.com/ch32-rs/wlink

It’s a Rust application that speaks directly to the WCH-LinkE and similar devices. The command to read memory contents is called ‘dump’, and the specific syntax to capture the boot loader area of the memory is:

wlink dump 0x1FFFF000 1920 --out bootloader_xxx.bin

where bootloader_xxx.bin will be renamed for each of the four samples we seek.

For the -F4P6 version, the wlink utility responded with:

00:04:04 [INFO] Connected to WCH-Link v2.15(v35) (WCH-LinkE-CH32V305)
00:04:04 [INFO] Attached chip: CH32V003 [CH32V003F4P6] (ChipID: 0x00300500)
00:04:04 [INFO] Read memory from 0x1ffff000 to 0x1ffff780
00:04:04 [INFO] 1920 bytes written to file bootloader_CH32V003F4P6.bin

I’m not 100% sure what the time-stamp at the beginning of each line means, but in any case it happened pretty quickly. Interestingly, the utility did not disturb the little chip too much in the performance of its duties, as it kept right along, and didn’t lose count of its statistics.

Now for the CH32V003A4M6 variant:

00:09:26 [INFO] Connected to WCH-Link v2.15(v35) (WCH-LinkE-CH32V305)
00:09:26 [INFO] Attached chip: CH32V003 [CH32V003A4M6] (ChipID: 0x00320500)
00:09:26 [INFO] Read memory from 0x1ffff000 to 0x1ffff780
00:09:26 [INFO] 1920 bytes written to file bootloader_CH32V003A4P6.bin

These two files are identical. Let’s gather more data. The -J4P6 variant produces this message:

00:14:50 [INFO] Connected to WCH-Link v2.15(v35) (WCH-LinkE-CH32V305)
00:14:50 [INFO] Attached chip: CH32V003 [CH32V003J4M6] (ChipID: 0x00330500)
00:14:50 [INFO] Read memory from 0x1ffff000 to 0x1ffff780
00:14:50 [INFO] 1920 bytes written to file bootloader_CH32V003J4P6.bin

It’s also the same file. Only one more candidate to investigate: the original troublemaker, the CH32V003F4U6:

00:18:21 [INFO] Connected to WCH-Link v2.15(v35) (WCH-LinkE-CH32V305)
00:18:21 [INFO] Attached chip: CH32V003 [CH32V003F4U6] (ChipID: 0x00310510)
00:18:21 [INFO] Read memory from 0x1ffff000 to 0x1ffff780
00:18:21 [INFO] 1920 bytes written to file bootloader_CH32V003F4U6.bin

All boot loaders are identical. Or at least all the boot loaders in the chips before me are identical. It’s also nice to have the ChipID recorded for each of these samples, as well.

Now we should look at the “Vendor Bytes” section of flash, which is 64 bytes long and starts at address 0x1FFFF7C0. The wlink command line would look like this:

wlink dump 0x1FFFF7C0 64 --out vendor_bytes_CH32V003F4P6.bin

As expected, the files differ. The ‘diff’ utility confirms this quite tersely, without going into much detail:

Binary files vendor_bytes_CH32V003F4P6.bin and vendor_bytes_CH32V003A4P6.bin differ

Well, yeah. Let’s gather the rest of the ‘Vendor Bytes’ images from the remaining chips.

Here’s what’s in the ‘Vendor Bytes’ section of the -F4P6’s flash memory:

00000000 34 FE 78 DC 00 05 30 00 09 18 2A 13 03 5A 00 00
00000010 FF FF FF FF FF FF FF FF 00 00 00 00 05 FA AA 55
00000020 10 00 FF FF FF FF FF FF CD AB B3 A5 49 BC C9 0D
00000030 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

We can see the ChipID word at offset 0x04: 0x1FFFF7C4, expressed in “little endian’ fashion as “00 05 30 00”, or 0x00300500 in hexadecimal. I’m guessing that the ‘FF’ data is not being used at this time. That’s the “erased” state of this type of memory.

Now let’s compare that to the -A4P6 data:

00000000 34 FE 78 DC 00 05 32 00 09 18 3F 13 03 5A 00 00
00000010 FF FF FF FF FF FF FF FF 00 00 00 00 05 FA AA 55
00000020 10 00 FF FF FF FF FF FF CD AB D8 A8 05 BC AA 10
00000030 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

The ChipID is different, of course, being 0x00320500 for the -A4P6 package. There’s also a change at offset 0x0A and several starting at offset 0x29. Those might be the HSI calibration data, but I’m totally speculating here. Of course, there’s a way to find out, but let’s continue with this particular exercise, shall we?

The -J4M6 SOP8 package has this data:

00000000 34 FE 78 DC 00 05 32 00 0A 18 4C 13 03 5A 00 00
00000010 FF FF FF FF FF FF FF FF 00 00 00 00 05 FA AA 55
00000020 10 00 FF FF FF FF FF FF CD AB DB 87 59 BC 01 F0
00000030 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

This shows the same pattern of similarities and differences.

Lastly, here is the CH32V003F4U6 data:

00000000 34 FE 78 DC 10 05 31 00 09 18 3C 13 03 5A 00 00
00000010 FF FF FF FF 0E 00 00 00 FF FF FF FF 05 FA AA 55
00000020 10 00 FF FF FF FF FF FF CD AB EA 1A F0 BC A7 83
00000030 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

There’s something different here, starting at offsets 0x14 and 0x20. Would you like to guess? That’s all I can do at the moment.

The RM, p. 169, Table 15-1 “ESIG-related registers list”, does tell us about some of the fields in this range:

Address    Offset Name
---------- ------ -----------------------
0x1FFFF7E0 0x20   Flash capacity register
0x1FFFF7E8 0x28   UID register 1
0x1FFFF7EC 0x2C   UID register 2
0x1FFFF7F0 0x30   UID register 3

It’s interesting to me that in every case, the “unique identification code”, which is specified as 96 bits long, has all ones (FF FF FF FF) as the upper 32 bits, yielding “only” 64 bits of ID.

Another way to test this “same or different software” questions is to write my own bare-metal code for this testing, instead of relying on the WCH-supplied SDK. I have been exploring a couple of alternatives already, one for C language programming and another for RISC-V assembly language use. I’ll describe these in more detail for you tomorrow.

Leave a Reply