ESP32-WROOM-32Eでサーボモーターを操作する

組み込みエンジニア

こんにちは、ENGかぴです。

ESP32-WROOM-32Eはサーボモーターを操作する標準ライブラリが実装されていません。ESP32用のライブラリを追加しWiFi通信を使ってブラウザーからサーボモーターを操作して動作確認を行いました。

サーボモーターはArduinoのスターターキットに付属されていたSG90を使用しています。アクセスポイントにはWZR-HP-G300NH(バッファロー:生産中止)を使用しています。ESP32-WROOM-32Eで動作確認したことについてリンクをまとめています。

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

ライブラリを追加してサーボモーターを操作する

SG90の仕様説明
引用:SG90のデータシート SG90の仕様説明

サーボモーターはパルス波形を与えてモータを動作させるものです。SG90のデータシートによるとPWM周期(キャリア周波数)が50Hzの波形でデューティーサイクル(Duty Cycle)が0.5~2.4msになるように調整することで回転角が決まります。

データシートにはOperating speed(動作速度)が0.12s/60 degreeと記載されています。60度回転させる場合はキャリア周波数20msでデューティーサイクルが1.13mであるパルス波形を120ms経過するまで出力する必要があります。

データシートのOperating voltage(動作電圧)は3.3V~6Vとなっています。パルス波形の例では4.8V~5Vとなっていますが3.3Vのパルスを与えても動作させることができます。

サーボモーターの電源電圧が3.3Vの場合で5Vのパルスを与えるのは入力電圧が電源電圧を超えることになるため注意が必要です。

ESP32シリーズはArduino環境に対応していますがPWM波形の生成方法が異なっているため標準関数でサーボモーターを操作することができません。別途ライブラリを追加する必要があります。

以下はESP32用のサーボモーターのライブラリを追加してサーボモーターの操作を行う手順を説明します。

すき間時間で資格をゲット【STUDYing(スタディング)】

ESP32用のライブラリを追加する

ESP32Servoライブラリの追加
ESP32Servoライブラリの追加

ESP32ServoライブラリをArduino IDEで追加します。Arduino IDEのスケッチ欄からライブラリをインクルードを選択するとライブラリを管理の項目が表示されるのでクリックしてライブラリマネージャに遷移します。

ライブラリマネージャの検索欄にesp32servo(大文字小文字の区別はない)を入力すると候補としてServoに関するライブラリが表示されます。「ESP32Servo」を選択してインストールします。

360度回転するサーボの場合は「ESPServo360」ライブラリを選択するのもありですがSG90は180度回転なのでESP32Servoライブラリを選択しています。

ライブラリの準備と初期化

#include <ESP32Servo.h>

Servo sg90; //Servoクラスの型の変数を宣言

void setup() {
  sg90.attach(SERVO_PIN,510,2400); //サーボモーターの初期化
}

ライブラリを使用するためESP32Servo.hをインクルードします。サーボモーター専用のクラスの型の変数としてsg90(任意の宣言名でOK)を宣言してオブジェクトを作成します。

attach()関数でサーボモーターの初期化を行います。第1引数にサーボモーターを操作するDO出力のピンを指定します。第2引数にサーボモーターの操作の最小値(usの値)を指定します。第3引数にサーボモーターの操作の最大値(usの値)を指定します。

第2引数と第3引数を指定しない場合はそれぞれ最小値が544・最大値が2400となります。SG90のデータシートではデューティーサイクルが0.5ms~2.4msとなっているので例では第2引数に510・第3引数に2400を指定しています。

最小値をデータシートのとおり500(0.5ms)にすると回転角がギリギリな状態になりジリジリ音とを伴って過回転しようとするためデフォルトもしくはギリギリの回転角にならないように調整が必要です。

すき間時間で資格をゲット【STUDYing(スタディング)】

ライブラリの使用例

#define WAIT_TIM 50 //500msウェイト

