A Little About Debugging on CH32V00x and SDI Print

I usually use UART TX logging to debug my code. However, sometimes this method is unavailable: the hardware UART might be occupied by other peripherals, or the TX line simply wasn’t routed during the PCB layout because it seemed unnecessary. But you still need a way to debug the firmware.

For the CH32V series microcontrollers, there is an excellent alternative to UART - the SDI Print (Serial Debug Interface) mechanism. It allows you to send log messages over the single programming wire and read them on your PC.

At the hardware level, two 32-bit registers are used for data transmission:

#define DEBUG_DATA0_ADDRESS  ((volatile uint32_t*)0xE00000F4)
#define DEBUG_DATA1_ADDRESS  ((volatile uint32_t*)0xE00000F8)

The way you interact with these registers differs drastically depending on the framework you choose: the official one from WCH (noneos-sdk) or the minimalist ch32fun.

In this guide, I will describe how to set up logging specifically for the official WCH framework.

In the application code, instead of the standard UART initialization, you need to enable SDI Print by calling the SDI_Printf_Enable() function:

#include <ch32v00x.h>
#include <debug.h>

void NMI_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void HardFault_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

int main(void) {
    SystemCoreClockUpdate();
    Delay_Init();
    SDI_Printf_Enable();

    uint16_t counter = 0;
    while (1) {
        printf("Hello from printf! %d\n", counter);
        counter++;
        Delay_Ms(1000);
    }
    return 0;
}

void NMI_Handler(void) {}
void HardFault_Handler(void)
{
    while (1)
    {
    }
}

But merely calling the function in your code isn’t enough. For the standard printf to start redirecting data to the debug registers instead of UART, you need to properly configure the build using preprocessor macros:

  • DEBUG=0 — disables standard logging via UART.

  • SDI_PRINT=SDI_PR_OPEN — enables the data “packing” algorithm into the DATA0 and DATA1 registers (you can peek under the hood to see how this works in the framework’s debug.c file, specifically in the _write() function).

Since I use PlatformIO, the most convenient way to set these flags is directly in the platformio.ini configuration file:

...
build_flags = -DDEBUG=0 -DSDI_PRINT=SDI_PR_OPEN

After that, compile and flash the firmware to the microcontroller.

I would try running minichlink -T, which is supposed to launch a terminal and display the output. However, with firmware that uses the official WCH framework, this didn’t work, and the terminal remained blank. So, we put minichlink aside — this tool is primarily tailored for interacting with firmwares built on ch32fun.

Instead, we will use a another utility — wlink. It can be installed using the cargo package manager (if you have Rust installed):

cargo install wlink

After experimenting with wlink, I found the necessary command to activate our debugging:

$ wlink sdi-print enable
19:15:14 [INFO] Connected to WCH-Link v2.21(v41) (WCH-LinkE-CH32V305)
19:15:14 [INFO] Attached chip: CH32V003 [CH32V003A4M6] (ChipID: 0x00320510)
19:15:14 [INFO] Enabling SDI print
19:15:14 [INFO] Now you can connect to the WCH-Link serial port

And here is where it gets really interesting! The WCH-LinkE programmer has a built-in USB-UART adapter, which I use quite often. The executed command instructs the programmer to intercept data from the SDI line and redirect it at the hardware level to its own virtual COM port.

Now, if we open /dev/ttyACM0 (or the corresponding port on your OS) in the PlatformIO Serial Monitor or any other terminal, we will finally see our output:

--- Terminal on /dev/ttyACM0 | 9600 8-N-1
--- Available filters and text transformations: debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
Hello from printf! 0
Hello from printf! 1
Hello from printf! 2
Hello from printf! 3

Important detail: You need to switch the WCH-LinkE into SDI Print mode (using the wlink sdi-print enable command) every time after flashing the microcontroller or reconnecting the USB, as the programmer resets this state.

P.S. An important note on saving memory

Avoid using printf unless absolutely necessary — it’s an extremely “heavy” function that eats up quite a bit of precious Flash memory (and the CH32V003 only has 16 KB of it). Since it drags along the entire format parsing mechanism under the hood (%d, %s, %x, etc.), calling it will cost you a few kilobytes.

Here is a clear example. If you use the standard printf:

    char *msg = "Hello from printf!\n";
    uint8_t msg_len = strlen(msg);
    printf(msg);

The compiler will show the following memory usage:

Flash: [===       ]  27.6% (used 4520 bytes from 16384 bytes)

But if you only need to output plain text messages for debugging without variable formatting, it is better to call the low-level _write function directly:

    char *msg = "Hello from printf!\n";
    uint8_t msg_len = strlen(msg);
    _write(0, msg, msg_len);

In this case, resource consumption drops significantly:

Flash: [=         ]   8.9% (used 1456 bytes from 16384 bytes)

A difference of 3 kilobytes for a microcontroller of this class is a massive amount of space.

Comments

Popular posts from this blog

YD-RP2040 Module

How to Add CH32V Support to PlatformIO

Orange Pi Zero 3 - exploring GPIO