作ったきっかけ
自己紹介をしたときなど、「趣味は電子工作です。こういうの作ってます。」と写真を見せながら話したものの、実機がないことで相手がピンと来ていないと感じたことが多々ありました。
皆さんも似た経験があるのではないでしょうか?
それがあって、どこでも連れていける手乗りサイズのロボットを作ってみました。
ほしい要素と選定
作るうえで、まずほしい要素をあげてみました。
- 多機能であること(動く・話す・表情が変わる の3つ欲しい。)
- いつでも動かせること(携帯性・信頼性・安全性が大事。動きませんでしたは悲しい。)
- かわいいこと(かわいいは正義)
以上を満たすため、多機能で入手性がよいM5Stackを選定し、電源は乾電池を使い、一等身サイズにすることにしました。
ハード構成
使用部品など | 説明 |
---|---|
M5Stack | ESP32搭載のボタン・LCD・スピーカといった、様々な機能がつまった便利モジュール |
自作基板 | M5StackのBUSからサーボの信号を分配する。電源スイッチ付き。 |
ボディ | 3Dプリンタにて製作。サーボをはめ込むことで固定ができる。 |
サーボ | 型式:ES9051 足に4個、腕に2個の計6個使用。500円ほどで買えるデジタルサーボ。 |
電池ボックス | 単4電池を4本直列で使用。自作基板を通しサーボには直結・M5Stackには3.3vに降圧して印加 |
こんな感じで組み立てました。
携帯する以上、ケーブルが出ていことがマストです。
ソフト構成
以下のようにスマホからBluetoothで移動等のキー入力・言わせたい言葉を送信します。
そしてそれにより合成音声を生成するAquestalkで喋らせたり、LCDで表情を変えたり、サーボを動かします。
実際の動き
こんな感じです。
BTで話す
Aquestalkの導入は以下を参考にしました。
「AquesTalk-ESP32 Ver.2.0」を使う
プログラムはこんな感じです。
BTで送った文字を読み上げる
#include <M5Stack.h>
#include "AquesTalkTTS.h"
const char* licencekey = "XXX-XXX-XXX"; // AquesTalk-ESP32 licencekey
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
String BTdata, Button;
void setup() {
m5.begin();
Serial.begin(115200);
SerialBT.begin("Tenori"); //Bluetooth device name
M5.Lcd.fillScreen(WHITE);
M5.Lcd.fillRect(80, 100, 30, 75, BLACK);
M5.Lcd.fillRect(210, 100, 30, 75, BLACK);
M5.update();
int iret = TTS.createK(licencekey);
if (iret) {
Serial.print("ERR: TTS_createK():");
Serial.println(iret);
}
TTS.playK("こんにちわ!", 90);
delay(1000);
}
void loop() {
if (SerialBT.available() > 0) {
BTdata = SerialBT.readString();
TTS.playK(BTdata.c_str(), 90);
}
delay(100);
}
}
サーボをゆっくり動かす
ネットでは「ESP32(m5Stack)でサーボを動かすなら ledcWriteを使え」という金太郎飴みたいなコピペ記事が多いです。
参考にして指定角度までサーボをゆっくり&複数同時に動かすプログラムを作りました。
サーボ3個の場合です。(これ以上の数はやや動作が不安定でした。ledcWriteを使わない方法も試してみたいところ)
サーボを複数ゆっくり動かす
#include <M5Stack.h>
int Servo1_pin = 15;
int Servo2_pin = 16;
int Servo3_pin = 17;
//PWMの設定
const int PWM_Hz = 250; //PWM周波数
const int PWM_level = 10; //PWM 10bit 1024分割
const int MAX_SERVO_ANGLE = 120; //サーボの動作角 大抵プラマイ60度
const int MIN_PULSE_WIDTH = 800;//-60度のときのPWM
const int MAX_PULSE_WIDTH = 2200;//60度のときのPWM
int PosNowPulse[3];
int PosDeltaPulse[3];
void setup() {
pinMode(Servo1_pin, OUTPUT);
pinMode(Servo2_pin, OUTPUT);
pinMode(Servo3_pin, OUTPUT);
//モータのPWMのチャンネル、周波数の設定
ledcSetup((uint8_t)1, PWM_Hz, PWM_level);
ledcSetup((uint8_t)2, PWM_Hz, PWM_level);
ledcSetup((uint8_t)3, PWM_Hz, PWM_level);
//モータのピンとチャンネルの設定
ledcAttachPin(Servo1_pin, 1);
ledcAttachPin(Servo2_pin, 2);
ledcAttachPin(Servo3_pin, 3);
}
void loop() {
AllPosS(0, 0, 0, 100); //1秒かけて3個とも0度へ回転
delay(1000);
AllPosS(40, 80, 120, 100); //1秒かけて40度・80度・120度へ回転
delay(1000);
}
//メインのプログラム
void AllPosS(int S1, int S2, int S3, int Speed) { //サーボ1、サーボ2、サーボ3、スピード(1に近いほど速い)
int S[] = {S1, S2, S3};
for (int i = 0; i < 3; i++) {
//PWM設定を元に角度からパルス幅・変化する差分を計算
int PULSE_WIDTH_FROM_ANGLE = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
PosDeltaPulse[i] = PULSE_WIDTH_FROM_ANGLE - PosNowPulse[i];
}
//一回あたりのパルス幅の変化量を計算し、サーボを回転させる
for (int i = 1; i <= Speed ; i++) {
for (int j = 0; j < 3; j++) {
int PulseBuf = PosNowPulse[j] + PosDeltaPulse[j] * i / Speed;
int ledcWrite_secound_value = PulseBuf * PWM_Hz * pow(2, PWM_level) / 1000000;
ledcWrite(j + 1, ledcWrite_secound_value);
}
delay(10);
}
//現在のパルス幅を保存
for (int i = 0; i < 3; i++) {
PosNowPulse[i] = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
}
}
以上を応用し、手乗りロボの全サーボをゆっくり制御しています。
見せられるプログラムだけ公開…
手乗りロボ_挨拶して前進
#include <M5Stack.h>
#include "AquesTalkTTS.h"
const char* licencekey = "XXX-XXX-XXX"; // AquesTalk-ESP32 licencekey
int wait = 200;
int FrameTest = 40;
int Servo1_pin = 15;
int Servo2_pin = 16;
int Servo3_pin = 17;
int Servo4_pin = 22;
int Servo5_pin = 5;
int Servo6_pin = 21;
//PWMの設定
const int PWM_Hz = 250; //PWM周波数
const int PWM_level = 10; //PWM 10bit 1024分割
const int MAX_SERVO_ANGLE = 120;
const int MIN_PULSE_WIDTH = 800;
const int MAX_PULSE_WIDTH = 2200;
int PosEditNum = 0;
int PosNowPulse[6];
int PosDeltaPulse[6];
void setup() {
m5.begin();
Serial.begin(115200);
pinMode(Servo1_pin, OUTPUT);
pinMode(Servo2_pin, OUTPUT);
pinMode(Servo3_pin, OUTPUT);
pinMode(Servo4_pin, OUTPUT);
pinMode(Servo5_pin, OUTPUT);
pinMode(Servo6_pin, OUTPUT);
//サーボののPWMのチャンネル、周波数の設定
ledcSetup((uint8_t)1, PWM_Hz, PWM_level);
ledcSetup((uint8_t)2, PWM_Hz, PWM_level);
ledcSetup((uint8_t)3, PWM_Hz, PWM_level);
ledcSetup((uint8_t)4, PWM_Hz, PWM_level);
ledcSetup((uint8_t)5, PWM_Hz, PWM_level);
ledcSetup((uint8_t)6, PWM_Hz, PWM_level);
//サーボのピンとチャンネルの設定
ledcAttachPin(Servo1_pin, 1);
ledcAttachPin(Servo2_pin, 2);
ledcAttachPin(Servo3_pin, 3);
ledcAttachPin(Servo4_pin, 4);
ledcAttachPin(Servo5_pin, 5);
ledcAttachPin(Servo6_pin, 6);
//顔表示
M5.Lcd.fillScreen(WHITE);
M5.Lcd.fillRect(80, 100, 30, 75, BLACK);
M5.Lcd.fillRect(210, 100, 30, 75, BLACK);
M5.update();
int iret = TTS.createK(licencekey);
if (iret) {
Serial.print("ERR: TTS_createK():");
Serial.println(iret);
}
TTS.playK("こんにちわ!", 90);
Serial.println("こんにちわ!");
AllPos(15, 105, 50, 70, 66, 47); //直立
delay(1000);
AllPosS(15, 105, 50, 70, 66, 60, FrameTest);
delay(wait);
AllPosS(15, 105, 50, 70, 50, 60, FrameTest);
delay(wait);
AllPosS(15, 105, 50, 70, 50, 42, FrameTest);
delay(wait);
}
void loop() {
AllPosS(45, 75, 30, 50, 50, 42, FrameTest);
delay(wait);
AllPosS(45, 75, 30, 50, 66, 47, FrameTest);
delay(wait);
AllPosS(45, 75, 30, 50, 50, 60, FrameTest);
delay(wait);
AllPosS(45, 75, 30, 50, 66, 60, FrameTest);
delay(wait);
AllPosS(45, 75, 70, 90, 66, 60, FrameTest);
delay(wait);
AllPosS(45, 75, 70, 90, 66, 47, FrameTest);
delay(wait);
AllPosS(45, 75, 70, 90, 66, 60, 20);
delay(50);
AllPosS(45, 75, 70, 90, 50, 60, 20);
delay(wait);
AllPosS(45, 75, 70, 90, 50, 42, FrameTest);
delay(wait);
}
void ServoMove(int Servo_num, int angle) {
int PULSE_WIDTH_FROM_ANGLE = map(angle, 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
int ledcWrite_secound_value = PULSE_WIDTH_FROM_ANGLE * PWM_Hz * pow(2, PWM_level) / 1000000;
ledcWrite(Servo_num, ledcWrite_secound_value);
PosNowPulse[Servo_num - 1] = ledcWrite_secound_value;
}
void AllPos(int S1, int S2, int S3, int S4, int S5, int S6) { //右手,左手,右足Y,左足Y,右足R,左足R
int S[] = {S1, S2, S3, S4, S5, S6};
for (int i = 0; i < 6; i++) {
int PULSE_WIDTH_FROM_ANGLE = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
int ledcWrite_secound_value = PULSE_WIDTH_FROM_ANGLE * PWM_Hz * pow(2, PWM_level) / 1000000;
ledcWrite(i + 1, ledcWrite_secound_value);
PosNowPulse[i] = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
}
}
void AllPosS(int S1, int S2, int S3, int S4, int S5, int S6, int Frame) { //右手,左手,右足Y,左足Y,右足R,左足R,スピード
int S[] = {S1, S2, S3, S4, S5, S6};
for (int i = 0; i < 6; i++) {
int PULSE_WIDTH_FROM_ANGLE = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
PosDeltaPulse[i] = PULSE_WIDTH_FROM_ANGLE - PosNowPulse[i];
}
for (int i = 1; i <= Frame ; i++) {
for (int j = 0; j < 6; j++) {
int PulseBuf = PosNowPulse[j] + PosDeltaPulse[j] * i / Frame;
int ledcWrite_secound_value = PulseBuf * PWM_Hz * pow(2, PWM_level) / 1000000;
ledcWrite(j + 1, ledcWrite_secound_value);
}
delay(10);
}
for (int i = 0; i < 6; i++) {
PosNowPulse[i] = map(S[i], 0, MAX_SERVO_ANGLE, MIN_PULSE_WIDTH, MAX_PULSE_WIDTH);
}
}
最後に
1
5
-
PONDA
さんが
2021/02/28
に
編集
をしました。
(メッセージ: 初版)
Opening
masayasan
2021/03/13 Opening
chrmlinux03
2022/02/23
ログインしてコメントを投稿する凄く参考になりました。
デザインも可愛くて、自分も作ってみたくなりました。
"#ぢゃないほうのロボット"
お誕生日おめでとうございます
この記事がまとめられたのがちょうど今日になりますね
今後もますますの開発を期待してます
頑張りましょう!