Skip to content

Releases: Community-PIO-CH32V/ArduinoCore-CH32H4

1.5.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 12 Sep 13:53

Arduino core for the WCH CH32H41x. Install through Boards Manager with the index URL in the README, or download ch32h4-1.5.0.zip for a manual install.

MP3 audio and web radio. A new MP3Audio library: Helix-based decoder, a player that feeds any AudioSink, and a WebRadio example that resolves what stations actually publish -- .m3u, .pls, .asx and Windows Media [Reference] files, chains of them, redirects that change scheme, and Shoutcast metadata.

Every part in the family. Variants for the CH32H417QEU6, MEU6 and WEU6, the CH32H416RDU6 and the CH32H415REU6, extracted from the datasheet's pin tables and cross-checked against two independent vendor sources.

A selectable CA trust store. None by default, so a sketch still names its own root; or 15 roots for about 15 KB of flash, or all 121 Mozilla roots for about 140 KB. Tools > CA trust store in the IDE. RAM is unchanged either way, because only the root matching the server is parsed.

Fixes. A pbuf reference leak that throttled every TCP receive to a few hundred bytes a second; a player deadlock against any audio sink smaller than one MP3 frame; half-read Shoutcast metadata blocks; and the trap where configuring TLS made a later http:// URL fail. Turning mbedTLS debug off by default returns 110 KB of flash.

1.4.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 06 Sep 16:29

A FAT filesystem on the chip's own flash — and on a USB stick

FatFS.begin();          // FAT on the internal flash partition
FatFSUSB.begin();       // ...and a PC sees it as a removable drive
SDFS.begin();           // the SD card, at the same time

Drag a file onto the drive from a PC and read it back from a sketch. There is
one copy of ChaN's FatFs now, built for two volumes, so the flash filesystem
and the SD card can be mounted together — which is what this release is for.

That differs from arduino-pico, whose FatFS is flash-only and whose SDFS
is a separate FAT implementation (SdFat) entirely. The library names match
upstream; the internals deliberately do not.

Verified on hardware: Windows enumerates the board as a 216 KB drive,
mounts it as a 198 KB FAT volume, and files copied on land in flash. Worst-case
stall inside a mass-storage write is 1.3 ms, so USB transfers absorb it.

New libraries

FatFS FAT on the internal flash, behind the same FS/File/Dir API as SDFS and LittleFS
FatFSUSB that volume presented to a host as a USB stick

SDFS and SD are unchanged for sketches — they simply became FatFs volume 1.

Before you use FatFS

A 256 KB minimum filesystem partition, from the Filesystem Size menu, or
board_build.filesystem_size = 256k. Below that the wear-levelling layer's
fixed reserve leaves under 90 KB, which is a legal but odd FAT12 volume and a
coin-toss on a Windows host. begin() refuses and names the setting.

It shares the partition with LittleFS, and only one filesystem can live
there. Flashing a FatFS sketch over a LittleFS one reformats it, and the
reverse is equally true — the sketch on the board decides which filesystem the
board has. If you want the flash readable from a PC, FAT is the one to pick; if
only sketches ever read it, LittleFS wears better and has no minimum size.

The sketch and a host must never both hold the volume. FatFs caches
directory and allocation sectors, so a host writing underneath that cache
corrupts one or both views silently. Watch FatFSUSB.hostChanged() and call
FatFS.end(); the USBDrive example shows the whole handover.

onPlug/onUnplug exist but are best-effort: they come from SCSI commands a
host is not obliged to send, and Windows sends neither — measured, mounting
and writing and ejecting with the callbacks silent throughout. hostChanged()
also goes true on the first host write, which no host can perform silently.
Only Windows has been tested; macOS and Linux are expected to be better
behaved, but nobody has checked.

Five bugs found porting the wear-levelling layer

All from upstream's flash translation layer writing 4096/512 as a literal
8 — right for the RP2040's 4096-byte erase blocks, wrong for this part's
8192, which hold sixteen 512-byte sectors. Four of them are silent:

  • The logical-to-physical map packed a 3-bit slot index, so LBA n aliased
    onto n+8: a boot sector appeared twice and a file's directory entry landed
    on the allocation table.
  • The per-block count of live sectors was a nibble, so a full 16-slot block
    counted 16, truncated to zero, and the allocator erased a block that still
    held data.
  • Garbage collection stopped filling a reclaimed block at 8 of 16 slots,
    freeing half the space it should and wearing the flash about twice as fast.
    Invisible to a correctness test; now 15 writes per erase against a
    theoretical 16.
  • gcScore() went negative for a block holding more than eight sectors,
    mis-ranking collection candidates.
  • Two loops had no bound — a corrupt block state or inconsistent free-space
    accounting spun forever, which on this part wedges the debug probe rather
    than merely hanging the sketch. Both now fail the write instead.

docs/hazards.md has the measurements. The regression tests are the point:
the aliasing was missed by the original tests because they used LBAs 0, 1, 17
and 100, and no two of those are eight apart.

