rpine lab Tech Blog

Tech blog covering a wide range of topics, including my hobby of programming (web and backend), setting up a home lab, and electronics projects using microcontrollers.

Driving an RC Servo Motor with the CH32V003 (PWM Output)

Driving an RC Servo Motor with the CH32V003 (PWM Output)
Table of contents
🤖
This article was AI-translated. Some nuances may differ from the original Japanese version.

Introduction

In this post I drive an RC servo motor using the PWM output of a timer on the CH32V003 microcontroller.

Environment

  • RC servo motor: MG90D (digital servo)
🌏
The product card above links to Japanese shops (Amazon Japan, Rakuten, Yahoo! Shopping). The TowerPro MG90D is a common part and is also sold on Amazon in other regions and on AliExpress.
  • Microcontroller: CH32V003F4P6 (SSOP-20)
  • IDE: MounRiver Studio Ⅱ (the official VSCode-based development environment)
  • SDK: official SDK (bundled with MRS2)

Wiring

Connect the servo signal wire to the PD2 pin of the microcontroller (TIM1 channel 1 output).

Servo wire connections:

  • Yellow (signal) → PD2 (pin 19)
  • Red (power) → 5 V
  • Brown (GND) → GND
CH32V003F4P6 pinout (from the CH32V003 datasheet)
CH32V003F4P6 pinout (from the CH32V003 datasheet)

Timer Configuration

The signal needed to control the servo is generated with the PWM output of Timer 1 (TIM1).

  • Count frequency: 1 MHz
    • So that the pulse width can be set in microseconds (us)
    • Generated by dividing the MCU clock (48 MHz) by 48 with the prescaler (the actual division ratio is computed dynamically from the global variable SystemCoreClock, which holds the MCU operating frequency)
  • Period: 20 ms (20,000 counts)
    • Sets the PWM signal repetition rate to 50 Hz

Program

First, create a project in MounRiver Studio Ⅱ. (How to create and configure the project is covered in the previous article.)

To make the servo control functions easy to reuse, I put them in separate files rather than in main.c. Add the following two files (servo.h, servo.c) to the User folder.

#ifndef __SERVO_H
#define __SERVO_H

#include "ch32v00x_conf.h"

#ifdef __cplusplus
 extern "C" {
#endif

void Servo_Init(void);
void Servo_SetPulse(uint16_t us);

#ifdef __cplusplus
}
#endif

#endif /* __SERVO_H */
#include "servo.h"

/* Servo initialization */
void Servo_Init(void) {
    /* Supply clock to GPIO and TIM1 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_TIM1, ENABLE);

    /* Initialize PD2 in AF (Alternate Function = output driven by a peripheral) mode */
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    /* TIM1 configuration */
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
    TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStruct.TIM_Prescaler = SystemCoreClock / 1000000 - 1; /* Division ratio (note the -1) */
    TIM_TimeBaseInitStruct.TIM_Period = 20000; /* Period */
    TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);

    /* TIM1 output channel 1 configuration */
    TIM_OCInitTypeDef TIM_OCInitStruct;
    TIM_OCStructInit(&TIM_OCInitStruct);
    TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; /* Output mode */
    TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; /* Enable output */
    TIM_OCInitStruct.TIM_Pulse = 0; /* Initial pulse width */
    TIM_OC1Init(TIM1, &TIM_OCInitStruct);

    /* Enable TIM1 */
    TIM_CtrlPWMOutputs(TIM1, ENABLE);
    TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM1, ENABLE);
    TIM_Cmd(TIM1, ENABLE);
}

/* Set the servo signal pulse width (unit: us) */
void Servo_SetPulse(uint16_t us) {
    TIM_SetCompare1(TIM1, us);
}

Testing

Write a test program.

#include "debug.h"
#include "servo.h"

int main(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    SystemCoreClockUpdate();
    Delay_Init();
#if (SDI_PRINT == SDI_PR_OPEN)
    SDI_Printf_Enable();
#else
    USART_Printf_Init(115200);
#endif

    Servo_Init();

    Servo_SetPulse(1500); /* Center */
    Delay_Ms(1000);
    Servo_SetPulse(1000); /* Maximum in the reverse direction (about -45 degrees) */
    Delay_Ms(1000);
    Servo_SetPulse(2000); /* Maximum in the forward direction (about +45 degrees) */
    Delay_Ms(1000);
    Servo_SetPulse(1500); /* Center */
    Delay_Ms(2000);

    while (1) {
        for (uint16_t i = 1000; i <= 2000; i+=100) {
            Servo_SetPulse(i);
            printf("Pulse: %d\n", i);
            // Delay_Ms(500);
        }
    }
}

The program first moves the servo center → -45 degrees → +45 degrees → center, then sweeps in one direction while printing the current position with printf.

If you are not using the debug feature, replace printf with the Delay_Ms function.

The following GIF shows it running (first half only).

Servo motor control in action
Servo motor control in action

Below is the PWM output signal fed into an oscilloscope. The readout at the bottom of the scope shows a 1.5 ms pulse being output at a clean 50 Hz (20 ms) interval. The scope I use is the Alientek DS100 mini digital oscilloscope, which makes it easy to debug MCU output signals like this one.

Output at center = 1500 us
Output at center = 1500 us

Summary

I generated a PWM output on the CH32V003 microcontroller with the official SDK and used it to control an RC servo motor.

I plan to try out the other peripherals as well.

References

CH32V003EVT.ZIP (WCH official evaluation board package)

It contains the development board documentation plus a wide range of sample code organized by feature, so if you are unsure how to use the official SDK, this is the quickest place to look. The PWM output code was based on EVT/EXAM/TIM/PWM_Output/User/main.c.

If you found this article helpful, please consider supporting me!