GUIXでモーターを動かしてみよう

組込みミドルウエア

今回は組込み評価ボードでDCモータを動かしてみようと思います。
動かす際の速度設定を、GUIXを使った画面で行ってみたいと思います。

企画検討

まず、作りたいものを検討します。
今回はDCモータを動かすことが目的なので、モータの速度、回転方向を画面から制御出来ればOKとします。
使用するモータドライバはPWM波形を入力する必要があるので、これにはSTM32F4の持つタイマ機能を使う事にします。

環境

今回使用した評価ボードは、STMicroelectronics社のSTM32F429 Discovery kitです。(結構古い基板ですがLCDも小さなものが付いていて、ちょっとしたことをするにはちょうど良いボードと思います。)
DCモータはRE-280RAを使いました。
モータドライバにはTB6612を使用したキットを使いました。
DCモータとモータドライバは、共に秋月電子通商社で取り扱いがありました。
ベースプロジェクトとして、以前弊社で行っていたハンズオンセミナのプロジェクトを流用します。
統合環境はIAR社のEWARMを使用しました。

画面設計

画面上からは、モータの速度(回転数)と方向を制御出来れば良いので以下の様な画面を作りました。

画面上部のボタンは、モータのON/OFFを行い、画面中央のスライダは左右で正転/逆転を行い、中央から離れるほど高速にするといった感じです。

スレッド設計

今回は、GUIXのアプリケーションとは別に、モータ制御の為のPWM設定用スレッドを作ることにします。

GUIXのイベントハンドラはGUIXのシステムスレッドから呼ばれるので、時間のかかる処理を行ってしまうと画面の応答が遅くなります。今回はタイマのPWM設定のみなのでイベントハンドラでも構いませんが、例えばI2Cの様な通信して制御するような場合、最悪タイムアウトエラーが起きるまで制御が戻らない可能性がありますし、ドライバによっては無限ループになってしまう可能性もあります。このようなことを避けるうえでもGUIXのイベントハンドラとは別のスレッドで制御することが望ましいと思います。

概要図にするとこんな感じです。

今回、2つのスレッド間はグローバル変数で情報伝達を行います。速度と回転方向だけなので、情報はイベントハンドラ⇒PWMスレッドの一方通行で済みます。より複雑な処理が必要な場合はOS機能のデータキューなどを使う事も良いと思います。
グローバル変数を使うので、PWMスレッドはグローバル変数をポーリングして、タイマに設定を行います。PWMスレッドの動作が遅いとタイマへの設定も遅くなり、モータの速度変化の反応も遅くなります。なのでPWMスレッドの優先度も重要になります。

PWMスレッド

PWMスレッドの目的はグローバル変数をポーリングして値を取得し、タイマに設定することです。

グローバル変数のポーリングはvolatile修飾子を付けておけば特別何もする必要もなく、普通に使用することで値を設定できます。(CPUキャッシュがあるようなCPUの場合はキャッシュ操作が必要な場合があります。)

今回はグローバル変数をタイマに設定した後、1msの間、スレッドを休止させています。

以下の様なコードで動作させました。(iSpd、iFgEnable、iFgDirはグローバル変数です。GUIX側から設定されます。)

void TM_TIMER_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_BaseStruct;
   
    /* Enable clock for TIM4 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    TIM_BaseStruct.TIM_Prescaler = 0;
    /* Count up */
    TIM_BaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_BaseStruct.TIM_Period = 4199; /* 20kHz PWM */
    TIM_BaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_BaseStruct.TIM_RepetitionCounter = 0;
    /* Initialize TIM4 */
    TIM_TimeBaseInit(TIM4, &TIM_BaseStruct);
    /* Start count on TIM4 */
    TIM_Cmd(TIM4, ENABLE);
}
 
void TM_PWM_Init(void) {
    TIM_OCInitTypeDef TIM_OCStruct;
   
    /* Common settings */
   
    /* PWM mode 2 = Clear on compare match */
    /* PWM mode 1 = Set on compare match */
    TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM2;
    TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_Low;
   
    TIM_OCStruct.TIM_Pulse = 1049; /* 25% duty cycle */
    TIM_OC1Init(TIM4, &TIM_OCStruct);
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
}
 
