コードの説明 ①
// サーボモータを曲げセンサーで動かす
// by Gounbeee
// coaramause.com
// 2023
// 今回使用するモータドライバー、PCA9685を動かすためのライブラリ
//https://github.com/adafruit/Adafruit-PWM-Servo-Driver-Library
#include <Adafruit_PWMServoDriver.h>
// ESP32が、2つのコアを持つことを利用し、
// Core 0 -> サーボモータを動かす
// Core 1 -> 曲げセンサーの入力を受ける
// で動かしたく思います。
//
// そのため、まず2つのタスクをしてするためのオブジェクトを用意します。
TaskHandle_t tsk1;
TaskHandle_t tsk2;
解説 ①
それでは、コードの解説をしていきます。まず、#include で、必要な道具を取り入れています。#includeと記述し他のファイルを指定することで、他人が書いたコードを取り入れることができます。他のプログラミング言語ではimportなどの言葉を用いています。今回の演習では、Adafruit社のPWMサーボドライバーを使用します。
TaskHandleを2つ作っているのは、曲げセンサーの入力を受け取るタスクと、サーボモータを動かすタスクの2つの処理をさせているからです。
ハンドルと表現するのは、C言語の場合、メモリのアドレスを入れておき、後でアクセスすることで情報が得られるポインターがあるので、「この部分から」情報を掴むことができる、という意味で覚えるといいと思います。
コードの説明 ②
// --------------------------------------------------------
// 上記で取り入れたライブラリを使って、サーボドライバー制御用オブジェクトを作ります。
Adafruit_PWMServoDriver servoDrv = Adafruit_PWMServoDriver(0x40);
解説 ②
早速、Adafruit社のサーボモータドライバーをオブジェクトとして作成します。サーボモータのドライバーとESP32は、I2C(アイ・スクエアド・シーと発音)という通信方式で情報のやり取りをします。
そこで、サーボモータドライバーのオブジェクトを作成するとき、I2C方式のコミュニケーションのため、0x40とメモリアドレスを指定しています。
0x40という表記の「0x」は、このあと続く40という数字が「16進数」という意味です。
コードの説明 ③
// --------------------------------------------------------
// ここは、Task2用の、センサーのためのグローバルな情報です。
// 基本的には、別タスクになっている以上、同じメモリの場所を参照するときは
// 注意が必要です。共有している情報が、書き換えられる時間帯は、まだ情報が確定していないためです。
// よって、タスク間の通信をちゃんと行なって、「待ってもらう」などの処理が必要なときもありますが、
// ここでは省略します。
// ADC(Analog-to-Digital Converter)の機能を持つPINです。
int sensorPin = 4;
// センサーから受けた情報です。
// センサーからの情報を元に、サーボモータが動くべき地点を計算します。
int sensorValue = 0;
解説 ③
ここでは、センサーのためのピン設定を行います。4番を使用しています。sensorValue変数は、センサーから受け取った情報を保存する場所として設定しました。それを、今後サーボモータが回転する角度として使用します。
コードの説明 ④
// このスケッチが実行されたとき、一度実行され必要な情報の準備を行う関数
void setup() {
// Serial通信(ここではUSB)を通し、
// Arduino IDEが持つ「シリアルモニター」を使って
// 値を出力するためのセットアップ
Serial.begin(115200);
// --------------------------
// センサーのためのPIN設定
pinMode(sensorPin, INPUT);
// --------------------------
// サーボモータのためのセットアップ
servoDrv.begin();
// 今回使用するサーボモータを60Hzの速度で動くように設定
servoDrv.setPWMFreq(60);
delay(10);
// TASKを設定
// サーボモータ用
// 下記に定義している関数を指す
//
// Task1Job : タスクの名前。この情報を用いて、タスク間のやり取りをしないといけないときもあります
// "tsk1" : タスクの名前。この情報を用いて、タスク間のやり取りをしないといけないときもあります
// 10000 : このタスクが使用するメモリの大きさ
// NULL. : このタスクに送る情報
// 1 : タスク処理の優先順位
// &tsk1 : タスクオブジェクトを指すポインター。このタスクの情報を見るための入り口のようなものです
// 0. : このタスクを担当するコアの番号
xTaskCreatePinnedToCore(
Task1Job,
"tsk1",
10000,
NULL,
1,
&tsk1,
0);
delay(500);
xTaskCreatePinnedToCore(
Task2Job,
"tsk2",
10000,
NULL,
1,
&tsk2,
1);
delay(500);
}
解説 ④
setup関数の説明です。ESP32を使用していますが、ソフトウェアではArduinoライブラリを使用しているため、基本的な設定(setup関数、loop関数)などの構成に従っています。このように、一見ルールのように見える構成の仕方は、使用するソフトウェアの枠組みによって決められることがあります。ということは、他の枠組みを取り入れて作業をするときは、当然ですが、違う名称の関数を設定したりします。敢えてESP32の場合を話しますと、ESP32は、「ESP-IDF」というC言語(ArduinoはC++)で記述しながら使用する枠組みも使用できます。Arduinoライブラリよりコードの長さも長くなったりしますが、マイコンをレジストリレベルで設定したり、より細かい設定ができる側面もあります。もちろん、Arduinoライブラリを使用しながらも、より細かい設定を行うオプションもあります。
コードの詳細はコメントに書いている内容通りですが、USB経由で出力を確認するためのSerial初期化、また、センサーのためのピン設定などを行っています。さらに、すでにグローバルで生成したサーボモータのドライバーオブジェクトを使用し、PWM処理の周期を決めています。今回使用したサーボモータはより早い処理周期の設定が可能ですが、一先ずここは60Hzに設定しました。
次に、xTaskCreatePinnedToCore関数を用いて、2つのタスクを作成します。これまでは、ハンドルとなる変数を用意していただけでしたが、ここではタスクの詳細を設定し、作ったハンドルも登録しています。まずこの関数は、ESP32がサポートするFreeRTOSが持つ機能です。FreeRTOSは、組み込みシステムで使用するOSの一つで、2つのタスクを作成するため、2回呼び出されています。
タスクは、基本的には独立して動き続ける処理のことです。そして各タスクはそれぞれのループを持っています。簡略させたイメージとして、Arduinoライブラリを使用するときのsetup関数とloop関数が、それぞれのタスクの中で入っているような感じです。ループ自体はforループで設定され、条件が設けられていないため、絶対に抜け出すことができない、「無限ループ」になっています。
xTaskCreatePinnedToCore関数の設定では、このタスクに関する内容が保管されるメモリーの量を決める必要があります。当然ですが、処理の数が多ければ多いほど、必要なメモリーは多くなるでしょう。そして、ESP32はコアを2つ用意してくれているため、各タスクを担当するコアの番号も、それぞれ違う数値で指定しています。
最後に、1番目の引数に、これから宣言する関数の名前が登録されていることにご注意ください。この関数の中に、実際やってほしい2つの処理(センサー入力とモータ回転)を記述していきます。
コードの説明 ⑤
void loop() {
// 通常の LOOPは使用しません。
}
解説 ⑤
loop関数は、上記で記述したように、FreeRTOSのタスク機能ではない、Arduinoが提供する構成の仕方です。そのため、今回は使用しません。
コードの説明 ⑥
// タスク1
// サーボモータを動かす処理
void Task1Job( void * pvParameters ) {
// TASKは、独立したループを持つ必要があります。
// 下記のFor文はそのためのものです。
// また、For文はdelay(1)でもさせるようにしましょう。
for(;;) {
// サーボドライバーオブジェクトを使用し、
// 最初から5個までのモータにセンサーから受け取った結果を反映し
// 回転させる。
servoDrv.writeMicroseconds(0, sensorValue );
servoDrv.writeMicroseconds(1, sensorValue );
servoDrv.writeMicroseconds(2, sensorValue );
servoDrv.writeMicroseconds(3, sensorValue );
servoDrv.writeMicroseconds(4, sensorValue );
delay(10);
}
}
解説 ⑥
タスクの一つ目は、「サーボモータを回す」ものです。Task1Jobという名称にしていますが、この名前を、setup関数でタスク設定の時に使用していることに注意しましょう。このように、タスクは、ハンドルや、名前による関数の登録などから設定します。
また、タスクは内部に独自のループを持つことが必要だと話しました。よってforループの中にサーボモータを動かす関数を記述しています。
サーボモータは、パルス波形を時間的な幅を持って受け取った時、それを回転すべき「角度」として解釈します。そこで、回転のため使っている関数、writeMicrosecondsでは、2番目の引数として結局のところ「時間」を送っているのです。送っている情報は時間ですが、結局それは送るパルス波形の形状になるわけです。
ちなみに、writeMicroseconds関数の1番目の引数は、ドライバーのモータをつなげるスロットの番号になっています。つまり、ここでは1番から5番までのモータを時間的な差がない、同時に動かしています。
コードの説明 ⑦
// タスク2
// センサーの入力を受け取る処理
void Task2Job( void * pvParameters ) {
for(;;) {
// 最初に設定した、PIN4からセンサーの情報を受け取る
sensorValue = analogRead(sensorPin);
//Serial.println(sensorValue);
//Serial.println(" | ");
// 曲げセンサーの入力値は、0から4095(4096までの幅)
// ですが、フィルムをある程度曲げるまではずっと4095を得られ、
// そこから一気に1300辺りまで下がって変動しました。
// そこで、一旦、4095(曲っていない状態)は、擬似的に数字を減らしました。
if (sensorValue == 4095) sensorValue = 2000;
// その上、map関数を使用して、値をスケールします。
sensorValue = map(sensorValue, 0, 2000, 500, 2000);
// さらに、今回のサーボモータは500~2500までのパルス幅を入力とし、
// その値が、0°から270°までの回転上の位置を意味しているので、
// その値を外れるような値が出ないようにconstrain関数でで切り取ります。
sensorValue = constrain(sensorValue, 500, 2500);
Serial.println(sensorValue);
delay(20);
}
}
解説 ⑦
次に、タスク2の説明です。タスク2はセンサーからの入力を受け取るための処理になっています。
ここの処理は、Arduinoライブラリの機能を使用し、アナログ入力として設定したピン4番から、analogRead関数を使ってデータを受け取っています。
次に、曲げセンサーを実際動かしてみて詳細な動きを調整する部分になっています。動きの速度、角度の限界値などを動かしながら数値を調整する、ということです。さらに、このような開発では、実際動かしながら調整する過程も必要ですし、使っている機材の書類を参照することも大事だということを忘れないでください。
次は、map関数と、constrain関数を使って、入力された値の範囲を調整、そして切り取っています。モータが受けつくことができる角度の値が決まっているからこそ、それを離れるような数値が入力されないようにするためです。
最後は、ある程度のdelayを入れて終了となりますが、ここでも、モータの処理速度を60Hzとしていることを意識し、それに近い速度のループになるよう設定しました。