CH32V003 C Program Example Reference
CH32V003 C Example Program 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 flagsRCC->CTLR— HSI/HSE/PLL enable and ready bitsSystemCoreClockvariable (updated bySystemCoreClockUpdate())
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.