void TM_PWM_SET(int iFgMOut, int iFgMDir, int iFgMGo) {
    TIM_OCInitTypeDef TIM_OCStruct;
 
    if (iFgMOut) {
        GPIO_WriteBit(GPIOA, GPIO_Pin_5, Bit_SET);
        if (iFgMDir) {
            GPIO_WriteBit(GPIOA, GPIO_Pin_9, Bit_RESET);
            GPIO_WriteBit(GPIOA, GPIO_Pin_10, Bit_SET);
        } else {
            GPIO_WriteBit(GPIOA, GPIO_Pin_9, Bit_SET);
            GPIO_WriteBit(GPIOA, GPIO_Pin_10, Bit_RESET);
        }
    } else {
        GPIO_WriteBit(GPIOA, GPIO_Pin_5, Bit_RESET);
    }
    if (iFgMGo) {
        TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM2;
        TIM_OCStruct.TIM_OutputState = TIM_OutputState_Enable;
        TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_Low;
        TIM_OCStruct.TIM_Pulse = (iSpd * 3149) / 100;
        TIM_OC1Init(TIM4, &TIM_OCStruct);
        TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
    } else {
        TIM_OCStruct.TIM_OCMode = TIM_OCMode_PWM2;
        TIM_OCStruct.TIM_OutputState = TIM_OutputState_Disable;
        TIM_OCStruct.TIM_OCPolarity = TIM_OCPolarity_Low;
        TIM_OCStruct.TIM_Pulse = 0;    /* 0% duty cycle */
        TIM_OC1Init(TIM4, &TIM_OCStruct);
        TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
    }
}
 
VOID ctrl_thread_entry(ULONG thread_input)
{
    iFgEnable = iFgDir = 0;
 
    /* Init leds */
    TM_LEDS_Init();
    /* Init timer */
    TM_TIMER_Init();
    /* Init PWM */
    TM_PWM_Init();
 
    TM_PWM_SET(iFgEnable, iFgDir, 1);
 
    while (1) {
        TM_PWM_SET(iFgEnable, iFgDir, 1);
        tx_thread_sleep(1);
    }
}

GUIXアプリケーション

GUIXのアプリケーションコードは、イベントが発生するごとにGUIXのシステムスレッドから呼び出されます。
画面上のON/OFFボタンやスライダを動かすとイベントが発生してGUIXのアプリケーションコードが呼ばれるといった仕組みです。
なので、スライダの操作でモータの回転速度と方向を、ON/OFFボタンでモータのON/OFFをグローバル変数に設定します。グローバル変数に設定しておけばPWMスレッドが動作した時にタイマへの設定を更新することが出来ます。
GUIXアプリケーションコードは以下の様にしました。

void    UpdateEnableDisable(WNDMAIN_CONTROL_BLOCK *win, int iFg)
{
    if (!iFg) {
        iFgEnable = 0;
        gx_text_button_text_id_set(&win->wndMain_btnEnable,
                                   GX_STRING_ID_STRING_1);
    } else {
        iFgEnable = 1;
        gx_text_button_text_id_set(&win->wndMain_btnEnable,
                                   GX_STRING_ID_STRING_2);
    }
}
 
UINT main_window_handler(GX_WINDOW *window, GX_EVENT *myevent)
{
    int                     lValue;
    UINT                    uiRet=0;
    WNDMAIN_CONTROL_BLOCK   *win = (WNDMAIN_CONTROL_BLOCK *)window;
 
    switch (myevent->gx_event_type) {
        case GX_EVENT_SHOW:
            gx_window_event_process(window, myevent);
            UpdateEnableDisable(win, 0);
            break;
 
        case GX_SIGNAL(ID_BTN_ENA, GX_EVENT_CLICKED):
            if (iFgEnable) {
                UpdateEnableDisable(win, 0);
            } else {
                UpdateEnableDisable(win, 1);
            }
            break;
 
        case GX_SIGNAL(ID_SLD_SPD, GX_EVENT_SLIDER_VALUE):
            lValue = myevent->gx_event_payload.gx_event_longdata;
            if ((lValue <= 100) && (lValue > 0)) {
                iFgDir = 0;
                iSpd = lValue;
            } else if ((lValue >= -100) && (lValue < 0)) {
                iFgDir = 1;
                iSpd = lValue * -1;
            } else {
                iFgDir = 0;
                iSpd = 0;
            }
            break;
 
        default:
            uiRet = gx_window_event_process(window, myevent);
            break;
    }
    return uiRet;
}

結線表

評価ボードとモータドライバの結線は以下の様にしてみました。

動作確認

実際に結線して組み立ててみました。
モータドライバには別途ACアダプタで3Vを供給しています。

ボタンを押してモータ制御を有効にして、スライダを右側に動かすと正転に回転し、左側に動かすと逆転します。

まとめ

如何でしたでしょうか。
モータ制御は弊社があまり引き受けていない分野ですが、GUIXを使用した制御として試してみました。
今回はモータ制御しか行っていないですし、タイマを使ったのでスレッドを分けなくても同様の制御は出来ると思いますが、他のスレッドなどが同時に動作するような環境の場合は、スレッドを分けたほうがUIの反応が良くなります。