- GitHub sample sources for the CH32V003 firmware, here.
- GitHub sources for the patched version of WCHs OpenOCD, here.
Continuing our exploration of embedded CI jobs, let’s delve into achieving a comprehensive test-runner for a tiny embedded MCU. Instrumenting a WCH MCU that costs just $0.10, breaking free from the confines of a 16KB flash and 2KB RAM.
I’ve previously discussed this remarkable chip in a prior blog post. It’s a highly economical 32-bit MCU with a RISC-V2A core and a treasure trove of handy peripherals. WCH took an inventive approach to the debug interface, eschewing the resource-heavy JTAG in favour of a space-efficient single-wire debug interface. A mere trio of jumper cables (VCC, GND, SWDIO) are all you need to run and program this chip.

However, there are challenges. The non-standard interface needs its own set of drivers and code. While WCH has shown openness to open-source ideals, they’ve forked their own version of OpenOCD to cater to their interface nuances. You’ll find this bundled with MounRiver Studio or as a standalone toolchain. A polite inquiry to their support usually garners the source code swiftly.
As of my current writing, the MounRiver Studio 1.8 port regrettably lacks semihosting support. In response, I’ve revived it for their interface. My sources reside here: I forked
Karl Palssons’ well-maintained WCH OpenOCD sources and seamlessly merged in the pivotal commits from WCH’s divergence from the mainline RISCV OpenOCD. I’ve also submitted a patch WCHs’ way, hoping it’ll find its place in the forthcoming release. A pull request to Karls’s repo is a work in progress too.
What is Semihosting?
Semihosting, an ingenious mechanism, bridges the gap between a microcontroller or processor and its host environment through debug calls. These calls unlock a realm of possibilities:
- Debugging and Testing: Console input and status retrieval from the target.
- I/O Mastery: Orchestrating file exchanges within the host system.
- Code Crafting and Testing: When hardware drivers are still germinating in the early development phases, semihosting is an easy way to evaluate a target.
- Logging: Robust logging and tracing capabilities.
Tom Verbeure has a comprehensive writeup and practical examples for another RISCV core that I encourage anyone to read too. The genesis traces back to ARM’s pioneering steps, with a fortunate twist: RISCV embraced ARM’s interface blueprint. Consequently, OpenOCD developers found themselves tasked with seamlessly extending the same support to RISCV as they did for ARM.
The main drawback of semihosting lies in its speed. It operates at considerably reduced speeds compared to specialized hardware interfaces like UART. This deceleration originates from the debugger’s polling loop. A break state is triggered in the RISCV to signal a host call. Typically, the debugger polls the target execution state in intervals of roughly 100ms or less. On ARM this is even worse since some architectures need to interrupt to call the debugger. This limits you to around tens of host calls per second. Each host call carries an ID and buffer pointer, necessitating data extraction by the debugger after acknowledging the target request. Consequently, for tasks as simple as outputting a character, the process becomes sluggish. Optimal utilization entails conducting substantial bulk transfers with each call.
Another drawback is that semihosting code should steer clear of production deployment. Depending on the core configuration, the code could encounter hard faults (absence of trap handler or trap support) or linger indefinitely due to an “ebreak” state in the absence of a debugger connection. This makes semihosting a prime choice for prototyping, debugging, and testing endeavors.
If your focus narrows to just console I/O, alternative projects are emerging that employ RISCV DMI data transfer registers for console output, as shown by Charles Lohr’s work showcased here. Unfortunately, the tooling seems confined to CH32V003 and lacks extensive testing or applicability across WCH’s broader product lines. Moreover, adopting this avenue sacrifices the array of automation and test capabilities intrinsic to OpenOCD. Though ongoing development holds the promise of resolving these challenges, this remains brimming with potential.
Implementing Console Output
The WCH’s RISC-V toolchain supports the standard C library. This implies seamless integration of familiar C functions akin to your desktop experience. Yet, unlocking their utility necessitates the establishment of underlying bindings, showcased in the source found here.
Regrettably, the standard library confines printf to character-based IO. However, optimizing performance leads us to printf_, a variant empowering block transfers, thereby accelerating the pace, as shown in the main function.
Where to go from here
In my professional realm, this technique is a staple for continuous integration testing. I employ a pyexpect script to monitor OpenOCD’s console output, complemented by a loopback telnet session for target management. This is particularly useful for automated software testing. Enter Unity, a sleek C-powered test runner tailor-made to accommodate a spectrum of resource-limited systems. The process of porting this to your application is simple: tweak the read and write functions for semihosting, and assess pass-fail criteria through scripting within OpenOCD. Moreover, you can easily tweak my CH32V003 sample’s versatility to file-based I/O with the host system.