ESP32マウス(shota)マウス自作研修

ESP32マウスPart.34 MCPWMでモータを回す

ESP32マウス(shota)

こんにちは、shotaです。

前回の記事ではESP-IDFのsdkconfig設定方法について書きました。

今回はMCPWM機能を使ってモータを回します。

ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples

初めてのオリジナルマウス Especial

回した結果がこちらです。

今回は記事が長いので最初に結果を貼ります。モータが回ってますね!!!
この記事を読めばESP32でモータを回せるようになりますよ!!!

プログラムを書く前の下準備

さて今回もプログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。

モータドライバ回路の回路図を確認

まずはじめに、Especialの回路図を確認します。

↓Especialの回路図はこちらです。今回必要なところは、モータドライバ回路です。
especial回路図.pdf

↓モータドライバ回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修16[回路設計④ モータドライバ・エンコーダ回路]

Especial – モータドライバ回路

Especial – ESP32の接続先

モータドライバ回路はMOT_SLEEP、MOT_R{L}_PH、MOT_R{L}_ENでモータドライバIC DRV8838を制御します。

制御ロジックは次の表のとおりです。
nSLEEPピンで制御をON/OFFします。
PHピンで正転・逆転を切り替えます。
ENピンでモータ回転数(モータにかかる電圧)を調整します。

RV8838 Device Logic
http://www.tij.co.jp/jp/lit/ds/symlink/drv8838.pdf

このことより、MOT_SLEEPにはGPIO機能を割り当ててON/OFFさせる、
MOT_R{L}_PHにもGPIO機能を割り当ててON/OFFさせる、
MOT_R{L}_ENにはPWM機能を割り当てて0 ~ 100%で出力を可変できれば良さそうです。

(実際には、MOT_R{L}_PHは、GPIOではなくMCPWM機能でON/OFFします)

