はじめに
CH32V003マイコンのタイマーのPWM出力を使ってRCサーボモーターを制御してみます。
環境
- RCサーボモーター:MG90D(デジタルサーボ)
- マイコン:CH32V003F4P6(SSOP20ピン)
- IDE:MounRiver Studio Ⅱ(VSCodeベースの公式開発環境)
- SDK:公式SDK(MRS2に同梱)
配線
マイコンのPD2ピン(TIM1チャンネル1出力)にサーボモーターの信号線を接続します。
サーボ信号線の接続:
- 黄色(信号線)→PD2(19番ピン)
- 赤色(電源)→5V
- 茶色(GND)→GND

タイマーの設定
サーボモーターを制御するために必要な信号は、タイマー1(TIM1)のPWM出力を使用して生成します。
- カウント周波数:1 MHz
- マイクロ秒(us)単位でパルス幅を設定可能にするため
- マイコンの動作周波数(48 MHz)をプリスケーラー(分周器)で1/48に分周することで生成(実際の分周比はマイコンの動作周波数がセットされるグローバル変数
SystemCoreClock
を使って動的に計算)
- 周期:20 ms(20,000カウント)
- PWM信号の送信周期を50 Hzに
プログラム
まず、MounRiver Studio Ⅱでプロジェクトを作成します。(プロジェクト作成・設定方法は前記事にて解説)
再利用しやすいようにサーボ制御関数はmain.cとは別ファイルに切り出します。Userフォルダに次の2ファイル(servo.h, servo.c)を追加します。
#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"
/* サーボ初期化 */
void Servo_Init(void) {
/* GPIOとTIM1にクロック供給 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_TIM1, ENABLE);
/* PD2ピンをAF(Alternate Function=各種ペリフェラルによる出力)モードで初期化 */
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の設定 */
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; /* 分周比(-1に注意) */
TIM_TimeBaseInitStruct.TIM_Period = 20000; /* 周期 */
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
/* TIM1出力チャンネル1の設定 */
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; /* 出力モード */
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; /* 出力有効 */
TIM_OCInitStruct.TIM_Pulse = 0; /* 初期パルス幅 */
TIM_OC1Init(TIM1, &TIM_OCInitStruct);
/* TIM1の有効化 */
TIM_CtrlPWMOutputs(TIM1, ENABLE);
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM1, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
/* サーボ信号のパルス幅を設定(単位:us) */
void Servo_SetPulse(uint16_t us) {
TIM_SetCompare1(TIM1, us);
}
動作確認
テストプログラムを作成します。
#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); /* 中心 */
Delay_Ms(1000);
Servo_SetPulse(1000); /* 逆方向の最大(≒-45度) */
Delay_Ms(1000);
Servo_SetPulse(2000); /* 順方向の最大(≒+45度) */
Delay_Ms(1000);
Servo_SetPulse(1500); /* 中心 */
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);
}
}
}
最初に中心→-45度→+45度→中心と動かした後、現在地をprintfで出力しながら一方向に首振りをするプログラムになっています。
デバッグ機能を使わない場合はprintfをDelay_Ms関数に置き換える必要があります。
これを動作させた様子(前半のみ)が次のGIFです。

出力したPWM信号をオシロに入力して確認した様子を以下に示します。オシロの下の表示を見ると、きちんと50 Hz(20 ms)間隔で1.5msのパルス幅の信号が出力されていることが分かります。使っているオシロはAlientek DS100ミニデジタルオシロスコープで、こんな感じのマイコンの出力信号であれば手軽にデバッグできます。
.DQ0twJun.jpg)
まとめ
CH32V003マイコンを使って公式SDKでPWM出力を生成し、RCサーボモーターを制御してみました。
他の機能も色々試していきたいと思います。
参考サイト
開発ボードのドキュメントと共にサンプルコードが機能ごとに色々入っているので、公式SDKの使い方が分からなかったらこれを見るのが一番手っ取り早い。PWM出力のコードはEVT/EXAM/TIM/PWM_Output/User/main.c
を参考にした。