void loop() {

  switch (move){
  case 0: //回転指令
    if(timcount == TIME_UP ){
      sg90.write(60); //回転角を指定
      timcount = WAIT_TIM; //回転角を指定するまでの待機時間
      move = 1;
    }
    break;
  case 1:
    if(timcount == TIME_UP ){
      sg90.write(120); //回転角を指定
      timcount = WAIT_TIM;
      move = 2;
    }
    break;
}

サーボモーターの回転角の指定をモード管理で行います。回転角の指定の待機を管理するタイマ(timcount)がタイムアップすると回転角の指定を行いtimcountにウェイトさせる時間を指定します。これを1つのセットとして回転パターンを追加します。

サーボモーターの回転角をwrite()関数で指定します。引数に操作したい角度を指定します。例では60度(モード0)・120度(モード1)を指定しています。

回転角を指定した後はtimcountにウェイト時間をセットしモードを1に遷移させてタイムアップまで待機します。以降は追加したパターン分同様の処理を行います。

SG90のデータシートによるとOperating speed(動作速度)が0.12s/60 degreeなので60度回転させる場合は120ms以上のウェイトを置く必要があります。例では500msのタイマを置いています。

回転角を指定した後に500ms毎にウェイトを置きましたが、サーボモーターが回転角に達するまでの時間以上であれば任意で調整することができます。

回転角に達する前に次の回転角を指定すると回転角の指定が上書きされるため動作が不安定になるため注意が必要です。

Webサーバーを使用する

#include <WebServer.h>

void setup() {
  Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
  Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数を指定
  Wserver.onNotFound(handleNotFound); //URLが存在しない場合の関数を指定
  Wserver.begin(); //Webサーバーの開始
}
void loop(){
  Wserver.handleClient();
}

Webサーバーライブラリの使い方は下記記事まとめています。ここでは要点のみまとめています。

ESP32-WROOM-32EでWebServerを実装する

WebServerライブラリを使用するためWebServer.hをインクルードします。on()関数を使用するとブラウザーからアクセスしたときのURLで表示するページを分岐することができます。

on()関数は第1引数にURLアドレスの階層を示す文字、第2引数にHTTPの種類を指定、第3引数に接続要求の処理を行う関数を指定します。

ブラウザーからのリクエストでページを表示する場合はHTTP_GETを使用します。表示したページから設定項目などのデータを受け取る場合はHTTP_POSTを使用します。

onNotFound()関数はURLが存在しない時に表示する処理を行う関数を指定します。

handleClient()関数でクライアントからのリクエストを常時監視します。クライアントからリクエストがあれば登録したURLに対応する処理が呼び出されます。

void HtmlPost(void){

  selRatio = Wserver.arg("selRatio"); //selRatioの値を取得
  sel = selRatio.toInt(); //整数に変換

  if( sel == 0 ){
    rnd1_s = Wserver.arg("rnd1_s");
    rnd1 = rnd1_s.toInt();
  }
  else{
    ptn_s = Wserver.arg("ptn_s");
    ptn = ptn_s.toInt();
  }
}

HtmlPost()は表示したページからデータを取得すると遷移します。Webサーバーからデータを取得するためにarg()関数を使用します。arg()関数の引数はHTMLデータから取得する要素の名前を指定します。

例ではselRatioを取得しています。selRatioはHTMLで設置しているラジオボタンの任意のグループ名でありブラウザー上で変更するとvalue値が更新されます。arg()関数でvalueの値を取得しています。

arg()関数で取得した値はHTMLデータであるため文字列になります。文字列の数値をtoInt()関数で整数値に変換してパターンの切り替えや回転角の指定に使用します。

動作確認

動作確認用の回路図
動作確認用の回路図

サーボモーターの電源は外部の電源からDC5Vを供給しています。サーボモーターのようにモータ系の負荷になると消費電流が大きいためマイコンのDO出力では過負荷になるためです。外部電源とArduinoのGNDは同一系統にするため接続します。

マイコンのDOの出力電流は最大でも10mA程度であるため過負荷の状態が続くとマイコンが発熱し故障する可能性があるため注意が必要です。

WiFi通信を使ってブラウザー上でSG90を操作します。スマホなどのブラウザーからWiFiのアクセスポイントに接続します。IPはアクセスポイントとESP32-WROOM-32Eと同じ系列に設定します。

例えばアクセスポイントのIPを「192.168.11.1」に設定した場合はESP32-WROOM-32EのIPを「192.168.11.20」などに固定します。

ESP32-WROOM-32EのIP設定をDHCPによる自動割り付けにしている場合は接続後に自分のIPを把握する必要があるためシリアルモニタに表示するなど工夫が必要です。今回はIPを固定しているので指定したIPで接続するとブラウザー上で表示できます。

ブラウザーでの動作確認
ブラウザーでの動作確認

項目の任意を選択して回転1~回転3まで任意の回転角を指定することができます。モード選択ではモード1~モード3までのパターンを選択することができます。

モード1は0度(-90度)→90度(0度)→180度(90度)を繰り返します。モード2は45度(-45度)→90度(0度)→135度(45度)を繰り返します。モード3は60度(-20度)→120度(30度)→60度(-20度)を繰り返します。

「通信開始」を押すとパターンを反映してSG90の動作のパターンが変更できます。

スポンサーリンク

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESP32Servo.h>

#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define SERVO_PIN 5
#define WAIT_TIM 50 //500msウェイト

const char *ssid = "001D73912B41"; //SSID
const char *pass = "123456789abcdef"; //password
const IPAddress ip(192,168,11,20); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク

WebServer Wserver(80);
uint16_t waitcnt;
int8_t cnt10ms;
int8_t timcount;
uint32_t beforetimCnt = millis();
String selRatio;
uint8_t sel;
String rnd1_s; String rnd2_s; String rnd3_s;
uint8_t rnd1; uint8_t rnd2; uint8_t rnd3;
String ptn_s;
uint8_t ptn;
Servo sg90;
uint8_t mode;
uint8_t angle;

void mainApp(void);
void handleNotFound(void);
void HtmlSet(void);
void HtmlPost(void);

void setup() {

  Serial.begin(115200);
  WiFi.mode(WIFI_STA); //ステーションモード
  WiFi.begin(ssid,pass);
  WiFi.config(ip,ip,subnet);

  while( WiFi.status() != WL_CONNECTED ){
    delay(1000);
    Serial.print(".");
    if( ++waitcnt >= 60 ){
       esp_restart();
    }
    Serial.print(waitcnt);
  }

  Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数
  Wserver.on("/", HTTP_POST, HtmlPost); //URLを指定して処理する関数
  Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数
  Wserver.begin(); //Webサーバーの開始
  selRatio = "1";
  sg90.attach(SERVO_PIN, 510, 2400);
}

void loop() {

  mainTimer();
  mainApp();
  Wserver.handleClient();
}
/* メイン処理関数 */
void mainApp(void){

  switch (mode){
  case 0: //回転指令
    if(timcount == TIME_UP ){
      if(sel == 1){ //モードの角度
        switch (ptn)
        {
        case 1:
          angle = 0;
          break;
        case 2:
          angle = 45;
          break;
        case 3:
          angle = 60;
          break;   
        default:
          angle = 0;
          break;
        }
      }
      else{
        angle = rnd1;
      }

      sg90.write(angle); //回転角度を指定
      timcount = WAIT_TIM; //回転角を指定するまでの待機時間
      mode = 1;
    }
    break;
  case 1:
    if(timcount == TIME_UP ){
      if(sel == 1){ //モードの角度
        switch (ptn)
        {
        case 1:
          angle = 90;
          break;
        case 2:
          angle = 90;
          break;
        case 3:
          angle = 120;
          break;   
        default:
          angle = 0;
          break;
        }
      }
      else{
        angle = rnd2;
      }

      sg90.write(angle); //回転角度を指定
      timcount = WAIT_TIM;
      mode = 2;
    }
    break;
  case 2: //回転指令
    if(timcount == TIME_UP ){
      if(sel == 1){ //モードの角度
        switch (ptn)
        {
        case 1:
          angle = 180;
          break;
        case 2:
          angle = 135;
          break;
        case 3:
          angle = 60;
          break;   
        default:
          angle = 0;
          break;
        }
      }
      else{
        angle = rnd3;
      }

      sg90.write(angle); //回転角度を指定
      timcount = WAIT_TIM;
      mode = 0;
    }
    break;
  }
}
/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();

    if( timcount > TIME_UP ){
      timcount--;
    }
  }
}
/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
  String str = "";

  str += "<html lang=\"ja\">";
  str += "<head>";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>SG90の動作確認</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>SG90の動作確認</h1>";
  str += "<form method='post'>";
  str += "<label>サーボモーターの回転角を指定します。</label>";
  str += "<br>";
  str += "<table border='1pt'  style='background:lightyellow;'>";
  str += "<tr><th>項目</th><th>内容</th></tr>";
  str += "<td><input type='radio' name='selRatio' value='0' checked> 任意 ";
  str += "</td>";  
  str += "<td>任意の回転角を指定します。<br>";
  str += "回転1:  ";
  str += "<input type='text' name='rnd1_s' value='" + rnd1_s + "' placeholder='0' style ='width:50px;font-size:20px;'>";
  str += "回転2:  ";
  str += "<input type='text' name='rnd2_s' value='" + rnd2_s + "' placeholder='90' style ='width:50px;font-size:20px;'>";
  str += "回転3:  ";
  str += "<input type='text' name='rnd3_s' value='" + rnd3_s + "' placeholder='180' style ='width:50px;font-size:20px;'>";
  str += "</td>"; 
  str += "<tr><td><input type='radio' name='selRatio' value='1' checked> モード選択";
  str += "<td>回転パターンを選択します。<br>";
  str += "<input type='radio' name='ptn_s' value='1' checked> モード1 ";
  str += "<input type='radio' name='ptn_s' value='2' > モード2   ";
  str += "<input type='radio' name='ptn_s' value='3' > モード3   ";
  str += "</td></tr>";
  str += "</table>";
  str += "<br><br>";
  str += "<input type='submit' value='通信開始'";
  str += "style='padding: 20px; min-width: 300px;border-radius: 5px;";
  str += "font-family: inherit; background: lightgreen; font-size: 2rem;'>";
  str += "</form>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}

