ロボットの頭の制作にとりかかろうと思っています。頭脳ではなく、特に動きの部分についてです。首を左右に振る動作、顔を上げ下げする動作が出来るように、サーボモーターで動作するアクチュエーターを作りたいと思います。
アニマトロニクス
以前より良く拝見していた James Bruton さんの YouTubu チャンネルにロボットの頭をスムーズに動かすというテーマの動画がありました。(私も何度も見ていますが、56万回も再生されてます。)
構造は、こちらを見本にして、まずは、作ってみたいと思います。またテーマにもなっているスムーズな動作を取り入れて動くようにしたいと思います。
見出しの「アニマトロニクス」ですが、これは「生物の形や動きを模倣するロボット技術」のことで、今回の場合は、特に動作のことを指しています。通常、Arduino でサーボモータを動かす場合は、一定速度で動き出しから停止まで制御しますが、これだと直線的な動きになってしまい、いかにもロボット的で、スムーズな動きには見えません。つまり、動作の中に強弱がないと、生物らしく見えないということですね。
この動画の中では、動き出しは最大速度で止まる時に徐々に速度を下げるようにして、滑らかな動きを再現していました。
YouTubeチャンネルの説明欄にデータの保存先があるのでアクセスしてダウンロードしました。
https://github.com/XRobots/ServoSmoothin
CADフォルダの中にSTEPファイルがあるので、Fusion360 で開いて、STLファイルを作ります。
何だか顔のパーツとしては、目玉だけですが愛嬌があってかわいいですね。
多分、これが動くと一層かわいさが増すような気がします。
安定化電源(バッテリーボックス)を用意する
今回作ろうとしているロボットヘッドは、サーボモータが7台取り付けられており、サーボモータをしっかり動かすためには、電圧を安定させることと、ある程度の電流が必要になります。したがって、Arduinoの5Vとは、別に外部電源で6V(Max2A)を供給するようにバッテリーボックスを用意することにしました。
ベースは、以下のブログで紹介したものを利用します。
上記のブログ時点のバッテリーボックスから以下の点を改良しています。
- スイッチを取り付ける
- 充電端子は、MicroUSB x 2 から Type-C USB x 1 に変更
⇒これは、Type-C USB x1 から並列接続で TP4056 x 2 を接続 - フタ、カバーを取り付ける
バッテリー回路は、以下のように接続します。
以下の回路図イメージとは異なりますが、充電電源は、USB Type-C を TP4056 モジュールに接続して行います。それぞれを 18650 Li-Poバッテリーを接続して個々に充電します。出力は、TP4056 のOUTを繋ぐことでプラスとプラス、マイナスとマイナスを並列接続にしています。
Li-Po バッテリーの基準電圧は、3.7V 1Amax なので、出力は、3.7V 2Amax になる想定です。
電流は、2Amaxと、おそらくサーボモータが動くレベルのものになったと思いますが、電圧については、3.7V ではサーボモータは動きません。これを MT3608 によって昇圧して6Vに調整できるようにしたいと思います。
回路に使用した部品のリンクを以下に貼り付けておきます。
実際に回路をはんだ付けして繋ぎました。
ちょっとグチャっとなっていますが、各モジュールを電線で繋いではんだ付けを行いました。
こちらを3Dプリンターで印刷したバッテリーボックスに組み付けていきます。
中蓋と電池ボックスを取り付けていきます。
電池ボックスには、電池との接点を取り付けます。
ここまでで充電して、放電までは可能になりましたが、見出しの安定化電源にはなっていません。
安定化電源のために電圧計を取り付けようと思っています。これは、電池は放電を行っていくと電圧が下がり機器の動作が不安定になってしまいます。そこで電圧を MT3608 によっていつでも調整できるようにしています。
電圧の測定は、以下の部品を使って行います。この部品では電流も表示されます。
さらにこの電圧電流計も含めて電源スイッチを付けてオン/オフできるようにしました。
このスイッチは、必須の部品ではないかと思います。今まで、色々と実験をしていて、電源周りが部品の破損や感電などの原因になることが多々ありました。
大抵は、電源コネクタ接続時のショートによるものが多かったと思います。なので回路の接続作業時などには、確実に電源が切れている状態にする必要があります。また、バッテリーの無駄な消費も軽減することができます。(サーボモータなどは、電源を繋げた状態にしておくと待機電力を消費しています。)
組み立ててカバーをネジで止めると、以下のようになります。
18650 リチウムイオン充電池の出し入れもできるようにしました。
充電は、電源に接続したプラグからUSB TYPE-Cコネクタを接続して行います。
充電が完了すると TP4056 のLEDが青色に代わります。一応、窓を付けて確認しやすいようにしました。
電源スイッチをオンにして、出力電圧の調整を行います。今回は、サーボモータの入力電圧に合わせて6Vに調整します。
調整は、MT3608 の電圧調整用のツマミをバッテリーケース横の六角形の穴からマイナスドライバーを差し込んで行います。
目標電圧の6Vと電圧電流計が消費していると思われる 0.01Aが表示されました。
なお、実際に写真を撮影しても電圧と電流は、同時表示されている状態では、映りません。フレームレートが非常に遅いので肉眼でもチカチカ表示になります。
これでバッテリーボックスは、完成です。
コントローラーをつくってみる
左右の目の動きは、ジョイスティックを使って縦と横の動きをコントロールします。ジョイスティックを押し込むとボタンになりますので、こちらは瞬きをするためのスイッチとしました。タクトスイッチは、プルダウンのために10KΩの抵抗を取り付けています。首を左右に振るための操作ボタンとしています。
バッテリーボックスは、カバーを取り付けてモジュールとして完成形にしましたが、コントローラーは、カバーを取り付けずにブレッドボードが見える形にしました。
下が実際の配線図です。空中でワイヤーが絡まるとジョイスティックの操作の支障になるため、出来るだけブレッドボード用のジャンパーワイヤーを使ってArduinoの近くまで持っていくようにしました。
使用した部品等は、以下になります。
Arduino UNO R3 は、最初スターターキットで始めました。
コントローラーとしては、触れていませんが、サーボモータを複数動かすために PCA9685 を使用しています。
アナログジョイスティックコントローラーとタクトスイッチです。タクトスイッチは、Amazonだと100個単位の大ロットだったので、秋月電子通商で5個程度を個別購入しました。
ブレッドボード用のジャンプワイヤーは、以下を使用しました。
ロボットヘッドを動かしてみる
サーボモーター
マイクロサーボモーターは、Tower Pro の SG90 です。Amazon だと純正品が買えるかわからないので、秋月電子通商で購入しています。元々は、安価なMS18というマイクロサーボモーターを使おうとしていましたが、動作が不安定なので取りやめて純正品に置き換えました。
通常のサーボモーターは、互換品の MG995 です。このサーボモーターも Tower Pro の製品でしたが、現在、純正品は、後継品の MG996R になっています。今のところ不具合等はないです。ただ、安いかというとそうでもないので、純正品を購入すれば良かったと思っています。
Arduino IDE スケッチ
動作に使ったArduino IDEのスケッチは、以下になります。
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
// PWMコントローラー
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40);
// サーボモータの定数値
#define SERVO_FREQ 50 // 50Hz
#define SG90_SERVOMIN 500 // SG90の最小パルス幅(μs)
#define SG90_SERVOMAX 2500 // SG90の最大パルス幅(μs)
#define SG90_SERVOMID 1500 // SG90の中間パルス幅(μs)
#define MG995_SERVOMIN 500 // MG995の最大パルス幅(μs)
#define MG995_SERVOMAX 2500 // MG995の最小パルス幅(μs)
#define MG995_SERVOMID 1500 // MG995の中間パルス幅(μs)
// ジョイスティックの定数値
#define JS_MIN 0
#define JS_MID 512
#define JS_MAX 1023
// デバッグ用
#define SKIP_COUNT 200
#define STOP_COUNT 0
int loop_count = 0;
// 各ジョイスティック、スイッチの接続先ピン
int RX_PIN = A0; // 右ジョイスティックX軸
int RY_PIN = A1; // 右ジョイスティックY軸
int RSW_PIN = 3; // 右ジョイスティックボタン
int LX_PIN = A2; // 左ジョイスティックX軸
int LY_PIN = A3; // 左ジョイスティックY軸
int LSW_PIN = 4; // 左ジョイスティックボタン
int RTSW_PIN = 5; // 右タクトスイッチ
int LTSW_PIN = 6; // 左タクトスイッチ
// 各ジョイスティック、スイッチの位置
int RX_pos; // 右目X軸のジョイスティック位置
int RY_pos; // 右目Y軸のジョイスティック位置
int RSW_flg; // 右目ボタン(1が押してないとき、0が押された時)
int RZ_pos; // 右瞼のジョイスティック位置(仮想)
int LX_pos; // 左目X軸のジョイスティック位置
int LY_pos; // 左目Y軸のジョイスティック位置
int LSW_flg; // 左目ボタン(1が押してないとき、0が押された時)
int LZ_pos; // 左瞼のジョイスティック位置(仮想)
int RTSW_flg; // 右タクトスイッチ(0が押してないとき、1が押された時)
int LTSW_flg; // 左タクトスイッチ(0が押してないとき、1が押された時)
int NECK_pos; // 首のジョイスティック位置(仮想)
// 各モーターのピン
int SERVO_L1 = 0; // 左目の上下
int SERVO_L2 = 1; // 左目の左右
int SERVO_L3 = 2; // 左目の瞼開閉
int SERVO_R1 = 3; // 右目の上下
int SERVO_R2 = 4; // 右目の左右
int SERVO_R3 = 5; // 右目の瞼開閉
int SERVO_N1 = 6; // 首の左右回転
// サーボモーターをスムーズに動かすための変数
double L1_Scaled, L1_Smoothed, L1_SmoothedPrev;
double L2_Scaled, L2_Smoothed, L2_SmoothedPrev;
double L3_Scaled, L3_Smoothed, L3_SmoothedPrev;
double R1_Scaled, R1_Smoothed, R1_SmoothedPrev;
double R2_Scaled, R2_Smoothed, R2_SmoothedPrev;
double R3_Scaled, R3_Smoothed, R3_SmoothedPrev;
double N1_Scaled, N1_Smoothed, N1_SmoothedPrev;
void setup() {
// 入力ピンのセットアップ
pinMode(RX_PIN, INPUT);
pinMode(RY_PIN, INPUT);
pinMode(RSW_PIN, INPUT);
pinMode(LX_PIN, INPUT);
pinMode(LY_PIN, INPUT);
pinMode(LSW_PIN, INPUT);
digitalWrite(RSW_PIN, HIGH);
digitalWrite(LSW_PIN, HIGH);
pinMode(RTSW_PIN, INPUT);
pinMode(LTSW_PIN, INPUT);
// サーボモータを初期化する。
pwm.begin();
pwm.setPWMFreq(SERVO_FREQ);
initServoPos();
// シリアルモニタの初期化
Serial.begin(115200);
delay(500);
}
/***********************************************************
* サーボモータの位置の初期化
***********************************************************/
void initServoPos() {
pwm.writeMicroseconds(SERVO_L1, MG995_SERVOMID);
pwm.writeMicroseconds(SERVO_L2, SG90_SERVOMID);
pwm.writeMicroseconds(SERVO_L3, SG90_SERVOMID);
pwm.writeMicroseconds(SERVO_R1, MG995_SERVOMID);
pwm.writeMicroseconds(SERVO_R2, SG90_SERVOMID);
pwm.writeMicroseconds(SERVO_R3, SG90_SERVOMID);
pwm.writeMicroseconds(SERVO_N1, MG995_SERVOMID);
}
void loop() {
// ループカウントアップ
loop_count++;
// 入力値の取得
RY_pos = analogRead(RY_PIN);
RX_pos = analogRead(RX_PIN);
RSW_flg = digitalRead(RSW_PIN);
if (RSW_flg) { RZ_pos = JS_MID; } else { RZ_pos = JS_MIN; }
LY_pos = analogRead(LY_PIN);
LX_pos = analogRead(LX_PIN);
LSW_flg = digitalRead(LSW_PIN);
if (LSW_flg) { LZ_pos = JS_MID; } else { LZ_pos = JS_MAX; }
RTSW_flg = digitalRead(RTSW_PIN);
LTSW_flg = digitalRead(LTSW_PIN);
if (RTSW_flg && !LTSW_flg) { NECK_pos = JS_MAX; }
else if (!RTSW_flg && LTSW_flg) { NECK_pos = JS_MIN; }
else { NECK_pos = JS_MID; }
// スケーリング
R1_Scaled = ((RY_pos - JS_MID) * -1.5) + MG995_SERVOMID;
R2_Scaled = ((RX_pos - JS_MID) * 1.8) + SG90_SERVOMID;
R3_Scaled = ((RZ_pos - JS_MID) * 1.2) + SG90_SERVOMID;
L1_Scaled = ((LY_pos - JS_MID) * 1.5) + MG995_SERVOMID;
L2_Scaled = ((LX_pos - JS_MID) * 1.8) + SG90_SERVOMID;
L3_Scaled = ((LZ_pos - JS_MID) * 1.2) + SG90_SERVOMID;
N1_Scaled = ((NECK_pos - JS_MID) * -1.6) + MG995_SERVOMID;
// スムーズ補正
R1_Smoothed = (R1_Scaled * 0.02) + (R1_SmoothedPrev * 0.98);
R2_Smoothed = (R2_Scaled * 0.02) + (R2_SmoothedPrev * 0.98);
R3_Smoothed = (R3_Scaled * 0.05) + (R3_SmoothedPrev * 0.95);
L1_Smoothed = (L1_Scaled * 0.02) + (L1_SmoothedPrev * 0.98);
L2_Smoothed = (L2_Scaled * 0.02) + (L2_SmoothedPrev * 0.98);
L3_Smoothed = (L3_Scaled * 0.05) + (L3_SmoothedPrev * 0.95);
N1_Smoothed = (N1_Scaled * 0.02) + (N1_SmoothedPrev * 0.98);
// スムーズ補正を保存する(次回使うために)
R1_SmoothedPrev = R1_Smoothed;
R2_SmoothedPrev = R2_Smoothed;
R3_SmoothedPrev = R3_Smoothed;
L1_SmoothedPrev = L1_Smoothed;
L2_SmoothedPrev = L2_Smoothed;
L3_SmoothedPrev = L3_Smoothed;
N1_SmoothedPrev = N1_Smoothed;
// シリアルプリント
Serial.print(R1_Smoothed);
Serial.print(" , ");
Serial.print(R2_Smoothed);
Serial.print(" , ");
Serial.print(R3_Smoothed);
Serial.print(" , ");
Serial.print(L1_Smoothed);
Serial.print(" , ");
Serial.print(L2_Smoothed);
Serial.print(" , ");
Serial.print(L3_Smoothed);
Serial.print(" , ");
Serial.println(N1_Smoothed);
// 起動直後は、不安定なためモーター駆動はスキップする
if (SKIP_COUNT > loop_count) {
return;
} else if (SKIP_COUNT == loop_count) {
Serial.println("Start the servo motor operation.");
}
// サーボモーターへ出力
pwm.writeMicroseconds(SERVO_R1, (int)R1_Smoothed);
pwm.writeMicroseconds(SERVO_R2, (int)R2_Smoothed);
pwm.writeMicroseconds(SERVO_R3, (int)R3_Smoothed);
pwm.writeMicroseconds(SERVO_L1, (int)L1_Smoothed);
pwm.writeMicroseconds(SERVO_L2, (int)L2_Smoothed);
pwm.writeMicroseconds(SERVO_L3, (int)L3_Smoothed);
pwm.writeMicroseconds(SERVO_N1, (int)N1_Smoothed);
delay(5);
if (STOP_COUNT > 0 && loop_count > STOP_COUNT) {
exit(0);
}
}
Cアニマトロニクス制御の部分は、次のようなロジックになっています。
- ジョイスティックの中心位置が 512 として、中心から現在のジョイスティック位置の相対位置を求め係数を掛けます。その値ににサーボモーターの中心位置を加算します。
R1_Scaled = ((RY_pos – JS_MID) * -1.5) + MG995_SERVOMID; - delay(5)としているので、1秒間に200回ループするとして、上記の値に0.02を掛けて5m秒後の想定位置に、前回の位置 * 0.98 を加算します。前半部分は線形計算、後半部分が非線形計算になっています。
R1_Smoothed = (R1_Scaled * 0.02) + (R1_SmoothedPrev * 0.98); - 計算結果を前回の位置に退避して、サーボモータに計算結果の位置への制御命令を送ります。
R1_SmoothedPrev = R1_Smoothed;
pwm.writeMicroseconds(SERVO_R1, (int)R1_Smoothed);
最初の200回をスキップしているのは、起動時から200回位ループするまでは、計算結果が安定しないため捨てています。
動かしてみる
バッテリーボックスとコントローラー、コントローラーとロボットヘッドのサーボモーターを繋いで動かしてみました。このスケッチで動かしてみました。
追記予定
この設計、サーボ制御を参考にして、次はオリジナルのカメラを搭載したロボットヘッドを制作したいと思います。
以上
コメント