1.3.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 05 Sep 20:12

Over-the-air updates

Flash a sketch over the network. The board appears in the Arduino IDE under
Tools → Port as a network port, and PlatformIO takes upload_protocol = espota. It speaks the espota protocol, so tools/espota.py works by hand too.

ArduinoOTA and Updater are ported from arduino-pico, keeping their LGPL
headers and attribution, and adapted only where the storage differs.
arduino-pico stages a new image on LittleFS and lets its bootloader apply it.
This part has no bootloader and no second slot — the sketch region is 912 KB of
a 960 KB part — so the image is held in RAM until it is complete and its MD5
checks out, and only then is it committed over the running sketch by a routine
that runs from ITCM because it erases the flash it is stored in.

Two consequences worth knowing before building on it:

  • An over-the-air image is capped by the heap, not the flash — around
    430 KB against the 912 KB a probe can write. ArduinoOTA.maxImageSize()
    reports what fits right now, and an oversized upload is refused at the invite
    rather than two thirds of the way through.
  • The commit is not power-fail safe. Between the first erase and the last
    program the sketch region is neither image. Verifying the MD5 before erasing
    keeps that window as small as it can be; it cannot close it.

The build now also produces firmware_ota.bin — the sketch half of the image,
without the V3F boot stub — and the Updater rejects a full firmware.bin sent
in its place, which is the mistake that would otherwise put the boot stub where
the sketch belongs.

Verified end to end on hardware, repeatedly: transfer, MD5, commit, reboot,
running the new firmware with the network back up.

Next-line branch prediction is off, and that is a bug fix

corecfgr bit 15 makes the V5F mispredict and resume execution at an address
it was never sent to
. It presented as a wild pointer inside lwIP on every
boot, and moved or vanished with any unrelated change — a compiler flag, an
added string, an enabled assert.

What it actually did: execution ran off from the middle of memp_malloc() into
the middle of memp_free(), 0xF8 further on, between two byte-identical
instructions — so lwIP's pool index was computed twice and the second result
indexed hundreds of kilobytes past the table into unprogrammed flash.

Measured three ways on one unchanged build:

I-cache NLP result
on on faults, deterministically
off on runs — NLP does nothing without the cache
on off runs, at full cached speed

The middle row is why this looks like a cache bug and is not one. The cache
stays on; the predictor does not. docs/hazards.md has the full account.

Other fixes

  • A crash guard that wedged the debug probe. When the V5F faulted
    repeatedly, the V3F printed "reflash, or reset twice to try again" and then
    spun in for (;;) {} — which wedges the WCH-Link, so the escape hatch needed
    the probe it had locked out. Now __WFI(), and the probe attaches to a
    fault-guarded board.
  • ltoa / utoa / ultoa, which the Arduino API requires a core to supply
    and newlib does not have. String(millis()) had never linked.
  • -mno-save-restore, so nothing in ITCM calls into flash. -msave-restore
    put __riscv_save_4 into the OTA committer's prologue, and GCC turned its
    deliberate word-at-a-time copy back into a memcpy — two calls into the
    flash being erased, neither of them in the source. tools/check_itcm.py now
    fails the build on any call out of ITCM.
  • Fault records now capture ra, a0 and a1. The handler is naked, so
    these are the faulting context's real values; they are what identified the
    predictor bug.
  • lwIP asserts and corruption detectors behind
    -DCH32H4_LWIP_ASSERT_CONSOLE, routed to the raw UART.

Since 1.2.0 also

mDNS with _arduino._tcp advertisement; multicast reception that works
(hash-table filter with the bit-reversed CRC the MAC actually wants); a single
ITCM flash driver instead of three copies of the register sequences; automatic
parking of the other core on any flash write; Keyboard, Mouse and
Joystick over USB HID; a CH32H4 library with a cross-core mutex;
LED_BUILTIN, LED1 and LED2; examples for every library that had none;
mbedTLS and lwIP as ordinary libraries, so board_build.tls and
board_build.network are gone.

1.2.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 05 Sep 10:23

HTTPS, served by the board

EthernetServerSecure and WebServerSecure — the same WebServer routes over
TLS. Mutual TLS too: requireClientCert(true) with a client CA, so the board
can refuse anyone without a certificate it trusts.

It is a build option, board_build.tls = mbedtls-server, because the server
half of mbedTLS is 45 KB of flash that a sketch fetching an HTTPS endpoint
never enters. The client-only board_build.tls = mbedtls is unchanged.

Verified on hardware against Python's ssl module — an implementation sharing
no code with mbedTLS — including the negative cases: a client trusting the
wrong CA is refused, a wrong hostname is refused, a client with no certificate
is refused under mutual TLS.

Link-time optimization, on by default

Worth 3–7% of flash: 2.2 KB off the smallest sketch, 24 KB off a TLS one.
board_build.lto = disabled turns it off, and the IDE has the same menu on the
same default.

