CH32V003 C Program Example Reference

CH32V003 C Example Program Reference

CH32V003 C Program Example Reference

The CH32V003 is an ultra-low-cost 32-bit RISC-V microcontroller (QingKe V2A core) from WCH, operating at up to 48 MHz with 2 KB SRAM and 16 KB flash. Programming is typically performed using WCH's MounRiver Studio IDE or open-source toolchains (e.g., PlatformIO with openwch/ch32v003 SDK or ch32v003fun). The examples below use the standard WCH Non-OS SDK style (C with peripheral libraries: ch32v00x.h, debug.h, etc.), which is the most common approach for this device.

All code assumes:

  • Inclusion of startup code and linker script from the official SDK (e.g., from https://github.com/openwch/ch32v003).
  • debug_init() for UART printf (usually USART1 on PD5/PD6 at 115200 baud).
  • Compilation with riscv-none-elf-gcc or equivalent toolchain.

1. Drive a GPIO pin high/low at 1 ms intervals

This example toggles PC0 (common LED pin on many boards) every 1 ms using a busy-wait delay.

#include "ch32v00x.h"
#include "debug.h"

void Delay_Ms(uint32_t n)
{
    uint32_t i;
    while(n--)
    {
        i = 48000;          // Approx 1 ms @ 48 MHz
        while(i--);
    }
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure = {0};
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    while(1)
    {
        GPIO_SetBits(GPIOC, GPIO_Pin_0);
        Delay_Ms(1);
        GPIO_ResetBits(GPIOC, GPIO_Pin_0);
        Delay_Ms(1);
    }
}

Note: Delay_Init() and Delay_Ms() are approximate; for better precision use TIM1 or SysTick.

2. Read ADC 20 times per second and append to a file

The CH32V003 has a 12-bit SAR ADC with up to 8 channels (AIN0–AIN7). No built-in filesystem exists (no SD card or flash FAT in basic SDK). This example reads ADC channel 0 (e.g., PA0) at 20 Hz and prints values over UART (viewable in a serial terminal). File appending requires external storage and a library (not included here).

#include "ch32v00x.h"
#include "debug.h"

void ADC_Config(void)
{
    ADC_InitTypeDef ADC_InitStructure = {0};
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div8);  // ADC clock ≤ 14 MHz

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    ADC_DeInit(ADC1);
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

uint16_t ADC_Read(uint8_t channel)
{
    ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_241Cycles);
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
    return ADC_GetConversionValue(ADC1);
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    ADC_Config();

    while(1)
    {
        uint16_t val = ADC_Read(ADC_Channel_0);  // PA0 / AIN0
        printf("ADC: %u\n", val);
        Delay_Ms(50);  // 20 Hz
    }
}

3. PWM example with 5 different duty cycles

The CH32V003 has TIM1 (advanced timer) with up to 4 PWM channels + complementary outputs. This example uses TIM1_CH1 on PC3 (~1 kHz PWM) and cycles through approximate 0%, 25%, 50%, 75%, 100% duty.

#include "ch32v00x.h"
#include "debug.h"

void TIM1_PWM_Out_Init(uint16_t arr, uint16_t psc, uint16_t ccp)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    TIM_OCInitTypeDef TIM_OCInitStructure = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure = {0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_TIM1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    TIM_TimeBaseInitStructure.TIM_Period = arr;
    TIM_TimeBaseInitStructure.TIM_Prescaler = psc;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStructure);

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = ccp;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM1, &TIM_OCInitStructure);

    TIM_CtrlPWMOutputs(TIM1, ENABLE);
    TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Disable);
    TIM_ARRPreloadConfig(TIM1, ENABLE);
    TIM_Cmd(TIM1, ENABLE);
}

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    // ~1 kHz PWM @ 48 MHz: ARR=47999, PSC=0 → period = 48000 cycles
    TIM1_PWM_Out_Init(47999, 0, 0);

    uint16_t duties[] = {0, 12000, 24000, 36000, 47999};  // ≈0%,25%,50%,75%,100%

    while(1)
    {
        for(uint8_t i = 0; i < 5; i++)
        {
            TIM_SetCompare1(TIM1, duties[i]);
            printf("Duty: %u / 47999\n", duties[i]);
            Delay_Ms(2000);
        }
    }
}

4. Output to serial port (UART)

The CH32V003 has USART1 (PD5 TX, PD6 RX). This example prints a counter over UART.

#include "ch32v00x.h"
#include "debug.h"

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);  // Uses debug.h routine (USART1 @ PD5/PD6)

    uint32_t counter = 0;

    while(1)
    {
        printf("Counter: %lu\n", counter++);
        Delay_Ms(1000);
    }
}

Note: No native USB device support exists on CH32V003 (unlike some other WCH parts).

5. Clocks on the CH32V003 and how to read them

The CH32V003 clock tree includes:

  • HSI — Internal 24 MHz RC oscillator (default after reset)
  • HSE — External 4–25 MHz crystal (commonly 24 MHz for PLL ×2 → 48 MHz max SYSCLK)
  • PLL — Multiplies HSE or HSI by 2 (max 48 MHz output)
  • SYSCLK — System clock (HSI, HSE, or PLL; max 48 MHz)
  • HCLK — AHB clock (SYSCLK divided by 1–256)
  • PCLK1/PCLK2 — APB clocks (HCLK divided)
  • ADCCLK — ADC clock (PCLK2 divided by 2/4/6/8; max 14 MHz)
  • LSI — Internal ~128 kHz RC (for IWDG)
  • MCO — Clock output pin (can output SYSCLK, HSI, HSE, PLL/2)

Reading is done via RCC registers:

  • RCC->CFGR0 — Shows PLL source, prescalers, clock status flags
  • RCC->CTLR — HSI/HSE/PLL enable and ready bits
  • SystemCoreClock variable (updated by SystemCoreClockUpdate())

Example program printing clock status:

#include "ch32v00x.h"
#include "debug.h"

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    while(1)
    {
        printf("SYSCLK: %lu Hz\n", SystemCoreClock);

        uint32_t cfgr = RCC->CFGR0;
        printf("PLLSRC: %s  PLLMUL: %s  HPRE: %lu\n",
               (cfgr & RCC_PLLSRC) ? "HSE" : "HSI/2",
               (cfgr & RCC_PLLMULL) ? "x2" : "x1",
               1 << ((cfgr & RCC_HPRE) >> 4));

        printf("HSI_RDY: %d  HSE_RDY: %d  PLL_RDY: %d\n",
               (RCC->CTLR & RCC_HSI_RDY) ? 1 : 0,
               (RCC->CTLR & RCC_HSE_RDY) ? 1 : 0,
               (RCC->CTLR & RCC_PLL_RDY) ? 1 : 0);

        Delay_Ms(2000);
    }
}

Typical defaults (no HSE/PLL configured): SYSCLK ≈ 24 MHz (HSI). With 24 MHz HSE and PLL enabled: SYSCLK = 48 MHz.

These examples should compile with the official WCH SDK or PlatformIO ports. For production use, consider calibration of HSI/PLL if precision timing is required. If you need variants using ch32v003fun, Rust, or Zig, please specify.

Linux Rocks Every Day