ピンの接続先は次のとおりです。

  • MOT_SLEEPはIO33
  • MOT_R_PHはIO25
  • MOT_R_ENはIO26
  • MOT_L_PHはIO16
  • MOT_L_ENはIO17
  • これで必要な情報は揃いました。

    あれ?どのピンでPWM機能が使えるか調べなくていいの?

    実は、ESP32ではほとんどのピンでPWM機能が使えるのです。

    まずいつものようにESP32モジュールのデータシートを見てみましょう。

    ESP32-WROOM-32 Pin Definitions

    PWM機能の割り当ては特に明記されていないですね。

    次に、ESP32モジュールの中に入っている、チップ(ESP32-D0WDQ6)のデータシートを見てみましょう。

    ESP32-D0WDQ6 – Peripheral Pin Configurations

    Motor PWMの割り当てはAny GPIO Pinsと書かれています。
    GPIOピンならどのピンでもPWMが使えるのです。便利ですね〜〜〜。

    他にもAny GPIO Pinsな機能はいくつかあるので、眺めてみると楽しいですよ。

    プログラムの作成

    それではプログラムを作成します。

    参考にしたサンプルコードはexamples/peripherals/mcpwm/mcpwm_brushed_dc_controlです。

    esp-idf/examples/peripherals/mcpwm/mcpwm_brushed_dc_control at v3.3.1 · espressif/esp-idf
    Espressif IoT Development Framework. Official development framework for Espressif SoCs. - espressif/esp-idf

    作成したプログラムがこちらです。
    長くなったので、GitHubに公開しているコードの閲覧をおすすめします。

    especial/examples/4_motor_driver/main/main.c at master · ShotaAk/especial
    ESP32を搭載したマイクロマウスのプログラム. Contribute to ShotaAk/especial development by creating an account on GitHub.

    また、MCPWMの公式マニュアルも参照してください。

    Motor Control Pulse Width Modulator (MCPWM) - ESP32 - — ESP-IDF Programming Guide v5.4 documentation
    #include <stdio.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "driver/gpio.h"
    #include "driver/mcpwm.h"
    
    // 公式マニュアル:
    // Ref: https://docs.espressif.com/projects/esp-idf/en/stable/api-reference/peripherals/mcpwm.html
    
    static const gpio_num_t GPIO_NSLEEP = GPIO_NUM_33;
    
    enum SIDE{
        LEFT = 0,
        RIGHT,
        SIDE_SIZE
    };
    
    static const mcpwm_unit_t UNIT = MCPWM_UNIT_0;
    static const gpio_num_t GPIO_PH[SIDE_SIZE] = {
        GPIO_NUM_16, GPIO_NUM_25};
    static const gpio_num_t GPIO_EN[SIDE_SIZE] = {
        GPIO_NUM_17, GPIO_NUM_26};
    
    static const mcpwm_io_signals_t SIGNAL_PH[SIDE_SIZE] = {
        MCPWM0A, MCPWM1A};
    static const mcpwm_io_signals_t SIGNAL_EN[SIDE_SIZE] = {
        MCPWM0B, MCPWM1B};
    
    static const mcpwm_timer_t TIMER[SIDE_SIZE] = {
        MCPWM_TIMER_0, MCPWM_TIMER_1};
    
    static const mcpwm_operator_t OPR_PH = MCPWM_OPR_A;
    static const mcpwm_operator_t OPR_EN = MCPWM_OPR_B;
    
    static const mcpwm_duty_type_t DUTY_MODE = MCPWM_DUTY_MODE_0; // アクティブハイ
    
    
    static void drive_forward(mcpwm_timer_t timer_num , float duty)
    {
        mcpwm_set_signal_low(UNIT, timer_num, OPR_PH);
        mcpwm_set_duty(UNIT, timer_num, OPR_EN, duty);
        // set_signal_low/highを実行した後は、毎回set_duty_typeを実行すること
        mcpwm_set_duty_type(UNIT, timer_num, OPR_EN, DUTY_MODE);
    }
    
    static void drive_backward(mcpwm_timer_t timer_num , float duty)
    {
        mcpwm_set_signal_high(UNIT, timer_num, OPR_PH);
        mcpwm_set_duty(UNIT, timer_num, OPR_EN, duty);
        // set_signal_low/highを実行した後は、毎回set_duty_typeを実行すること
        mcpwm_set_duty_type(UNIT, timer_num, OPR_EN, DUTY_MODE); 
    }
    
    static void drive_brake(mcpwm_timer_t timer_num)
    {
        mcpwm_set_signal_low(UNIT, timer_num, OPR_EN);
    }
    
    static void motor_drive(const enum SIDE side, const float duty, const int go_back)
    {
        const float DUTY_MAX = 100;
        const float DUTY_MIN = 5;
    
        float target_duty = duty;
        if(target_duty > DUTY_MAX){
            target_duty = DUTY_MAX;
        }
    
        if(target_duty < DUTY_MIN){
            drive_brake(TIMER[side]);
        }else{
            if(go_back){
                drive_backward(TIMER[side], duty);
            }else{
                drive_forward(TIMER[side], duty);
            }
        }
    }
    
    void app_main()
    {
        // ----- GPIOの設定 -----
        gpio_config_t io_conf;
        io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
        io_conf.mode = GPIO_MODE_OUTPUT;
        io_conf.pin_bit_mask = (1ULL<<GPIO_NSLEEP);
        io_conf.pull_down_en = 0;
        io_conf.pull_up_en = 0;
        gpio_config(&io_conf);
    
        // nSLEEPをONにして、モータの電源OFF
        gpio_set_level(GPIO_NSLEEP, 0);
    
        // ----- MCPWMの設定 -----
        // GPIOの割り当て
        mcpwm_gpio_init(UNIT, SIGNAL_PH[LEFT],  GPIO_PH[LEFT]);
        mcpwm_gpio_init(UNIT, SIGNAL_EN[LEFT],  GPIO_EN[LEFT]);
        mcpwm_gpio_init(UNIT, SIGNAL_PH[RIGHT], GPIO_PH[RIGHT]);
        mcpwm_gpio_init(UNIT, SIGNAL_EN[RIGHT], GPIO_EN[RIGHT]);
    
        // MCPWMの詳細設定
        // frequencyの値によりdutyの分解能が変わるので注意すること
        // frequency = 10kHz -> 1%刻みのduty
        // frequency = 100kHz -> 10%刻みのduty
        mcpwm_config_t pwm_config;
        pwm_config.frequency = 10*1000; // PWM周波数= 10kHz,
        pwm_config.cmpr_a = 0; // デューティサイクルの初期値(0%)
        pwm_config.cmpr_b = 0; // デューティサイクルの初期値(0%)
        pwm_config.counter_mode = MCPWM_UP_COUNTER;
        pwm_config.duty_mode = DUTY_MODE; // アクティブハイ
    
        mcpwm_init(UNIT, TIMER[LEFT],  &pwm_config);
        mcpwm_init(UNIT, TIMER[RIGHT], &pwm_config);
    
        const int WAIT_TIME_MS = 100;
        const int DUTY_STEP = 1;
        const int DUTY_MAX = 50;
        const int DUTY_MIN = 0;
    
        // モータON
        gpio_set_level(GPIO_NSLEEP, 1);
        while (1) {
            for(int go_back=0; go_back<=1; go_back++){
                // 加速
                for(int duty=DUTY_MIN; duty<DUTY_MAX; duty+=DUTY_STEP){
                    motor_drive(LEFT, duty, go_back);
                    motor_drive(RIGHT, duty, go_back);
                    vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
                }
            
                // 減速
                for(int duty=DUTY_MAX; duty>=DUTY_MIN; duty-=DUTY_STEP){
                    motor_drive(LEFT, duty, go_back);
                    motor_drive(RIGHT, duty, go_back);
                    vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
                }
            }
        }
    }
    

    それでは、必要なところを解説します。

    プログラムの解説

    まず、GPIOの設定で、nSLEEPピン(IO33)を出力にしています。
    GPIOはこれまで何回も登場したので、説明は省略します。

        // ----- GPIOの設定 -----
        gpio_config_t io_conf;
        io_conf.intr_type = GPIO_PIN_INTR_DISABLE;
        io_conf.mode = GPIO_MODE_OUTPUT;
        io_conf.pin_bit_mask = (1ULL<<GPIO_NSLEEP);
        io_conf.pull_down_en = 0;
        io_conf.pull_up_en = 0;
        gpio_config(&io_conf);
    
        // nSLEEPをONにして、モータの電源OFF
        gpio_set_level(GPIO_NSLEEP, 0);
    

    次にMCPWMの設定です。
    まず左右のモータ用のPH、ENピンのGPIOを割り当てます。
    ピンの割り当てにはmcpwm_gpio_init関数を使います。
    どのUNIT(0~1)の、どの出力(0A~2A, 0B~2B)にピンを割り当てるのか設定します。

        // ----- MCPWMの設定 -----
        // GPIOの割り当て
        mcpwm_gpio_init(UNIT, SIGNAL_PH[LEFT],  GPIO_PH[LEFT]);
        mcpwm_gpio_init(UNIT, SIGNAL_EN[LEFT],  GPIO_EN[LEFT]);
        mcpwm_gpio_init(UNIT, SIGNAL_PH[RIGHT], GPIO_PH[RIGHT]);
        mcpwm_gpio_init(UNIT, SIGNAL_EN[RIGHT], GPIO_EN[RIGHT]);
    

    次にMCPWMの詳細設定です。
    mcpwm_init関数を使って、UNITにTIMER(0~2)とpwm_configを設定します。
    pwm_configには、PWM周波数やカウンタ、デューティモード等を設定します。

        // MCPWMの詳細設定
        mcpwm_config_t pwm_config;
        pwm_config.frequency = 10*1000; // PWM周波数= 10kHz,
        pwm_config.cmpr_a = 0; // デューティサイクルの初期値(0%)
        pwm_config.cmpr_b = 0; // デューティサイクルの初期値(0%)
        pwm_config.counter_mode = MCPWM_UP_COUNTER;
        pwm_config.duty_mode = DUTY_MODE; // アクティブハイ
    
        mcpwm_init(UNIT, TIMER[LEFT],  &pwm_config);
        mcpwm_init(UNIT, TIMER[RIGHT], &pwm_config);
    

    これで設定が終わりました。
    app_main関数には加速と減速を繰り返すコードを書いています。
    まず、GPIO_NSLEEPの出力を1にして、モータドライバを稼動状態にします。
    その後、自作のmotor_drive関数を使って、
    正転・逆転の切り替えと、dutyの増減を繰り返します。

        const int WAIT_TIME_MS = 100;
        const int DUTY_STEP = 1;
        const int DUTY_MAX = 50;
        const int DUTY_MIN = 0;
     
        // モータON
        gpio_set_level(GPIO_NSLEEP, 1);
        while (1) {
            for(int go_back=0; go_back<=1; go_back++){
                // 加速
                for(int duty=DUTY_MIN; duty<DUTY_MAX; duty+=DUTY_STEP){
                    motor_drive(LEFT, duty, go_back);
                    motor_drive(RIGHT, duty, go_back);
                    vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
                }
             
                // 減速
                for(int duty=DUTY_MAX; duty>=DUTY_MIN; duty-=DUTY_STEP){
                    motor_drive(LEFT, duty, go_back);
                    motor_drive(RIGHT, duty, go_back);
                    vTaskDelay(WAIT_TIME_MS / portTICK_RATE_MS);
                }
            }
        }
    

    自作のmotor_drive関数は次のように書いています。
    引数sideは、左右どちらのモータを動かすかの変数です。
    dutyはPWM信号のデューティ比です。
    go_backは正転・逆転のフラグです。0で正転します。

    mcpwmの関数には0 ~ 100[%]のdutyを渡します。
    DUTY_MAX、DUTY_MINでdutyの値を制限していますが、最小値は0ではなく5にしています。
    dutyを3とか4にしてモータを回してみましたが、力が弱く、マウスが進みませんでした。
    なので、dutyが5より小さいときはブレーキにしています。

    static void motor_drive(const enum SIDE side, const float duty, const int go_back)
    {
        const float DUTY_MAX = 100;
        const float DUTY_MIN = 5;
    
        float target_duty = duty;
        if(target_duty > DUTY_MAX){
            target_duty = DUTY_MAX;
        }
    
        if(target_duty < DUTY_MIN){
            drive_brake(TIMER[side]);
        }else{
            if(go_back){
                drive_backward(TIMER[side], duty);
            }else{
                drive_forward(TIMER[side], duty);
            }
        }
    }
    

    最後は自作関数drive_forwarddrive_backwarddrive_brakeの説明です。

    drive_forward関数は、デューティ比duty%のPWM信号をモータドライバに出力し、モータを正転させます。
    まず、mcpwm_set_signal_low関数で、PHピンをlowにします。これはGPIOのset_lowと同じです。
    次にmcpwm_set_duty関数でENピンのdutyをセットします。
    最後に、mcwpm_set_duty_typeでENピンのDUTY_MODE(今回はアクティブハイ)をセットします。

    set_signal_low/high関数を使った後は、再びDUTY_MODEをセットしなければなりません。これは仕様です
    今回、ENピンはdive_brake関数でset_lowされるので、set_duty_type関数の実行が必須です。

    drive_backward関数は、PHピンをhighにしてるだけで、forward関数とほとんど同じです。

    drive_brake関数はENピンをlowにしてモータをブレーキします。

    static void drive_forward(mcpwm_timer_t timer_num , float duty)
    {
        mcpwm_set_signal_low(UNIT, timer_num, OPR_PH);
        mcpwm_set_duty(UNIT, timer_num, OPR_EN, duty);
        // set_signal_low/highを実行した後は、毎回set_duty_typeを実行すること
        mcpwm_set_duty_type(UNIT, timer_num, OPR_EN, DUTY_MODE);
    }
    
    static void drive_backward(mcpwm_timer_t timer_num , float duty)
    {
        mcpwm_set_signal_high(UNIT, timer_num, OPR_PH);
        mcpwm_set_duty(UNIT, timer_num, OPR_EN, duty);
        // set_signal_low/highを実行した後は、毎回set_duty_typeを実行すること
        mcpwm_set_duty_type(UNIT, timer_num, OPR_EN, DUTY_MODE); 
    }
    
    static void drive_brake(mcpwm_timer_t timer_num)
    {
        mcpwm_set_signal_low(UNIT, timer_num, OPR_EN);
    }
    

    動かした結果がこちらです(2回目)

    それでは再び動画を見てみましょう。
    コードを見てから動画を見ると、より理解が深まると思います。

    こちらはおまけの動画です。

    次回の記事

    プログラムで1箇所気になるところがありますね。

        // MCPWMの詳細設定
        // frequencyの値によりdutyの分解能が変わるので注意すること
        // frequency = 10kHz -> 1%刻みのduty
        // frequency = 100kHz -> 10%刻みのduty
    

    次回の記事は、PWM周波数とduty比の関係について書きます。

    タイトルとURLをコピーしました