/* 送信を押したときに遷移する */
void HtmlPost(void){
  String str="";

  selRatio = Wserver.arg("selRatio");
  sel = selRatio.toInt();
  Serial.print("sel:");
  Serial.println(sel);

  if( sel == 0 ){
    rnd1_s = Wserver.arg("rnd1_s");
    rnd2_s = Wserver.arg("rnd2_s");
    rnd3_s = Wserver.arg("rnd3_s");
    rnd1 = rnd1_s.toInt();
    rnd2 = rnd2_s.toInt();
    rnd3 = rnd3_s.toInt();

    if(rnd1 >180){
      rnd1 = 180;
    }

    if(rnd2 >180){
      rnd2 = 180;
    }

    if(rnd3 >180){
      rnd3 = 180;
    }
  }
  else{
    ptn_s = Wserver.arg("ptn_s");
    ptn = ptn_s.toInt();
    Serial.print("ptn:");
    Serial.println(ptn);
  }

  str += "<html lang=\"ja\">";
  str += "<head>";
  str += "<meta http-equiv='refresh' content='1;'>";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>Sensor graph</title>";
  str += "</head>";
  str += "<body>";
  str += "<h1>パターンを変更中</h1>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200, "text/html", str);
}

/* URLが存在しない場合の処理 */
void handleNotFound(void) {

  String message = "File Not Found\n\n";
  message += "URI: ";
  message += Wserver.uri();
  message += "\nMethod: ";
  message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += Wserver.args();
  message += "\n";

  for (uint8_t i = 0; i < Wserver.args(); i++) {
    message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
  }
  Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}

WiFiのアクセスポイントのSSIDとパスワードは使用しているアクセスポイントの条件に置き換えて下さい。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

TECH::CAMPプログラミング教養【無料体験会】

最後まで、読んでいただきありがとうございました。

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