ESP32は上海の会社espressif systemsが開発したマイコンです。製造もTSMCで完全中国製マイコンになります。
ESP32とは?
Wifi、Bluetooth(Classic + BLE)が使えて、32bitのデュアルコア(XTensa)で最大動作240MHzとハイスペックなマイコンです。Arduino IDEにも対応していて、プログラム開発も簡単にできます。
しかも、UNOよりも小型でしかも価格も安い。同じespressif社が販売している開発ボード ESP32-DevKitCなら1500円ぐらいで手に入ります。UNOが3000円ぐらいなので、およそ半額ですね。
はやくて、安い…なんか牛丼屋みたい。
唯一、UNOの方が書籍やインターネットで情報が手に入りやすいというみたいなメリットはありますが、NXPのARMマイコンで開発している自分にとっては、情報が少ない?は?十分あるじゃんって感じなレベルなので、調べれば何かしら答えが見つかります。
今回やること
はじめてということで、とりあえずチュートリアルとして良さそうな題材を探しました。
ESP32は、海外のサイトの方が情報が豊富で、開発の参考になるのがこの辺でしょうか。この記事もこの二つの記事を参考にしています。
RANDOM NERD TUTORIALS
DRONEBOT WORKSHOP
手持ちでサーボ―モータがあったのと、ESP32といえば、無線機能ということで、WIFIをつかって、Webサーバからサーボモータを駆動するようなサンプルをしてみたいと思います。
基本、サンプルコードベースですが、ちょっと工夫して、オリジナリティを出していきたいと思います。
で、最終的に出来ること
こんな感じで、タブレット端末からポケモンが無線で動かせます。
ちょっとオリジナリティの出す方向性を間違えてますが、作業をざっくりまとめていきます。
使用した開発ボード
マイコンの開発元であるESPRESSIFが販売している開発ボード ESP32-DevKitCを使いました。お値段は1000円前後。
ちなみにESP32というと、いったい何を指しているのかわからなくなる時があります。というのは、写真のような開発ボード(今回つかうのは、ESP32-DevKitC)を指すのか、無線機能とマイコンをセットにしたモジュール(ESP32-DevKitCでは、ESP-WROOM-32)のことか、または、コントローラ自体のことなのか(ESP-WROOM-32では、ESP32-D0WDQ6)、それぞれに種類があって、混乱するからです。
この記事ではESP32というと開発ボードを含めたシステム全体を指すようにします。
ちなみに余談ですが、WIFIとかBluetoothとかの電波をつかう機器を日本で使うには、混信を防ぐために国が許可したものしか使ってはいけません。一般的に技適というやつです。
ESP32でも使用しているモジュールによっては、技適がないものもあるので、注意が必要です。
見分け方ですが、許可されたものはこんなマークがついてるはずです。
もちろんESP32-WROOM-32には技適マークがついてます。
こんな感じに。
使用するサーボモータ
ホビー用途でよくつかわれてるSG90というのを使います。上のプロペラみたいのが180deg回転します。同じサイズで、360deg回るのもありますね。
電源は5Vで、黄色の線にPWM信号を入れて、モータを制御します。茶色はグランド。
ESP32をArduino IDEで使うための設定
espressif社が開発しているESP-IDFというのがオフィシャルな開発環境とおもわれますが、とりあえず、手っ取り早く動かすにはArduino IDEを使うのがベストです。
ESP32をArduino IDEで使うには、ボードマネージャでライブラリをまずインストールする必要があります。
Arduino IDEを起動して、ファイルから環境設定をクリックします。
下の追加のボードマネージャのURLの箇所に下のリンクをコピペします。
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
次に、下のツールバーからボードマネージャをクリックします。
下のテキストボックスにesp32と打つと、ライブラリがリストアップされるので、最新バージョン(この記事を書いたときは1.04が最新)をインストールします。
ツールからボードの設定をESP32 ArduinoのWrover Moduleにします。
IDE側の設定は以上です。ESPをPCに繋ぎます。デバイスマネージャを開いてみて、下図のようにデバイスがうまく認識されていない場合は、
CP210xのドライバのインストールが必要です。ESP32はUSB-UARTブリッジとしてCP2102Nというチップ利用していて、VCPというドライバをいれると、COMとして認識されるようになります。
下のサイトからダウンロードできます。
うまくいくと下のようにCOMとして、認識されます。
Arduino IDEでビルドするときは、忘れずにポート番号の設定をここで認識した番号で設定しましょう。
Lチカで動作確認
とりあえず、Lチカで動作を確認します。どこでもいいですが、下のpinoutを確認して、今回は#18pinのGPIOにLEDをぶら下げることにします。
スケッチはArduinoと全く同じ関数が使えます。
// LED on GPIO18
int ledPin = 18;
void setup()
{
// #18ピンをデジタル出力に設定
pinMode(ledPin, OUTPUT);
// シリアルモニタ設定(ボーレートは115200bps)
Serial.begin(115200);
}
void loop()
{
Serial.println("LED ON");
digitalWrite(ledPin, HIGH);
delay(500);
Serial.println(" LED OFF");
digitalWrite(ledPin, LOW);
delay(500);
}
動作はこんな感じです。
WEBサーバ経由でLチカ
これもESP32のイントロでよく扱われていますが、トレースしていきます。WIFIを使う時には、下の二通りの方法があります。
・Wifiルータをアクセスポイントとして、ESP32をぶらさげる
・ESP32自体をアクセスポイントとして機能させる。
後者はインターネットにつなぐ必要がない場合にローカル環境で、ESP32とスマホとかPCとかと連動させるときに使えますが、今回はネット経由で制御したいので、前者の方法でいきます。一般的には、これをStation (STA)モードといいます。
STAのサンプルコードを下のように開きます。サンプル名がSimpleWifiServerというやつです。
このプログラムを実際に動作させるには、コード内で使用するwifiのSSIDとパスワードを設定しておく必要があります。
スケッチの30-31行目あたりのこれです。yourssidとyourpasswdを自分のWifiルータの環境に合わせて設定します。
const char* ssid = "yourssid";
const char* password = "yourpasswd";
自分の場合は、#18にLEDをつないでいるので、ピン番号の設定も下のようにかえてます。
pinMode(18, OUTPUT); //setup関数内の38行目 set the LED pin mode
if (currentLine.endsWith("GET /H")) { //メインループの105行目あたり
digitalWrite(18, HIGH);
}
if (currentLine.endsWith("GET /L")) {
digitalWrite(18, LOW); // GET /L turns the LED off
}
プログラムを実行して、シリアルモニタを確認するとESP32のIPアドレスが表示されるので、http://xxx.xxx.xxx.xxxとして、webブラウザからアクセスすると下のような感じLEDのON/OFFを制御できるようになります。
サンプルコードの中身をちょっとみる
サンプルコードを見ていくと、ESP32がWEBサーバになって、スマホなどからのリクエストがあった場合にホームページの情報を返すようになってます。
いわゆるhttp通信というやつですね。
実行して、スマホでESP32に接続した時のモニター結果がこれです。
2~3行目にある GET /HTTP/1.1とHost:192.168.3.32 (ESP32のIPアドレス)が実際にブラウザからESP32に送られたリクエストメッセージになります。
内容はHTTPバージョン1.1のルールに則り、指定したIPのウェブページをGETしたいんだけど、どうかな?というものです。
次にそれを受けたESP32側は、下のコードでもって、ブラウザにこう返してます。
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println();
ステータス200 で問題ありません。OKです。html形式で情報を今から送りますよ、と。
Arduino IDEでは、client.prinlnというメソッドをつかえば、HTTPリスポンスをこうやって、実現できます。その後、改行を一回して、次に続きます。
client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 5 on.<br>");
client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 5 off.<br>");
実際、スマホから見るWEBページの正体はこれです。client.printを除けば、htmlで作成されたホームページと変わりません。
実際、中身だけコピーすると、
See the Pen
LED by Michinori Takeuuchi (@michi1982)
on CodePen.
こんな感じで、普通にhtmlファイルとして、動作していることが分かります。つまり、EPS32がhtmlファイルをブラウザに送信する箇所をカスタマイズしていけば、自分の思い通りのページを作成することが出来るわけです。
もちろん内容によっては、html, CSS, java scriptなどの言語を知っとかないと作れませんが、その手の情報は副業ブームのせいか、無限にあるので、興味がある方は勉強してみてもいいかもしれません。
凝ったことをしなければ、そんな難しいものではないです。
一応、別記事で超基本的なものをまとめてみましたので、参考にしてください。
サーボモータを動かす。
さて、LEDはWebサーバ経由で制御が何となくできたので、次はサーボモータを動作させます。
サーボモータ用のライブラリがESP32用に公開されているので、それをまず、Arduino IDEにインストールするところから始めます。
ツールからライブラリを管理をクリック。
下のようにESP32servoと検索するとライブラリがリストされるので、これをインストールします。
サーボモータを動かすサンプルを実行します。ESP32Servoフォルダ内のsweepを開きましょう。
サンプルコードを動かす為に、下図のようにESP32とサーボモータをつなぎます。
サンプルコードは下のとおりです。
#include <ESP32Servo.h> //ESP32サーボモータのライブラリ
Servo myservo; //Servoクラスのオブジェクト化
int pos = 0;
int servoPin = 18; //PWM信号を出力するピン
void setup() {
// Allow allocation of all timers
ESP32PWM::allocateTimer(0); //タイマをPWM信号に使用する
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
myservo.setPeriodHertz(50); //PWM周波数を50Hzにする
myservo.attach(servoPin, 1000, 2000); //#18をPWM信号ピンに設定
}
void loop() {
for (pos = 0; pos <= 180; pos += 1) {
// in steps of 1 degree
myservo.write(pos);
delay(15);
}
for (pos = 180; pos >= 0; pos -= 1) {
myservo.write(pos);
delay(15);
}
}
プログラムの動作はサーボモータを0deg~180deg間で15msec周期に1degずつ変化させていっています。サーボモータは#18pinから出力されているPWM信号で制御されています。
PWM信号で重要なのが、制御周期とDUTYですが、それはこのライブラリでは下のように定義しています。
myservo.setPeriodHertz(50); //PWM周波数を50Hzにする
myservo.attach(servoPin, 1000, 2000);
//第1引数にpwm信号を出力するピン番号を指定、
//第2と第3引数に0degと180degのパルス幅をいれる(usec)
そして、writeメソッドに目標の角度をわたすことで、裏側では、attachしたパルス幅でpwm制御をしてくれるわけです。
myservo.write(pos);
ただ、パルス幅の設定は、default値だと、180degまで稼働しないので、修正が必要です。
WEBサーバを作成する
サーボモータは動きましたので、webサーバ用のページをhtmlで作成します。詳細は別記事まとめていますので、ここでは、こんなの作ったよ程度の内容です。
See the Pen
ESP32_WebServo by Michinori Takeuuchi (@michi1982)
on CodePen.
Ajaxっていう通信方法で、ユーザがブラウザで操作した値をESP32が拾っています。ここら辺の手法が理解できれば、ESP32で取得したセンサをブラウザでみるのも今回みたいにアクチュエータをブラウザ経由で動かすみたいなことも簡単にできます。
んで、この作ったhtmlファイルをLEDのときみたいに
client.println("<!DOCTYPE html>");
client.println("<html lang=\"ja\">");//以降、ずーっとつづく
に変換して、スケッチにコピペすれば、上のデモみたいなページがESP32で作れます。
が、これ結構面倒です。たとえば二行目とかはhtml内でダブルクオーテーションとかつかってますが、print文にするときは、エスケープ文字に変換しないといけません。
簡単に変換マクロをつくりましたので、使ってください。(すいませんがmacは動作未確認)
Html2ArduinoWebServer
動かなかったらすみませんが、簡単ですので、自分で作ってください。
最終的なソース
は、以下のようになります。サーボモータのサンプルとLチカのサンプルを組み合わせています。client.printlnがガーってなってるところは上のツールでhtmlから変換したソースをコピペしている箇所になります。
#include <WiFi.h>
#include <ESP32Servo.h>
Servo myservo; // create servo object to control a servo
// Servo GPIO pin
static const int servoPin = 18;
// Network credentials
const char* ssid = "10B1F88E5DB9-2G";
const char* password = "5530117932294";
// Web server on port 80 (http)
WiFiServer server(80);
// Variable to store the HTTP request
String header;
// Decode HTTP GET value
String valueString = String(5);
int pos1 = 0;
int pos2 = 0;
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
void setup() {
// Allow allocation of all timers for servo library
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
// Set servo PWM frequency to 50Hz
myservo.setPeriodHertz(50);
// Attach to servo and define minimum and maximum positions
// Modify as required
myservo.attach(servoPin,650, 2600);
// Start serial
Serial.begin(115200);
// Connect to Wi-Fi network with SSID and password
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Print local IP address and start web server
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
server.begin();
}
void loop(){
// Listen for incoming clients
WiFiClient client = server.available();
// Client Connected
if (client) {
// Set timer references
currentTime = millis();
previousTime = currentTime;
// Print to serial port
Serial.println("New Client.");
// String to hold data from client
String currentLine = "";
// Do while client is cponnected
while (client.connected() && currentTime - previousTime <= timeoutTime) {
currentTime = millis();
if (client.available()) { // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write(c); // print it out the serial monitor
header += c;
if (c == '\n') { // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK) and a content-type
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Display the HTML web page
client.println("<!DOCTYPE html>");
client.println("<html lang=\"ja\">");
client.println("<head>");
client.println(" <title>Wifi_Servo</title>");
client.println(" <meta charset=\"utf-8\">");
client.println(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>");
client.println("");
client.println(" <style>");
client.println(" h1 {color:black;background: lightsteelblue ;font-family:\"BIZ UDゴシック\";font-size: 150%; text-indent: 1em}");
client.println(" h2 {color:black;font-family:\"BIZ UDゴシック\";font-size: 120%; text-indent: 2em}");
client.println(" span {color:grey; font-family:\"BIZ UDゴシック\";font-size: 150%}");
client.println(" p{text-indent:2em}");
client.println("");
client.println("");
client.println(" .radio-area{text-indent:3em}");
client.println(" .angle{text-indent:10em}");
client.println(" .radio_item{display: block; color: grey}");
client.println("");
client.println(" .slider {");
client.println(" -webkit-appearance: none;");
client.println(" width: 350px;");
client.println(" height: 25px;");
client.println(" border-radius: 10px;");
client.println(" background: #008000;");
client.println(" outline: none;");
client.println(" opacity: 0.6;");
client.println(" -webkit-transition: .2s;");
client.println(" transition: opacity .2s;");
client.println(" }");
client.println("");
client.println(" </style>");
client.println(" <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script>");
client.println("</head>");
client.println("<body>");
client.println("<h1>ESP32をwebサーバ経由で制御するページだよ</h1>");
client.println("");
client.println("<div class=\"radio-area\">");
client.println(" <label class=\"radio_item\" id=\"pk_lb\"><input type=\"radio\" name=\"mode\" id=\"pika\" value=\"pk\" checked> ピカチュー こっちむいて</label>");
client.println(" <label class=\"radio_item\" id=\"fs_lb\"><input type=\"radio\" name=\"mode\" id =\"fusi\" value =\"fs\"> フシギダネ こっちむいて</label>");
client.println(" <label class=\"radio_item\" id=\"fr_lb\"><input type=\"radio\" name=\"mode\" id =\"free\" value =\"fr\"> ピカチューとフシギダネは自由な時間をすごします</label>");
client.println("</div>");
client.println("");
client.println("<h2>スライダーでも動かしてみよう</h2>");
client.println(" <input type=\"range\" min=\"0\" max=\"180\" class=\"slider\" id=\"servoSlider\" onchange=\"servo(this.value)\"/>");
client.println("</form>");
client.println("<h2>Current Your Target Angle is ..</h2>");
client.println(" <div class=\"angle\"><font size = \"7\"><span id=\"servoPos\"></span></font>deg</div>");
client.println("<script>");
client.println(" var slider = document.getElementById(\"servoSlider\");");
client.println(" var servoP = document.getElementById(\"servoPos\");");
client.println(" servoP.innerHTML = slider.value;");
client.println(" slider.oninput = function() {");
client.println(" slider.value = this.value;");
client.println(" servoP.innerHTML = this.value;");
client.println(" }");
client.println("");
client.println(" $(function(){");
client.println(" $( 'input[name=\"mode\"]:radio' ).change( function() {");
client.println(" var sel = $(this).val();");
client.println(" if(sel == \"pk\")");
client.println(" {");
client.println(" servoP.innerHTML = 180;");
client.println(" slider.value=180;");
client.println(" servo(slider.value);");
client.println(" }");
client.println(" if(sel == \"fs\")");
client.println(" {");
client.println(" servoP.innerHTML = 0;");
client.println(" slider.value=0;");
client.println(" servo(slider.value);");
client.println(" }");
client.println(" if(sel == \"fr\")");
client.println(" {");
client.println("");
client.println(" }");
client.println(" });");
client.println(" });");
client.println("");
client.println(" function freeControl(){");
client.println("");
client.println(" if(\"fr\"==$('input:radio[name=\"mode\"]:checked').val())");
client.println(" {");
client.println(" servoP.innerHTML = Math.round( Math.random()*180 );");
client.println(" slider.value=servoP.innerHTML;");
client.println(" servo(slider.value);");
client.println(" }");
client.println(" }");
client.println("");
client.println(" setInterval(freeControl, 1000);");
client.println(" $.ajaxSetup({timeout:1000});");
client.println(" function servo(pos) {");
client.println(" $.get(\"/?value=\" + pos + \"&\");");
client.println(" {Connection: close};");
client.println(" }");
client.println("</script>");
client.println("</body>");
client.println("</html>");
// GET data
if(header.indexOf("GET /?value=")>=0) {
pos1 = header.indexOf('=');
pos2 = header.indexOf('&');
// String with motor position
valueString = header.substring(pos1+1, pos2);
// Move servo into position
myservo.write(valueString.toInt());
// Print value to serial monitor
Serial.print("Val =");
Serial.println(valueString);
}
// The HTTP response ends with another blank line
client.println();
// Break out of the while loop
break;
} else {
// New lline is received, clear currentLine
currentLine = "";
}
} else if (c != '\r') { // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
}
}
// Clear the header variable
header = "";
// Close the connection
client.stop();
Serial.println("Client disconnected.");
Serial.println("");
}
}
以上です。実際、むちゃくちゃなコードですが、一応動作します。client.println()の連打で可読性が最悪です。
実は、SPIFFSというのを利用して、htmlファイルを別にESP32のフラッシュ上に保存して、実行時に読み込む方法がありますが、そっちのほうが実際には一般的です。
それはそれとして、また別の記事でまとめたいと思います。とりあえず今回は、ページがつくれたということで、ハッピーでした。
ばんざい ESP32 🙂