ネコ耳など生き物をモチーフにした作品に最適な、サーボモーターを滑らかに動かすArduino自作コードを紹介します。現在の角度と目的角度の差を1 / 10ずつ動かすことで、最初は早く、徐々に速度が落ちる自然な動きを実現します。
👆実際にこの記事のコードを使用してサーボモーターを制御している様子。
使用するライブラリについて
今回は、ESP32Servo.hライブラリを使用して、2つのサーボモーターを滑らかに動かします。ESP32Servo.hは、Arduino IDEのライブラリマネージャーから簡単にインストールできます。
また、サーボモーターを動かす関数には角度を指定するServo.write()とパルス幅を指定するServo.writeMicroseconds()がありますが、今回はServo.write()を使います。
コード全体の紹介
まずは、コード全体の大まかな説明をします。詳細な解説は「より詳しい解説・状況に応じた書き換え方」をご確認ください。
- #include <ESP32Servo.h>
- // Servo制御用の変数
- float servo_now = 90; // 現在の角度
- // サーボモータオブジェクト
- Servo servoR;
- Servo servoL;
- #define PIN_R 38 // 右のサーボに接続するピン
- #define PIN_L 39 // 左のサーボに接続するピン
- void setup() {
- // サーボモータの初期設定
- servoR.attach(PIN_R);
- servoL.attach(PIN_L);
- // サーボモータの初期位置設定
- servoR.write(90);
- servoL.write(90);
- }
- void loop(void) {
- delayAndMoveServo(2000, 90 + 45); // 2秒delayしつつ135度に動かす
- delayAndMoveServo(2000, 90 - 45); // 2秒delayしつつ45度に動かす
- }
- void delayAndMoveServo(int delay_time, int angle) { // delay_timeミリ秒delayしつつangleに動かす
- for (int i = 0; i < delay_time; i += 10){
- // サーボ動作
- servo_now = servo_now + ((float)angle - servo_now) / 10 ;
- moveServo((int)servo_now);
- delay(10);
- }
- }
- void moveServo(int angle) {
- servoR.write(angle);
- servoL.write(angle);
- }
- #include <ESP32Servo.h>
- // Servo制御用の変数
- float servo_now = 90; // 現在の角度
- // サーボモータオブジェクト
- Servo servoR;
- Servo servoL;
- #define PIN_R 38 // 右のサーボに接続するピン
- #define PIN_L 39 // 左のサーボに接続するピン
- void setup() {
- // サーボモータの初期設定
- servoR.attach(PIN_R);
- servoL.attach(PIN_L);
- // サーボモータの初期位置設定
- servoR.write(90);
- servoL.write(90);
- }
- void loop(void) {
- delayAndMoveServo(2000, 90 + 45); // 2秒delayしつつ135度に動かす
- delayAndMoveServo(2000, 90 - 45); // 2秒delayしつつ45度に動かす
- }
- void delayAndMoveServo(int delay_time, int angle) { // delay_timeミリ秒delayしつつangleに動かす
- for (int i = 0; i < delay_time; i += 10){
- // サーボ動作
- servo_now = servo_now + ((float)angle - servo_now) / 10 ;
- moveServo((int)servo_now);
- delay(10);
- }
- }
- void moveServo(int angle) {
- servoR.write(angle);
- servoL.write(angle);
- }
float型の変数 servo_nowで現在の角度を保存し、delayAndMoveServo()関数で目的の角度との差分の1 / 10ずつ動かすようにします。
delayAndMoveServo()関数では指定した時間を10ミリ秒に区切り、サーボモーターの動作と処理の待機を一度に行います。
moveServo()関数では呼び出し時に引数の角度にすべてのサーボモーターを動かします。
見ての通り、かなりシンプルなコードですがfor文で1度ずつ動かすよりもメリハリのある動きを再現できます。また、これは好みですがloop()関数内でfor文を回すよりもdelayAndMoveServo()関数のように関数として外に出す方が読みやすいコードになる気がするのでおすすめです。
より詳しい解説・状況に応じた書き換え方
ここではArduinoでサーボモーターを動かしたことが少ない方向けにより詳しく解説します。書き換える必要のある箇所がわからない方はぜひ読み進めながらコードを書いていってください。
まずは、コード冒頭の説明をします。冒頭ではライブラリの呼び出し、制御用の変数の宣言、サーボモーターオブジェクトの宣言、PIN番号の定義を行っています。
- #include <ESP32Servo.h>
- // Servo制御用の変数
- float servo_now = 90; // 現在の角度
- // サーボモータオブジェクト
- Servo servoR;
- Servo servoL;
- #define PIN_R 38 // 右のサーボに接続するピン
- #define PIN_L 39 // 左のサーボに接続するピン
- #include <ESP32Servo.h>
- // Servo制御用の変数
- float servo_now = 90; // 現在の角度
- // サーボモータオブジェクト
- Servo servoR;
- Servo servoL;
- #define PIN_R 38 // 右のサーボに接続するピン
- #define PIN_L 39 // 左のサーボに接続するピン
- ライブラリの呼び出し:#include <ESP32Servo.h>
- このプログラムはESP32-S3チップで動くAtomS3で動かすためにESP32Servo.hというライブラリを使用しています。
- ESP32でないデバイスだとESP32Servo.hで動作しない可能性があります。Servo.hなど他のライブラリをご使用ください。
- 制御用の変数の宣言:float servo_now = 90;
- サーボモーターの最初の角度を入力してください。のちに、小数点以下の計算を行いたいためfloat型で宣言しています。
- サーボモーターのオブジェクトを宣言:Servo servoR;
- Servo myServo; のように他の名前で宣言することが可能です。今回は制作物の右と左に1つずつサーボモーターがあるため、servoRとservoLの名前で宣言しています。
- 使うサーボモーターの数だけ用意してください。
- PIN番号の定義:#define PIN_R 38
- サーボモーターに割り当てるピンを定義します。使用する基盤でどのピンにモーターを接続するかを確認する必要があります。
- #define PIN_myServo のように他の名前で宣言することが可能です。ただ、defineで使用する名前は必ず他の変数や文字列と一致しないよう気をつけてください。
- defineはセミコロン;が不要なので行の最後に追加しないよう気をつけてください。
- こちらも使うサーボモーターの数だけ用意してください。
次にsetup()関数とloop()関数の説明をします。setup()関数ではピンの割り当て、初期位置への移動(実は不要)。loop()関数では関数の呼び出しを行っています。
- void setup() {
- // サーボモータの初期設定
- servoR.attach(PIN_R);
- servoL.attach(PIN_L);
- // サーボモータの初期位置設定
- servoR.write(90);
- servoL.write(90);
- }
- void loop(void) {
- delayAndMoveServo(2000, 90 + 45); // 2秒delayしつつ135度に動かす
- delayAndMoveServo(2000, 90 - 45); // 2秒delayしつつ45度に動かす
- }
- void setup() {
- // サーボモータの初期設定
- servoR.attach(PIN_R);
- servoL.attach(PIN_L);
- // サーボモータの初期位置設定
- servoR.write(90);
- servoL.write(90);
- }
- void loop(void) {
- delayAndMoveServo(2000, 90 + 45); // 2秒delayしつつ135度に動かす
- delayAndMoveServo(2000, 90 - 45); // 2秒delayしつつ45度に動かす
- }
- ピンの割り当て:servoR.attach(PIN_R);
- 冒頭で宣言したサーボモーターオブジェクトにピンを割り当てます。
- 宣言したオブジェクト名がmyServo、対応するPIN番号がPIN_myServoの場合は myServo.attach(PIN_myServo); 、といったように適宜オブジェクト名とPIN番号を変更してください。
- attach()の引数に直接PIN番号を入力することもできます。
- 初期位置への移動:servoR.write(90);
- サーボモーターを初期位置まで動かします。write()メソッドは入力した角度までサーボモーターを動かします。
- attach()メソッドと同じく 動作させたいサーボモーターのオブジェクト名.write(角度) のように変更してください。
実はなくても大丈夫ですが、ある方が初期の角度を意識できるので書いてます💦
- 関数の呼び出し:delayAndMoveServo(2000, 90 + 45);
- int型の引数を2つ持つ delayAndMoveServo()関数を呼び出しています。
- 1つ目の引数で動かす時間兼待機する時間をミリ秒で指定します。
- 2つ目の引数でサーボモーターを動かしたい角度を指定します。
- このコードでは135度と45度に2秒で切り替えるように指定していますが、ボタンやその他センサーと組み合わせても良いと思います。
関数を呼び出すタイミング、2つの引数を適宜変更してください。
- int型の引数を2つ持つ delayAndMoveServo()関数を呼び出しています。
次にdelayAndMoveServo()関数の中身を説明します。処理は単純で第1引数で指定した時間まで10ミリ秒ずつサーボモーターの位置の計算と待機を繰り返します。
- void delayAndMoveServo(int delay_time, int angle) { // delay_timeミリ秒delayしつつangleに動かす
- for (int i = 0; i < delay_time; i += 10){
- // サーボ動作
- servo_now = servo_now + ((float)angle - servo_now) / 10 ;
- moveServo((int)servo_now);
- delay(10);
- }
- }
- void delayAndMoveServo(int delay_time, int angle) { // delay_timeミリ秒delayしつつangleに動かす
- for (int i = 0; i < delay_time; i += 10){
- // サーボ動作
- servo_now = servo_now + ((float)angle - servo_now) / 10 ;
- moveServo((int)servo_now);
- delay(10);
- }
- }
- 第1引数の時間になるまでループさせる:for (int i = 0; i < delay_time; i += 10) { /* 処理内容 */ }
- 第1引数であるdelay_timeミリ秒になるまで{}内の処理を繰り返させます。
- delay()関数内の時間を変更する場合は i += 10 もそのdelay()関数で指定した時間に合わせてください。
- サーボモーターの現在地と動かしたい角度との差の1/10だけ角度を変更する:servo_now = servo_now + ((float)angle - servo_now) / 10 ;
- 今回のコードの肝となる計算です。
- servo_now変数の角度を動かしたい角度に1/10だけ近づけます。
- サーボの動きが遅い場合は最後の / 10 の値を減らすと速く、早い場合は増やすと遅く動くようにできます。
- サーボモーターを動かすための関数を呼び出す:moveServo((int)servo_now);
- moveServo関数に計算した角度を入れ、サーボモーターを動かします。
- moveServo関数の引数はint型なので必ず (int) で変数をint型に変換してください。
- myServo.write((int)servo_now); のように関数を挟まないでwrite()メソッドを呼び出してもらってもOKです。
- サーボモーターが多い場合や他の関数で呼び出す機会が多い場合、サーボモーターオブジェクト名がよく変わる場合などはmoveServo関数のように1箇所で書き換えれるようにすることをおすすめします。
- サーボモーター名のや変数名を変更する場合は Ctrl + L(もしくは command + L)で一気に変更できるので頭の片隅に置いておいてください。
- サーボモーターが多い場合や他の関数で呼び出す機会が多い場合、サーボモーターオブジェクト名がよく変わる場合などはmoveServo関数のように1箇所で書き換えれるようにすることをおすすめします。
- 10ミリ秒待機する:delay(10);
- delay()関数は呼び出すと引数の時間(ミリ秒)だけ次の処理を行わず待機します。
- 基本的にfor文とこのdelay()関数の引数を変更しなくても大丈夫ですがもし変更する場合はセットで変更してください。
次にmoveServo()関数の中身を説明します。引数で渡された角度にサーボモーターを動かします。
- void moveServo(int angle) {
- servoR.write(angle);
- servoL.write(angle);
- }
- void moveServo(int angle) {
- servoR.write(angle);
- servoL.write(angle);
- }
- サーボモーターオブジェクトのwrite()メソッドを呼び出す:servoR.write(angle);
- この関数を呼び出した際に動かしたいサーボモーターオブジェクトすべてでwrite()メソッドを呼び出してください。
- 左右反転させたい場合は servoL.write(180 - angle); のように180から引くとすぐに実装できます。
- servoR.write(angle + servoR_correct); のようにサーボモーターの角度を変更することはお勧めしません。
write()メソッドは0度から180度までしか動かないため、一部のモーターのみ動いていない角度が生まれかねません。
- servoR.write(angle + servoR_correct); のようにサーボモーターの角度を変更することはお勧めしません。
コードの詳細な説明は以上です。今回の記事が、サーボモーターの滑らかな制御に役立てば幸いです。
まとめ
今回はサーボモーターを滑らかに動かすコードの解説をしました。少し手間ですが単純に1度ずつ動かすよりも「生き物っぽさ」が生まれると思うのでそういったものがモチーフのものづくりにどんどん活用してください✨
謝辞
このコードはTechSeeker Hackathon 2024の成果物の一部です。このような学習の機会を用意してくださった方々、同じチームとして支えてくださったデジもく会のみなさんに感謝します。本当にありがとうございました。