CH32V examples and bootloader question

Hi all,

recently I started playing with the new CH32Vxx family of RISC-V microcontrollers. For this I use the PIO platform implementation ( by @maxgerhardt (thanks a lot!).

As a first step I started porting the original CH32V003 examples by OpenWCH to PIO. And for most of them porting consists merely of copy&paste. You can find the ports on my Github repo.

The only example I’m struggling with is the bootloader example, as I lack understanding of both PIO and CH32V003 :slightly_frowning_face: Here’s my current understanding:

Now my questions:

  1. is my above understanding correct?
  2. how can I instruct PIO to link the bootloader such that the storage address (for upload) starts at 0x1FFF F000, but that the address after re-mapping (for operation) starts at 0x0000 0000…? Or is this not required for some reason…?

Thanks a lot in advance!

Greetings, Georg

1 Like

Seems to be correct at first glance, though I’ll want to read up on the

one time closer again because it seems weird to me how an app couldn’t also just write to the flash controller registers and reprogram it…

As I see it, the “USART_IAP” project constists of the bootloader, linked with

which is just at 0x0 with a lenght restrictions of 1920 bytes, matching the datasheet

And further reading the reference manual gives me the impression that you indeed have to set a register bit to either reboot to Bootloader memory (so 0x1fff f000 will be remaped to 0x0) or the app (regular code flash remapped to 0x0)

And that remapping & reboot has to be triggered via the functions described by you already.

The bootloader / IAP program has to be written once at the specified address though, like the chinese PDF says…

将 IAP 程序通过 WCH—LinkUtility 下载到 0x1FFFF000。
→ “Download the IAP program to 0x1FFFF000 via WCH-LinkUtility.”

While we do have the facilities for it to change the start linker address, that’s not needed then because in IAP / Bootloader mode, the memory is remapped and so the app will run from 0x0 and thus should be compiled / linked as if it were at 0x0.

Hi Max,

do I understand correctly that both APP and IAP codes are linked for starting address 0x00, but are uploaded to 0x8000000 (APP) or 0x1FFFF000 (IAP)…? Actually that makes much more sense than my initial thought!

But then the next question: how do change the target upload address in PlatformIO for IAP? Apparently the default setting is set to 0x8000000 (APP)…? Thanks a lot in advance!

Regards, Georg

During an upload, OpenOCD is given the .elf file (upload_target = target_elf), from which it will automatically figure out the upload address based on the start addresses of the to-be-loaded sections, which are determined by the linker script of the application.

In PlatformIO we give it a unified linking script of

where the #flash_start template variables are substituted based on defaults or given platformio.ini values

meaning that, if you would add

board_upload.offset_address = 0x8000000 

the application would be linked to be starting at 0x8000000 and the upload program would also try and write to this address.

Note that the default is 0x0 for the flash origin address, because 0x8000000 would be remapped to 0x0 for a normal app run (just like the bootloader would).

…that however results in the pecularity that the flash origin address (linker script) can’t be independently controlled from the upload address, with the current logic they’ll always match. However, if say we want to build + upload the IAP bootloader using PlatformIO, it might be okay to use board_upload.offset_address = 0x1FFFF000 because 1. it will cause the program to be uploaded to the correct address and 2. even if the MCU remaps 0x0 → 0x1FFFF000, I think the memory is also still available, or rather mirrored, at the original 0x1FFFF000, so it happens to work.

Sadly I’ve not tested any of this yet.

Hi Max,

I have tried as you said:

  1. in platformio.ini add board_upload.offset_address = 0x1FFFF000

  2. build project IAP → success

RAM:   [==        ]  22.9% (used 468 bytes from 2048 bytes)
Flash: [=         ]  13.3% (used 2176 bytes from 16384 bytes)
.pio/build/ch32v003f4p6_evt_r0/firmware.elf  :
section           size        addr
.init              160   0x1ffff000
.vector             32   0x1ffff0a0
.text             1976   0x1ffff0c0
.fini                0   0x1ffff878
.dalign              0   0x20000000
.dlalign             0   0x1ffff878
.data                8   0x20000000
.bss               204   0x20000008
.stack             256   0x20000700
.debug_line      21528         0x0
.debug_info      24599         0x0
.debug_abbrev     5703         0x0
.debug_aranges    1120         0x0
.debug_str        6077         0x0
.debug_ranges     1008         0x0
.debug_loc        6072         0x0
.comment            51         0x0
.debug_frame      2488         0x0
Total            71282
  1. upload → fail with verify error
** Programming Started **
Info : device id = 0xaa42abcd
Info : flash size = 16kbytes
Warn : no flash bank found for address 0x1ffff000
Warn : no flash bank found for address 0x1ffff878
** Programming Finished **
** Verify Started **
Error: checksum mismatch - attempting binary compare
embedded:startup.tcl:1162: Error: ** Verify Failed **
in procedure 'program' 
in procedure 'program_error' called at file "embedded:startup.tcl", line 1223
at file "embedded:startup.tcl", line 1162
*** [upload] Error 1

I noticed that the build log reports a total codesize of ~69kB - mainly due to debug code. But according to the DS the “System Flash” has only 1920B. Can that be the problem?

Then OpenOCD may not be capable of that.

Can you use the WCH-LinkUtility instead like the chinese docs say? If you have MounRiver IDE installed, it’s C:\MounRiver\MounRiver_Studio\ExTool\SWDTool\WCH-LinkUtility.exe, otherwise,

hi again,
I haven’t Windows installed (anymore). Therefore I tried wlink. After some issues I can now use it to upload code to System Flash. Here’s the output of uploading a small (dummy) binary:

$ .cargo/bin/wlink flash test.bin --address 0x1FFFF000
20:45:08 [INFO] WCH-Link v2.8 (WCH-LinkE-CH32V305)
20:45:08 [INFO] Attached chip: CH32V003(0x00300500)
20:45:08 [INFO] Read test.bin as Binary format
20:45:08 [INFO] Flashing 257 bytes to 0x1ffff000
20:45:08 [INFO] Flash done
20:45:09 [INFO] Now reset...
20:45:09 [INFO] Resume executing...

But when I try to upload the IAP example built from PIO, I get the below error message and the bootloader is not working

$ .cargo/bin/wlink flash firmware.bin --address 0x1FFFF000
20:53:55 [INFO] WCH-Link v2.8 (WCH-LinkE-CH32V305)
20:53:55 [INFO] Attached chip: CH32V003(0x00300500)
20:53:55 [INFO] Read firmware.bin as Binary format
20:53:55 [INFO] Flashing 2176 bytes to 0x1ffff000
20:53:57 [INFO] Flash done
20:53:58 [INFO] Now reset...
Error: WCH-Link underlying protocol error: 55 [02]

IMHO that is because the IAP binary (2176B) is larger than the System Flash size (1920B). To me the below verbose build output seems to indicate that the debug code is responsible for most of that. Is there any way to omit debug code? And while we’re at it, maybe also tell PIO that for this project the flash size is only 1920B, not 16kB…?

Processing ch32v003f4p6_evt_r0 (board: ch32v003f4p6_evt_r0; platform: ch32v; framework: noneos-sdk; monitor_speed: 115200)
PLATFORM: WCH CH32V (1.0.0+sha.5894b83) (git+ > CH32V003F4P6-EVT-R0
HARDWARE: CH32V003F4P6 48MHz, 2KB RAM, 16KB Flash
DEBUG: Current (wch-link) On-board (wch-link)
 - framework-wch-noneos-sdk @ 2.10000.0+sha.22fd326 (git+ 
 - tool-openocd-riscv-wch @ 2.1100.230329 (11.0) 
 - toolchain-riscv @ 1.80200.190731+sha.e8e7ba9 (git+
LDF: Library Dependency Finder ->
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 0 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
MethodWrapper(["checkprogsize"], [".pio/build/ch32v003f4p6_evt_r0/firmware.elf"])
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [==        ]  22.9% (used 468 bytes from 2048 bytes)
Flash: [=         ]  13.3% (used 2176 bytes from 16384 bytes)
.pio/build/ch32v003f4p6_evt_r0/firmware.elf  :
section           size        addr
.init              160         0x0
.vector             32        0xa0
.text             1976        0xc0
.fini                0       0x878
.dalign              0   0x20000000
.dlalign             0       0x878
.data                8   0x20000000
.bss               204   0x20000008
.stack             256   0x20000700
.debug_line      21528         0x0
.debug_info      24599         0x0
.debug_abbrev     5703         0x0
.debug_aranges    1120         0x0
.debug_str        6077         0x0
.debug_ranges     1008         0x0
.debug_loc        6072         0x0
.comment            51         0x0
.debug_frame      2488         0x0
Total            71282

No the .debug sections do not contribute to the .bin file, they’re only debug info (because I do activate -g in release builds so that .elf have some symbol info…)

Only these ones are relevant. Flash is filled with .init, .vctor, .text, and that’s bigger than 1920 bytes.

However, if I compile the project it uses exactly 1920 bytes.

RAM:   [==        ]  22.9% (used 468 bytes from 2048 bytes)
Flash: [==========]  100.0% (used 1920 bytes from 1920 bytes)

(map file in amap:)

Weird! Here’s the output from a build under Linux / Xubuntu 22.04 for the original WCH IAP example:

RAM:   [==        ]  22.9% (used 468 bytes from 2048 bytes)
Flash: [=         ]  13.3% (used 2172 bytes from 16384 bytes)

and from map file in amap:

  1. Section .text seems to be responsible for the size difference. Does Linux use an older version of compiler toolchain? Or do I need to add a “special option” to platform.ini?
  2. The numbers in both your and my case don’t really add up to 1920B and 2172B, respectively. Which sections actually end up in flash?
  3. How do you tell PIO that System Flash is only 1920B?

Sorry, my mistake! If I use the project from your link, I also get exactly 1920B:

RAM:   [==        ]  22.9% (used 468 bytes from 2048 bytes)
Flash: [==========]  100.0% (used 1920 bytes from 1920 bytes)

Now I’ll check what the difference is…

Ok, update:

  1. I can now build the CH32V003 IAP bootloader example and upload it via wlink to system flash :slight_smile:
  2. adding the below 2 lines to platformio.ini allows to build for address 0x00 but upload to system flash
   upload_protocol = custom
   upload_command = wlink flash $SOURCE --address 0x1FFFF000
  1. after optimizing the example a bit I could add a mode indication by LED (fast blinking). The APP example blinks the same pin slowly. That way I always know which code is actually running
  2. then I seemingly bricked the device:
    • while playing around with the IAP example I commented the USART code and the mode switching. I figured I can always flash via debug interface
    • switching to IAP mode worked as expected (LED blinks fast), but now WCH-LinkE can no longer connect (Error: WCH-Link underlying protocol error: 55 [01]). Power off/on also has no effect, device remains in IAP mode. And since mode switching is commented, I cannot exit IAP mode

To my understanding that means:

  • boot mode information is indeed stored in flash (see initial post). Else information would be lost after power off
  • debug not possibly in IAP mode indicates that on µC side debug seems to be via code included in normal applications, which is omitted in IAP example. Is this why a special linker file is required? Or is there another information why the debugger can connect in APP mode, but not in IAP mode?

PS: any idea if/how I can recover my device? I don’t mind the 10ct, but replacing the µC is a pain

1 Like

Another person also thought they had bricked their ch32v003, but with a bit of timing, you can hold down the RESET button of the board (or power it off), press “Upload”, then when the uploader seems to hang or connect, release reset (or give power). Might need multiple tries.

thanks for the hint, but even after trying very often I wasn’t able to recover my CH32V003. Specifically I tried with WCHLink-Utility, WLINK, OpenOCD - but to no avail :frowning:

Seems like I have to replace the µC

PS: can you confirm my suspicion that debugging on µC side is via SW that is linked into the code? If not, why can debugger not connect to a device which is in IAP mode?

I’m trying to upload code via Platformio ->Upload and getting an error:


Configuring upload protocol…
AVAILABLE: isp, minichlink, wch-link
CURRENT: upload_protocol = wch-link
Uploading .pio\build\genericCH32V003F4P6\firmware.elf
Open On-Chip Debugger 0.11.0+dev-02415-gfad123a16-dirty (2023-01-03-10:00)
Licensed under GNU GPL v2
For bug reports, read
OpenOCD: Bug Reporting
debug_level: 1

Warn : Transport “sdi” was already selected
Ready for Remote Connections

*** [upload] Error 1

What could be the problem? Through the WCH Link Utility it is stitched without problems.
Chip ch32f008f4p6
VSCode 1.79.2
OS Windows_NT x64

Please file an issue in with the output of the project task → Advanced → Verbose Upload to keep this thread on-topic.

1 Like