こんにちは、shotaです。
前回の記事ではESP-IDFのsdkconfig設定方法について書きました。
今回はMCPWM機能を使ってモータを回します。
ブロクで書いたサンプルコードはGitHubに公開しています。
https://github.com/ShotaAk/especial/tree/master/examples
回した結果がこちらです。
今回は記事が長いので最初に結果を貼ります。モータが回ってますね!!!
この記事を読めばESP32でモータを回せるようになりますよ!!!
プログラムを書く前の下準備
さて今回もプログラムを書く前に、Especialの回路図やESP32モジュールのデータシートを読み、情報を集めます。
モータドライバ回路の回路図を確認
まずはじめに、Especialの回路図を確認します。
↓Especialの回路図はこちらです。今回必要なところは、モータドライバ回路です。
especial回路図.pdf
↓モータドライバ回路の設計ブログ記事はこちらです
shotaのマイクロマウス研修16[回路設計④ モータドライバ・エンコーダ回路]
モータドライバ回路はMOT_SLEEP、MOT_R{L}_PH、MOT_R{L}_ENでモータドライバIC DRV8838を制御します。
制御ロジックは次の表のとおりです。
nSLEEPピンで制御をON/OFFします。
PHピンで正転・逆転を切り替えます。
ENピンでモータ回転数(モータにかかる電圧)を調整します。
このことより、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します)
ピンの接続先は次のとおりです。
これで必要な情報は揃いました。
あれ?どのピンでPWM機能が使えるか調べなくていいの?
実は、ESP32ではほとんどのピンでPWM機能が使えるのです。
まずいつものようにESP32モジュールのデータシートを見てみましょう。
PWM機能の割り当ては特に明記されていないですね。
次に、ESP32モジュールの中に入っている、チップ(ESP32-D0WDQ6)のデータシートを見てみましょう。
Motor PWMの割り当てはAny GPIO Pinsと書かれています。
GPIOピンならどのピンでもPWMが使えるのです。便利ですね〜〜〜。
他にもAny GPIO Pinsな機能はいくつかあるので、眺めてみると楽しいですよ。
プログラムの作成
それではプログラムを作成します。
参考にしたサンプルコードはexamples/peripherals/mcpwm/mcpwm_brushed_dc_controlです。
作成したプログラムがこちらです。
長くなったので、GitHubに公開しているコードの閲覧をおすすめします。
また、MCPWMの公式マニュアルも参照してください。
#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_forward、drive_backward、drive_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比の関係について書きます。