Two bugs the new hardware tests found

HTTPClient truncated small responses to nothing. A body that arrived in its
own TCP segment just after the headers found the reader already gone: status
200, empty body. Large bodies were fine, which is what hid it.

WebServer leaked its client on every request. It deleted through a
Client*, and nothing in the Client/Stream/Print chain has a virtual
destructor, so no destructor ran at all.

Also

The Arduino IDE build gained a link-time-optimization menu, and TinyUSB's
buffers are now placed by section rather than by object filename — which is
what makes that placement mean the same thing under both build systems.
docs/hazards.md records what that investigation turned up: the belief that
this part's USB controller cannot reach DTCM, which this core was built around,
is not true.

382 tests: 109 host-side, 273 on hardware.

1.1.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 04 Sep 22:22

SPI and I2C slave modes, WebServer and HTTPClient, and a variant split.

SPI and I2C slave. SPISlave is a new library with arduino-pico's API. Wire gains the slave half of the standard Arduino API it already declared — begin(address), onReceive(), onRequest() — which until now were accepted and ignored. There is now a Wire1 as well, on I2C4. Interrupt dispatch for both peripherals lives in the core so the two SPI drivers and the two I2C roles contend through one place; a master-only SPI sketch links none of it.

Not yet verified on hardware: the test sketches and test files are present and skip cleanly without the bench wiring, which is not yet in place.

WebServer and HTTPClient, ported from arduino-pico to run over any Arduino Client, which here means Ethernet. Four things could not be carried across and are omitted rather than stubbed, because each would otherwise be a security claim the code does not honour — certificate-fingerprint pinning and the rest of the BearSSL-specific TLS surface. What remains is the ESP32 subset that mbedTLS backs. WebServerSecure is not in this release; it needs a TLS server, which is in progress.

The port also brought StreamString, MD5Builder, pgmspace, libb64 and http-parser into the core, and fixed two real defects found on the way: a client deleted through a base pointer with no virtual destructor, which leaked an entire mbedTLS context per request, and a clone() that the Arduino Client interface cannot provide.

Variants are now split into a chip package and a board. variants/CH32H417xx_QEU6 holds what the silicon decides — pin numbering, ADC pads, supply rails, the alternate-function tables, the memory layout — and variants/CH32H417QEU6 holds what this board wired, in 40 lines. A second board on the same chip copies those 40 lines rather than 1400. Verified that PlatformIO builds byte-for-byte identical firmware across the split.

Versions. package.json had sat at 0.1.0 since the core was scaffolded while the platform index reached 1.0.1 — two manifests for one artifact. Both are now 1.1.0, from one source.

Hardware suite: 216 passed, 29 skipped (the skips are the slave tests awaiting wiring).

1.0.1

Choose a tag to compare

@maxgerhardt maxgerhardt released this 04 Sep 16:08

Install this one, not 1.0.0.

Fixes three faults in the first release, all reported from real use on Windows.

  • Upload never worked from the Upload button. boards.txt declared only upload.tool; a current arduino-cli's pluggable upload wants upload.tool.default, so a plain upload failed with a programmer is required — surfaced by the IDE as request "upload" failed — while Upload Using Programmer worked, because that path reads program.tool.
  • wlink x64 cannot reach a WCH-LinkE on 64-bit Windows (USB error: incompatible driver is installed for this interface). Only the 32-bit build copes with both driver arrangements the probe appears under, which is what PlatformIO ships. Both Windows hosts now get win-x86.
  • No SVD. WCH publish one for this part (CH32H417 v1.3, 87 peripherals); the debugger's register view was empty without it. It is now in debug/ and named by the board.

1.0.0's assets were replaced several times while these were being fixed, so what that tag holds depends on when it was fetched. Releases are immutable from here; every platform change gets a new version.

1.0.0

Choose a tag to compare

@maxgerhardt maxgerhardt released this 04 Sep 11:57

First Boards Manager release of the CH32H41x Arduino core.

Add this URL under File > Preferences > Additional boards manager URLs, then install CH32H41x Boards:

https://raw.githubusercontent.com/Community-PIO-CH32V/ArduinoCore-CH32H4/main/package_ch32h4_index.json

The compiler, WCH's OpenOCD, wlink and — on Windows — a Python for the two prebuild hooks all install with it. Nothing needs to be on PATH beforehand. On Linux and macOS the hooks use python3 from PATH, as arduino-pico does.

Assets

  • ch32h4-1.0.0.zip — the platform, built from git ls-files --recurse-submodules so that all five submodules are present. GitHub's own source archives below contain none of them and will not build.
  • python3-3.11.9-embed-amd64-rooted.zip — python.org's Windows embeddable build, repacked under a single top-level directory because arduino-cli refuses an archive without one.

Verified by installing into a clean arduino-cli data directory and compiling with PATH cut down to system32 — no Python and no compiler available — then flashing the result, which boots at 400 MHz off HSE with USB up.