ガイド · 読了まで20分

Arduinoロボットアームの作り方:サーボの選び方、トルク計算、PCA9685とBluetooth

Arduinoロボットアームをステップごとに作ります。MDFまたは3Dプリントのフレーム、MG996Rのトルク計算、ポテンショメーターとジョイスティックでの操作、記録と再生、PCA9685とBluetoothのコードまで解説します。

Arduinoロボットアームは、サーボモーターで動く関節によって物をつかんで持ち上げ、向きを変えて別の場所に置ける、卓上サイズのロボットです。土台(ベース)、肩、ひじ、手首、グリッパーで構成され、機構設計、モーター選び、電源管理、プログラミングを一つのプロジェクトで学べます。しかも、Arduino Uno、数個のサーボ、MDFまたは3Dプリントの部品があれば家でも作れ、工場で働く産業用ロボットアームの小さな模型を自分の机の上で動かせます。

このガイドでは、4軸と6軸のロボットアームを最初から最後まで扱います。フレームの材料選び、SG90・MG90S・MG996Rから正しいサーボを選ぶためのトルク計算、5 Vの外部電源、ポテンショメーターとジョイスティックによる操作、動きの記録と再生、16チャンネルのPCA9685サーボドライバ、スマートフォンからのBluetooth操作、そして最もよくある二つのトラブルである震えとリセットの対策です。サーボの仕組みがよくわからない場合は、先にサーボモーターとは?を読んでおくと安心です。

SG90
SG90
MG90S
MG90S
MG996R
MG996R

ロボットアームの仕組みと、必要な軸の数

ロボットアームは人の腕にたとえるとわかりやすくなります。関節ごとにサーボモーターがあり、Arduinoは各サーボに「この角度へ動け」と指示します。サーボは内部のポテンショメーターのおかげで軸の位置を把握しており、負荷がかかってもその角度を保ちます。関節の角度を組み合わせて変えることで、グリッパーを狙った位置まで運びます。アームが行える独立した動きの数を自由度(DOF)と呼び、ふだんは単に「軸」と呼びます。

関節動き負荷おすすめのサーボ
ベース(腰)アームを左右に旋回させる持ち上げはしないが、アーム全体を支えるMG996R+ベアリング
肩アームを前後に上げ下げする最も負荷が大きい関節MG996Rまたは20 kg·cmクラスのサーボ
ひじ前腕を曲げる中〜高MG996R
手首(ピッチ)グリッパーの角度を決める低MG90S
手首の回転(ロール)グリッパーを軸まわりに回すごく低いMG90SまたはSG90
グリッパー物をつかんで離すつかむ力SG90またはMG90S

市販の「6軸ロボットアーム」キットの多くはサーボを6個使いますが、グリッパーは軸に数えないため、厳密には5自由度のアームにグリッパーが付いたものです。初めてのアームには4軸構成(ベース、肩、ひじ、グリッパー)が最適です。サーボ4個、ポテンショメーター4個、Arduino Unoがあれば足り、配線も単純で、トルク不足もめったに起きません。手首の関節は、グリッパーを水平に保ちたいときや、いろいろな角度から物をつかみたいときに役立ちます。サーボが5〜6個になると、PCA9685ドライバはほぼ必須になります。

部品リスト

部品4軸アームサーボ6個のアームメモ
コントローラーArduino UnoまたはNanoArduino Uno、NanoまたはESP32NanoはA6とA7の入力が多い分、ポテンショメーターを増やせる
サーボモーターSG90またはMG90S×4MG996R×3+MG90S×3下のトルク計算で選んだサーボを確認する
サーボドライバ不要PCA9685 16チャンネルモジュールI2Cで信号線は2本だけ
電源5 V 3 AのACアダプター5〜6 VのACアダプター、5 A以上サーボは絶対にArduinoから給電しない
コンデンサ470〜1000 µF、16 V1000〜2200 µF、16 Vサーボの電源ラインに入れる。極性に注意
操作部10 kΩポテンショメーター×4ジョイスティックモジュール×2またはHC-05両方使ってもよい
ボタン2個(記録、再生)ジョイスティックのボタンで足りるINPUT_PULLUPなら追加の抵抗は不要
機構部品3 mmのMDFまたは3Dプリント部品4 mmのMDF、3Dプリントまたはアルミブラケット608ベアリング、M3ボルト、ゆるみ止めナット

機構設計:MDFか3Dプリントか

材料長所短所向いている用途
3〜4 mmのMDF(レーザーカット)安くて軽く、ねじ止めも接着も簡単湿気に弱く、ねじ穴が時間とともに広がるMeArm風の最初のアーム、学校のプロジェクト
3 mmのアクリル見た目がよく、湿気の影響を受けない割れやすく、ねじを締めるとひびが入ることがある展示用・見せる作品
3Dプリント(PLA / PETG)複雑な形やサーボにぴったりのポケットを作れる印刷に時間がかかる。PLAは熱や常時かかる負荷で変形(クリープ)する自分で設計するアーム。負荷のかかる部品はPETG
アルミブラケットキット非常に頑丈で、MG996R用の既製部品がある重くて高価。肩への負荷が増える重い物を持ち上げるサーボ6個のアーム
段ボール、アイスの棒ほぼ無料で、試作が速い強度が低く、関節にガタが出る寸法の確認、子どもとの初めての工作
  • アームは短く軽く:肩にかかる負荷は距離に比例して大きくなります。アームを30 cmではなく24 cmにすると、肩のトルクは約5分の1減ります。
  • 重いサーボはベースの近くに:ひじのサーボを肩の位置まで下げ、プッシュロッド(平行リンク)でひじを動かすのは、MeArmなどのアームで使われている賢い工夫です。肩がそのサーボの重さを持ち上げなくて済みます。
  • ベースにはベアリングを:アーム全体の重さをベースサーボの軸にぶら下げないでください。608ベアリング(内径8 mm、外径22 mm、厚さ7 mm。スケートボードでおなじみのベアリング)や回転台用ベアリング(ターンテーブル)が荷重を受け、サーボは回すだけになります。
  • 関節は両側で支える:サーボがホーン側で関節を支えている間、反対側にはM3ボルトとゆるみ止めナットで回転軸を作ります。こうするとサーボの軸に曲げの力がかからず、ギアの寿命がずっと長くなります。
  • 金属ホーンを使う:MG996Rにはそれぞれ25歯(25T)の金属製丸ホーンを取り付け、中央のねじで軸に固定します。プラスチックのホーンは負荷の大きい関節で滑り、スプライン(歯)がつぶれてしまいます。
  • グリッパーの指にゴムを:指先に薄いゴムを貼ると、少ない力でも滑らずに物をつかめます。

サーボ選び:SG90、MG90S、MG996Rのどれにする?

型番トルク速度(60°あたり)重さギアストール電流ロボットアームでの役割
SG901.8 kg·cm (4.8 V)0.10 s (4.8 V)9 gプラスチック約0.6〜0.75 Aグリッパー、軽い手首、小型の4軸アーム
MG90S1.8 kg·cm (4.8 V), 2.2 kg·cm (6 V)0.10 s (4.8 V)13.4 g金属約0.7〜1 A手首、手首の回転、グリッパー
MG996R9.4 kg·cm (4.8 V), 11 kg·cm (6 V)0.17 s (4.8 V)55 g金属2.5 A (6 V)ベース、肩、ひじ
DS321819 kg·cm (5 V), 21.5 kg·cm (6.8 V)0.16 s (5 V)60 g金属約2〜3 A長いアームや重いアームの肩

表のトルクはストールトルクです。これは軸が止まる直前にサーボが出せる最大の力で、連続して使える値ではありません。サーボはこの限界に近づくほど電流がストール電流に向かって増え、発熱して震え始めます。実用上のルールは、関節に必要なトルクをサーボのストールトルクの半分以下に収めることです。電圧も効きます。MG996Rは4.8 Vで9.4 kg·cm、6 Vで11 kg·cmを出すので、同じサーボでも電源電圧だけで約17%強くなります。そのためMG996Rのアームには6 V電源が理にかなっていますが、SG90とMG90Sにとって6 Vは上限です。MG996Rの配線と限界についてはMG996Rサーボモーターのガイドも参考にしてください。

MG996Rメタルギアサーボモーター
MG996R:金属ギア、6 Vで11 kg·cmのトルク、25歯の出力スプライン。ロボットアームのベース、肩、ひじの定番です。

トルク計算:肩のサーボはこのアームを持ち上げられるか

サーボが持ち上げなければならないトルクは、各部品の質量に、関節の軸からの水平距離を掛けた値の合計です。トルク(kg·cm)=Σ質量(kg)×距離(cm)。最も厳しいのはアームを水平に伸ばしたときで、すべての距離が最大になります。アームが真上を向いていれば、このてこの長さはほぼゼロになります。ニュートンメートルに換算するには0.098を掛けます(1 kg·cm ≈ 0.098 N·m)。例として、肩からひじまで12 cm、ひじから手首まで12 cm、手首からグリッパーの先まで6 cm、ひじにMG996Rを使った卓上アームで、グリッパーが50 gの物を持っているときの肩の負荷を計算してみましょう。

部品質量肩の軸からの距離トルク
上腕の部品(MDFとボルト)40 g6 cm(重心)0.04 × 6 = 0.24 kg·cm
ひじのサーボ(MG996R)55 g12 cm0.055 × 12 = 0.66 kg·cm
前腕の部品30 g18 cm0.03 × 18 = 0.54 kg·cm
手首のサーボ+グリッパー(MG90S、SG90と部品)50 g25 cm0.05 × 25 = 1.25 kg·cm
つかんでいる物50 g30 cm0.05 × 30 = 1.50 kg·cm
合計225 g—4.19 kg·cm(≈ 0.41 N·m)

静止しているだけで、このアームの肩には4.19 kg·cmが必要です。加速やブレーキで生じる余分な負荷をまかない、サーボをストールから十分に遠ざけるために2倍します。つまり8.4 kg·cmです。したがって肩には最低でもMG996R(4.8 Vで9.4 kg·cm、6 Vで11 kg·cm)が必要で、SG90やMG90Sの1.8〜2.2 kg·cmではこのアームはびくともしません。ひじについて同じ計算をすると、前腕(0.03 × 6)、手首とグリッパー(0.05 × 13)、物(0.05 × 18)の合計は1.73 kg·cm、安全率を掛けると3.5 kg·cmなので、ひじにもMG996Rが必要です。手首に残るのはグリッパーと物だけなので負荷は約0.4 kg·cmで、MG90Sで十分です。同じ計算で、アームが運べる最も重い物もわかります。物の質量を増やして合計を計算し直し、サーボのトルクの半分を超えないようにします。

肩のトルクを減らす安上がりな方法は三つあります。アームを短くすること、ひじのサーボを肩に移してプッシュロッドでひじを動かすこと、そして肩の後ろにカウンターウェイトを付けるか、アームを上に引くばねを付けることです。たとえば肩の軸の5 cm後ろに400 gのおもりを置くと、0.4 × 5 = 2 kg·cmの逆向きのトルクが生まれ、上で計算したアームの負荷はほぼ半分になります。その分、ベースのベアリングが支える重さは少し増えます。

電源:サーボをArduinoから給電しない理由

ロボットアームのトラブルの大半は電源が原因です。MG996Rは動いている間に0.5〜0.9 A、ストール時には最大2.5 Aを流します。小さなSG90でもストール時には0.6 Aを超えることがあります。Arduino UnoをUSBで動かしている場合、5Vピンから取り出せる電流の合計はUSBの上限である約500 mAで、その一部はボード自身が使います。大きなサーボ3個が同時に動き出すと電圧が落ち込み、マイコンがリセットされて、アームは初期姿勢に跳ね戻ります。対策は、サーボを別の5〜6 V電源から給電し、すべてのGNDをつなぐことです。Arduinoは引き続きUSBや専用のACアダプターから給電して構いません。電源の選択肢はArduinoの電源のガイドを、電池で動かすアームの稼働時間の目安は電池寿命計算ツールを使ってみてください。

アームの種類推定電流おすすめの電源
SG90またはMG90S×4の小型アーム動作中0.5〜1 A、ピーク2〜3 A5 V 3 AのACアダプターまたはLM2596モジュール(最大3 A、放熱板なしで約2 A)
MG996R×3+MG90S×3のサーボ6個のアーム動作中2〜3 A、全部ストールすると8〜10 A5〜6 VのスイッチングACアダプター、5 A以上(余裕を持たせるなら10 A)
持ち運べるアームアームによる2S LiPo(7.4 V)+6 Vに設定した5 A以上の降圧モジュール(UBEC)
避けるべき電源—9 Vの角形電池(電流を出せない)、Arduinoの5Vピン、MG996Rのアームに単3アルカリ電池4本
サーボは絶対にArduinoの5Vピンから給電せず、外部電源のマイナスは必ずArduinoのGNDにつないでください。共通GNDがないと、サーボは信号を読み取れずにでたらめに動きます。サーボの電源ラインには、サーボのすぐそばに電解コンデンサを入れます。容量はサーボ1個あたり100 µF以上(サーボ6個のアームなら1000〜2200 µF、16 V以上)で、長い足をプラスにします。電解コンデンサを逆向きに付けると、膨らんで破裂することがあります。ブレッドボードの電源ラインは約1 Aを想定して作られているので、MG996Rのアームでは電源の線をねじ端子やPCA9685のV+入力に直接つないでください。

4軸アームの配線

下の表は、ポテンショメーターで操作する4軸アームのArduino UnoとRaspberry Pi Picoの配線です。このガイドのコードはこのピンを使います。UnoのServoライブラリはどのデジタルピンでもサーボを動かせますが、Timer1を使うため、サーボを接続している間はピン9と10でanalogWriteが使えなくなります。ボードのピン配置はピン配置ツールで確認できます。

接続Arduino UnoRaspberry Pi Pico
ベースサーボの信号(オレンジの線)D3GP10
肩サーボの信号D5GP11
ひじサーボの信号D6GP12
グリッパーサーボの信号D9GP13
ポテンショメーター1〜3の中央端子(ベース、肩、ひじ)A0, A1, A2GP26, GP27, GP28
ポテンショメーター4の中央端子(グリッパー)A3なし。グリッパーはGP14のボタンで操作
ポテンショメーターの両端の足5VとGND3V3とGND
記録ボタンと再生ボタン(反対側はGNDへ)D7とD8—
サーボの赤い線(+)外部5〜6 V電源のプラス外部5〜6 V電源のプラス
サーボの茶色の線(−)外部電源のマイナス+ArduinoのGND外部電源のマイナス+PicoのGND
10 kΩの回転式ポテンショメーター
10 kΩポテンショメーター:両端の足は5VとGNDへ、中央の足(ワイパー)はアナログ入力へつなぎます。軸を回すと、関節も同じ割合だけ回ります。

組み立て前のキャリブレーション:サーボを中央に合わせる

  1. ホーンを取り付ける前に、下のコードを書き込みます。4個のサーボが順番に90度へ動き、そこで待機します。
  2. アームを中立の姿勢(たとえば上腕は垂直、前腕は水平、グリッパーは半開き)に保ち、各ホーンをその姿勢にいちばん近いスプラインの歯に差し込んで、中央のねじで固定します。
  3. コードのCENTER_ONLYをfalseにし、TESTで関節を一つ選んで、A0のポテンショメーターをゆっくり回します。
  4. シリアルモニタで角度を見ながら、関節がどこにもぶつからず、ケーブルも引っ張らずに届く最小と最大の角度を書き留めます。
  5. グリッパーの閉じ角度は、指がちょうど触れる位置に設定します。さらに強く握ろうとし続けるサーボはストールしたままになり、発熱します。
  6. 求めた値を、以降のスケッチのMIN_AとMAX_Aの配列に入力します。
C++ (Arduino IDE)
1#include <Servo.h>
2
3const byte SERVO_PIN[4] = {3, 5, 6, 9};   // taban, omuz, dirsek, kıskaç / base, shoulder, elbow, gripper
4const byte TEST = 1;                       // denenecek servo (0–3) / servo under test (0–3)
5const bool CENTER_ONLY = true;             // true: hepsi 90°'de bekler / true: all wait at 90°
6
7Servo servo[4];
8
9void setup() {
10  Serial.begin(9600);
11  for (byte i = 0; i < 4; i++) {
12    servo[i].write(90);                    // önce açıyı ver / set the angle first
13    servo[i].attach(SERVO_PIN[i]);         // sonra bağla: zıplamaz / then attach: no jump
14    delay(300);                            // akım darbelerini ayır / spread the current surges
15  }
16}
17
18void loop() {
19  if (CENTER_ONLY) return;                 // korna takarken burada kal / stay here while fitting horns
20  int angle = map(analogRead(A0), 0, 1023, 0, 180);   // A0'daki pot / pot on A0
21  servo[TEST].write(angle);
22  Serial.print(F("Servo "));
23  Serial.print(TEST);
24  Serial.print(F(" aci / angle: "));
25  Serial.println(angle);                   // sınırları buradan oku / read the limits here
26  delay(50);
27}

このコードには小さいけれど大切な工夫があります。attach()の前にservo.write(90)を呼んでいるので、サーボは最初のパルスから指定の角度を受け取ります。順番を逆にすると、ライブラリはまずデフォルトの1500 µsのパルス(約90°)を送ります。開始角度が90でなければ、電源投入時にアームがいったん中央へ跳ね、そこから目標へ動きます。サーボを300 msずつ間をあけて一つずつ起動しているのは、電源投入時の突入電流を分散させるためでもあります。

ポテンショメーターでロボットアームを操作する

いちばん直感的な操作方法は、関節ごとにポテンショメーターを一つずつ使うやり方です。ポテンショメーターの軸がどの角度にあっても関節が同じ角度へ動くので、位置制御になります。ポテンショメーターをアームの小さな縮尺模型(段ボールや3Dプリントで、同じ比率)に組み込めば、模型を手で動かすと大きなアームがその動きを1対1でまねします。ただし、アナログの読み取り値はもともと数カウントほど揺れ、その揺れがサーボの震えとして現れます。コードにはその対策が二つ入っています。

C++ (Arduino IDE)
1#include <Servo.h>
2
3const byte SERVO_PIN[4] = {3, 5, 6, 9};        // taban, omuz, dirsek, kıskaç / base, shoulder, elbow, gripper
4const byte POT_PIN[4]   = {A0, A1, A2, A3};    // her eklem için bir pot / one pot per joint
5const byte MIN_A[4] = {0, 15, 20, 40};         // kalibrasyonda bulduğun alt sınırlar / lower limits from calibration
6const byte MAX_A[4] = {180, 165, 160, 110};    // üst sınırlar / upper limits
7
8Servo servo[4];
9float smooth[4];                               // süzülmüş pot değeri / filtered pot value
10int lastAngle[4];                              // son yazılan açı / last written angle
11
12void setup() {
13  for (byte i = 0; i < 4; i++) {
14    smooth[i] = analogRead(POT_PIN[i]);
15    lastAngle[i] = map((int)smooth[i], 0, 1023, MIN_A[i], MAX_A[i]);
16    servo[i].write(lastAngle[i]);              // önce açı, sonra attach / angle first, then attach
17    servo[i].attach(SERVO_PIN[i]);
18    delay(300);                                // servolar sırayla uyansın / wake the servos one by one
19  }
20}
21
22void loop() {
23  for (byte i = 0; i < 4; i++) {
24    smooth[i] += (analogRead(POT_PIN[i]) - smooth[i]) * 0.2;   // yumuşatma filtresi / low-pass filter
25    int angle = map((int)smooth[i], 0, 1023, MIN_A[i], MAX_A[i]);
26    if (abs(angle - lastAngle[i]) >= 2) {      // ölü bant: küçük gürültüyü yok say / deadband: ignore small noise
27      servo[i].write(angle);
28      lastAngle[i] = angle;
29    }
30  }
31  delay(15);
32}

smooth配列はシンプルなローパスフィルタです。ループごとに新しい読み取り値の20%だけを加えるので、急な跳びはなめらかになりつつ、アームは素早く反応します。0.2を0.1に変えると動きはさらになめらかになりますが、遅れが大きくなります。二つ目の対策は不感帯(デッドバンド)です。新しい角度が最後に書き込んだ角度と2度以上違わない限りサーボには何も送らないので、アームが止まっているときの震えがなくなります。map()がポテンショメーターの範囲を各関節の安全な限界へ直接変換するので、ポテンショメーターをいっぱいまで回しても関節が機械的なストッパーにぶつかることはありません。

動きを記録して再生する

ロボットアームを本当の意味でロボットにするのは、教えた動きを自分で繰り返すことです。方法は簡単です。ポテンショメーターでアームをある姿勢に動かして記録ボタンを押すと、Arduinoが4個の関節すべての角度を保存します。いくつかの姿勢を記録したら再生ボタンを押します。アームはそれらを順番に繰り返し、姿勢と姿勢の間をなめらかにつなぎます。姿勢はArduino Unoの1024バイトのEEPROMに書き込まれるので、電源を切っても消えません。姿勢一つで4バイトなので、100姿勢を記録しても401バイトしか使いません。

C++ (Arduino IDE)
1#include <Servo.h>
2#include <EEPROM.h>
3
4const byte SERVO_PIN[4] = {3, 5, 6, 9};        // taban, omuz, dirsek, kıskaç / base, shoulder, elbow, gripper
5const byte POT_PIN[4]   = {A0, A1, A2, A3};
6const byte MIN_A[4] = {0, 15, 20, 40};         // güvenli sınırlar / safe limits
7const byte MAX_A[4] = {180, 165, 160, 110};
8const byte BTN_REC = 7, BTN_PLAY = 8, LED = 13; // butonların diğer ucu GND'de / buttons go to GND
9const byte MAX_STEPS = 100;                    // 100 adım x 4 bayt = 400 bayt / 100 steps x 4 bytes
10
11Servo servo[4];
12float smooth[4];                               // süzülmüş pot değeri / filtered pot value
13byte pos[4];                                   // servoların şu anki açısı / current servo angles
14byte stepCount;                                // kayıtlı adım sayısı / number of saved steps
15
16bool pressed(byte pin) {                       // basılıyken true / true while pressed
17  return digitalRead(pin) == LOW;
18}
19
20byte potAngle(byte i) {
21  smooth[i] += (analogRead(POT_PIN[i]) - smooth[i]) * 0.2;   // yumuşatma / smoothing
22  return map((int)smooth[i], 0, 1023, MIN_A[i], MAX_A[i]);
23}
24
25void blink(byte n) {
26  for (byte k = 0; k < n; k++) {
27    digitalWrite(LED, HIGH); delay(80);
28    digitalWrite(LED, LOW);  delay(80);
29  }
30}
31
32// Tüm eklemleri aynı anda hedefe götürür / moves all joints to the target together
33void moveTo(const byte target[4], byte msPerDegree) {
34  byte start[4];
35  int steps = 0;
36  for (byte i = 0; i < 4; i++) {
37    start[i] = pos[i];
38    int d = abs((int)target[i] - (int)start[i]);
39    if (d > steps) steps = d;                  // en uzun hareket / longest move
40  }
41  for (int s = 1; s <= steps; s++) {
42    for (byte i = 0; i < 4; i++) {
43      pos[i] = start[i] + (long)((int)target[i] - (int)start[i]) * s / steps;   // ara açı / in-between angle
44      servo[i].write(pos[i]);
45    }
46    delay(msPerDegree);
47  }
48}
49
50void saveStep() {
51  if (stepCount >= MAX_STEPS) { blink(3); return; }           // hafıza dolu / memory full
52  for (byte i = 0; i < 4; i++) EEPROM.update(1 + stepCount * 4 + i, pos[i]);
53  stepCount++;
54  EEPROM.update(0, stepCount);                 // 0. bayt adım sayısını tutar / byte 0 holds the step count
55  blink(1);
56}
57
58void playBack() {
59  byte target[4];
60  for (byte s = 0; s < stepCount; s++) {
61    for (byte i = 0; i < 4; i++) target[i] = EEPROM.read(1 + s * 4 + i);
62    moveTo(target, 12);                        // derece başına 12 ms / 12 ms per degree
63    delay(400);                                // her pozda kısa bekleme / short pause at each pose
64    if (pressed(BTN_PLAY)) break;              // oynatırken basınca durur / press again to stop
65  }
66}
67
68void syncToPots() {                            // potların konumuna yumuşakça dön / ease back to the pots
69  byte target[4];
70  for (byte i = 0; i < 4; i++) {
71    smooth[i] = analogRead(POT_PIN[i]);
72    target[i] = map((int)smooth[i], 0, 1023, MIN_A[i], MAX_A[i]);
73  }
74  moveTo(target, 12);
75}
76
77void setup() {
78  pinMode(BTN_REC, INPUT_PULLUP);
79  pinMode(BTN_PLAY, INPUT_PULLUP);
80  pinMode(LED, OUTPUT);
81  stepCount = EEPROM.read(0);
82  if (stepCount > MAX_STEPS) stepCount = 0;    // boş EEPROM 255 okur / a blank EEPROM reads 255
83  for (byte i = 0; i < 4; i++) {
84    smooth[i] = analogRead(POT_PIN[i]);
85    pos[i] = potAngle(i);
86    servo[i].write(pos[i]);                    // önce açı, sonra attach / angle first, then attach
87    servo[i].attach(SERVO_PIN[i]);
88    delay(300);
89  }
90}
91
92void loop() {
93  // Kayıt: kısa bas = pozu kaydet, 2 sn basılı tut = hepsini sil / Record: short press = save pose, hold 2 s = erase all
94  if (pressed(BTN_REC)) {
95    unsigned long t0 = millis();
96    while (pressed(BTN_REC) && millis() - t0 < 2000) {}
97    if (millis() - t0 >= 2000) {
98      stepCount = 0;
99      EEPROM.update(0, 0);
100      blink(5);
101    } else {
102      saveStep();
103    }
104    while (pressed(BTN_REC)) {}                // bırakılmasını bekle / wait for release
105    delay(50);                                 // buton arkı / debounce
106  }
107
108  // Oynat: kaydedilen pozları sırayla tekrarla / play: repeat the saved poses in order
109  if (pressed(BTN_PLAY) && stepCount > 0) {
110    while (pressed(BTN_PLAY)) {}
111    delay(50);
112    digitalWrite(LED, HIGH);
113    playBack();
114    while (pressed(BTN_PLAY)) {}
115    syncToPots();
116    digitalWrite(LED, LOW);
117  }
118
119  // Normal mod: kolu potlarla sür / normal mode: drive the arm with the pots
120  for (byte i = 0; i < 4; i++) {
121    byte a = potAngle(i);
122    if (abs((int)a - (int)pos[i]) >= 2) {
123      pos[i] = a;
124      servo[i].write(a);
125    }
126  }
127  delay(15);
128}
操作やることアームの動作
姿勢を保存記録ボタン(D7)を短く押すLEDが1回点滅し、姿勢がEEPROMに書き込まれる
記録を消去記録ボタンを2秒間押し続けるLEDが5回点滅し、ステップ数がリセットされる
再生再生ボタン(D8)を押して離すLEDが点灯し、姿勢がなめらかなつなぎで順番に再生される
再生を止める再生中に再生ボタンを押し続けるアームは今の姿勢を終えてから止まる
通常モードに戻る再生が終わったら何もしないアームはポテンショメーターが示す位置へゆっくり戻り、LEDが消える

moveTo()関数はこのスケッチの心臓部です。移動量がいちばん大きい関節を見つけ、その数だけ動きをステップに分け、ステップごとにすべての関節を比例して進めます。こうするとすべての関節が同時に目標へ到着し、関節を一つずつカクカク動かすロボットらしい動きではなく、流れるような動きになります。1度あたり12 msなので、90度の移動には約1.1秒かかります。速く再生したい場合はこの値を小さくできますが、MG996Rは無負荷でも60度回るのに0.17秒、1度あたり約3 msかかります。それより小さくするとサーボが指令に追いつけず、動きがぎくしゃくします。

EEPROMの各セルは約10万回書き換えられます。コードはEEPROM.update()を使っており、値が変わったセルだけを書き込むので、実際に寿命を使い切ることはまずありません。もっと長い動きのシーケンスが必要なら、記録をSDカードに保存するか、Arduino Megaの4 KBのEEPROMを使えます。

PCA9685サーボドライバ:サーボ6個と震えのない信号

サーボが5〜6個になったり、Bluetoothなどのモジュールを追加したりするなら、PCA9685モジュールを使うとずっと楽になります。このボードは16チャンネル・12ビットのPWM発生器で、I2Cを使ってたった2本の線でArduinoと通信し、プロセッサとは独立に自分のクロックでサーボのパルスを作ります。デフォルトのアドレスは0x40で、A0〜A5のはんだジャンパーでアドレスを変えれば最大62枚、つまりサーボ992個までつなげられます。50 Hzでは20 msの周期が4096ステップに分かれるので、1ステップは約4.9 µsです。544〜2400 µsの範囲は約380ステップになり、分解能はおよそ0.5度です。

PCA9685のピンArduino UnoESP32Raspberry Pi Pico
GNDGNDGNDGND
VCC(チップの電源のみ)5V3V33V3
SDAA4GPIO 21GP4
SCLA5GPIO 22GP5
V+端子(サーボの電源)外部5〜6 V外部5〜6 V外部5〜6 V
OE未接続のまま未接続のまま未接続のまま
サーボ用ヘッダー0〜5ベース、肩、ひじ、手首、手首の回転、グリッパー同じ順番同じ順番
PCA9685には電源が二つあります。VCCはチップだけに給電し、サーボはねじ端子のV+入力から給電されます。V+に電源が来ていないと、ボードはI2Cでは見えるのにサーボが動きません。Arduino UnoではA4とA5をI2Cに使うので、PCA9685をつなぐとアナログ入力として残るのはA0〜A3だけです。ポテンショメーターを増やしたいなら、A6とA7の入力が多いArduino Nanoを使ってください。

ジョイスティックでロボットアームを操作する

ジョイスティックはポテンショメーターとは動きが違い、手を離すとばねで中央に戻ります。角度をジョイスティックの位置に直接結びつけると、手を離した瞬間に関節が90度へ跳ねてしまいます。正しいのは速度制御です。ジョイスティックを大きく倒すほど関節が速く回り、手を離すと関節はその場にとどまります。下のコードでは2本のジョイスティックで4個の関節を動かします(左のジョイスティックでベースと肩、右のジョイスティックでひじと手首)。左のジョイスティックのボタンでグリッパーを開閉し、右のボタンでアームをホーム姿勢に戻します。左のジョイスティックのVRxとVRyの出力をA0とA1に、右のジョイスティックのVRyとVRxの出力をA2とA3に、SWピンをD2とD4につなぎます。ジョイスティックはArduinoの5Vピンから給電して構いません。コードをコンパイルするには、ライブラリマネージャでAdafruit PWM Servo Driver Libraryをインストールし、依存ライブラリのAdafruit BusIOも一緒にインストールします。

2軸アナログジョイスティックモジュール
ジョイスティックモジュール:VRxとVRyの出力は静止時に約512を示し、SWボタンは押すとGNDに落ちます。モジュール2個でロボットアームの関節4個を動かせます。
C++ (Arduino IDE)
1#include <Wire.h>
2#include <Adafruit_PWMServoDriver.h>
3
4Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(0x40);   // varsayılan adres / default address
5
6// Kanal: 0 taban, 1 omuz, 2 dirsek, 3 bilek, 4 bilek dönüşü, 5 kıskaç / Channel: 0 base, 1 shoulder, 2 elbow, 3 wrist, 4 wrist roll, 5 gripper
7const byte N = 6;
8const float MIN_A[N] = {0, 15, 20, 10, 0, 40};        // güvenli sınırlar / safe limits
9const float MAX_A[N] = {180, 165, 160, 170, 180, 110};
10const float HOME[N]  = {90, 90, 90, 90, 90, 40};      // başlangıç pozu, kıskaç açık / home pose, gripper open
11const byte JOY[4] = {A0, A1, A2, A3};                  // J1 X, J1 Y, J2 Y, J2 X → kanal / channel 0, 1, 2, 3
12const int8_t DIR[4] = {1, -1, 1, 1};                   // ters giden ekseni -1 yap / set -1 to invert an axis
13const byte BTN_GRIP = 2, BTN_HOME = 4;                 // joystick SW pinleri / joystick SW pins
14const float MAX_SPEED = 90;                            // en yüksek hız, derece/sn / top speed, degrees/s
15const float DT = 0.02;                                 // 20 ms döngü / 20 ms loop
16const int DEADZONE = 60;                               // ortadaki ölü bölge / dead zone around center
17
18float target[N], current[N];
19bool gripClosed = false;
20
21void writeServo(byte ch, float a) {
22  // Servo.h ile aynı 544–2400 µs aralığı / same 544–2400 µs range as Servo.h
23  pca.writeMicroseconds(ch, (uint16_t)(544 + a * (2400 - 544) / 180.0));
24}
25
26bool newPress(byte pin, byte k) {                      // her basışta bir kez true / true once per press
27  static bool prev[2] = {HIGH, HIGH};
28  bool now = digitalRead(pin);
29  bool edge = (prev[k] == HIGH && now == LOW);
30  prev[k] = now;
31  return edge;
32}
33
34void setup() {
35  pinMode(BTN_GRIP, INPUT_PULLUP);
36  pinMode(BTN_HOME, INPUT_PULLUP);
37  pca.begin();
38  pca.setOscillatorFrequency(27000000);                // çoğu modülde ~27 MHz / ~27 MHz on most boards
39  pca.setPWMFreq(50);                                  // servolar için 50 Hz / 50 Hz for servos
40  Wire.setClock(400000);                               // hızlı I2C / fast I2C
41  for (byte i = 0; i < N; i++) {
42    target[i] = current[i] = HOME[i];
43    writeServo(i, HOME[i]);
44    delay(300);                                        // servolar sırayla uyansın / wake servos one by one
45  }
46}
47
48void loop() {
49  static unsigned long last = 0;
50  if (millis() - last < 20) return;                    // 50 Hz güncelleme / 50 Hz update
51  last = millis();
52
53  // Joystick hızı belirler, konumu değil / the joystick sets speed, not position
54  for (byte i = 0; i < 4; i++) {
55    int v = analogRead(JOY[i]) - 512;
56    if (abs(v) > DEADZONE) {
57      target[i] += DIR[i] * (v / 512.0) * MAX_SPEED * DT;
58      target[i] = constrain(target[i], MIN_A[i], MAX_A[i]);
59    }
60  }
61  if (newPress(BTN_GRIP, 0)) {                         // kıskaç aç/kapa / toggle gripper
62    gripClosed = !gripClosed;
63    target[5] = gripClosed ? MAX_A[5] : MIN_A[5];
64  }
65  if (newPress(BTN_HOME, 1)) {                         // başlangıç pozuna dön / go to home pose
66    for (byte i = 0; i < N; i++) target[i] = HOME[i];
67    gripClosed = false;
68  }
69
70  // Her eklem hedefe hız sınırıyla gider / each joint follows its target with a speed limit
71  for (byte i = 0; i < N; i++) {
72    float d = constrain(target[i] - current[i], -MAX_SPEED * DT, MAX_SPEED * DT);
73    if (d != 0) {
74      current[i] += d;
75      writeServo(i, current[i]);
76    }
77  }
78}

DEADZONEは、中央で490〜530のような小さなずれが読み取られても、アームが勝手にじわじわ動かないようにします。MAX_SPEEDはジョイスティックをいっぱいまで倒したときの速度(1秒あたりの度数)を決めます。最初は毎秒90度が安全な値です。関節がジョイスティックと逆に動く場合は、配線を入れ替えずにDIR配列の該当する値を-1にしてください。コードは関節ごとに目標位置(target)と実際の位置(current)を持っています。実際の位置は速度制限付きで目標に近づくので、ホームボタンを押したときもアームはなめらかに戻ります。このスケッチでは手首の回転は90度で固定です。Bluetoothの章のRコマンドで操作できます。この止まらないタイミング処理の手法はmillis()とdelay()の違いで詳しく解説しています。

スマートフォンからBluetoothで操作する(HC-05)

HC-05モジュールは、スマートフォンとArduinoの間に無線のシリアル通信路を作ります。TXDピンはArduinoのD10へ、RXDピンは分圧回路を通してD11へつなぎます。D11 → 1 kΩ → RXD → 2 kΩ → GNDです。分圧回路はArduinoの5 Vの信号を、HC-05が想定する約3.3 Vまで下げます。モジュールのデフォルトの通信速度は9600ボーで、ペアリングのPINはたいてい1234です。スマートフォンからはシンプルな文字のコマンドを送ります。英字で関節を選び、続く数字で角度を指定し、改行かセミコロンでコマンドを終えます。なお、日本では技適マークのない無線モジュールの使用は電波法で制限されています。

HC-05 Bluetoothシリアルモジュール
HC-05:クラシックBluetoothでスマートフォンとペアリングするシリアルモジュールです。VCCには5 Vを入れますが、RXD入力は3.3 Vのロジックで動きます。
コマンド意味例
B+角度ベースB90
S+角度肩S120
E+角度ひじE45
W+角度手首(ピッチ)W100
R+角度手首の回転R0
G+角度グリッパーG110(閉じる)、G40(開く)
Hホーム姿勢に戻るH
K / P / X / C姿勢を保存/ループ再生/停止/姿勢を消去K
C++ (Arduino IDE)
1#include <Wire.h>
2#include <SoftwareSerial.h>
3#include <Adafruit_PWMServoDriver.h>
4
5SoftwareSerial bt(10, 11);                 // D10 ← HC-05 TXD, D11 → bölücü / divider → HC-05 RXD
6Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(0x40);
7
8const byte N = 6;
9const char NAME[N] = {'B', 'S', 'E', 'W', 'R', 'G'};   // taban, omuz, dirsek, bilek, dönüş, kıskaç / base, shoulder, elbow, wrist, roll, gripper
10const byte MIN_A[N] = {0, 15, 20, 10, 0, 40};          // güvenli sınırlar / safe limits
11const byte MAX_A[N] = {180, 165, 160, 170, 180, 110};
12const byte HOME[N]  = {90, 90, 90, 90, 90, 40};
13const byte MAX_POSES = 30;
14const float SPEED = 1.5;                   // 20 ms'de en fazla 1,5° (75°/sn) / max 1.5° per 20 ms (75°/s)
15
16float current[N];
17byte target[N];
18byte poses[MAX_POSES][N];                  // kaydedilen pozlar (RAM) / recorded poses (RAM)
19byte poseCount = 0, playIndex = 0;
20bool playing = false;
21char buf[12];
22byte len = 0;
23
24void writeServo(byte ch, float a) {
25  pca.writeMicroseconds(ch, (uint16_t)(544 + a * (2400 - 544) / 180.0));   // 544–2400 µs
26}
27
28void handle(char *cmd) {                   // ör. / e.g. "S120", "H", "K", "P"
29  char c = toupper(cmd[0]);
30  if (c == 'H') {                          // başlangıç pozu / home pose
31    for (byte i = 0; i < N; i++) target[i] = HOME[i];
32    playing = false;
33  } else if (c == 'K') {                   // pozu kaydet / save pose
34    if (poseCount < MAX_POSES) {
35      for (byte i = 0; i < N; i++) poses[poseCount][i] = target[i];
36      poseCount++;
37    }
38    bt.print(F("Poz / pose: "));
39    bt.println(poseCount);
40  } else if (c == 'P') {                   // döngüyle oynat / play in a loop
41    if (poseCount > 0) { playing = true; playIndex = 0; }
42  } else if (c == 'X') {                   // durdur / stop
43    playing = false;
44  } else if (c == 'C') {                   // kayıtları sil / clear poses
45    poseCount = 0;
46    playing = false;
47  } else {
48    for (byte i = 0; i < N; i++) {
49      if (c == NAME[i]) {
50        target[i] = constrain(atoi(cmd + 1), MIN_A[i], MAX_A[i]);
51        playing = false;                   // elle komut oynatmayı keser / a manual command stops playback
52      }
53    }
54  }
55}
56
57bool arrived() {
58  for (byte i = 0; i < N; i++)
59    if (fabs(target[i] - current[i]) > 0.5) return false;
60  return true;
61}
62
63void setup() {
64  bt.begin(9600);                          // HC-05 varsayılan hızı / HC-05 default baud
65  pca.begin();
66  pca.setOscillatorFrequency(27000000);
67  pca.setPWMFreq(50);
68  for (byte i = 0; i < N; i++) {
69    target[i] = HOME[i];
70    current[i] = HOME[i];
71    writeServo(i, HOME[i]);
72    delay(300);                            // servolar sırayla uyansın / wake servos one by one
73  }
74}
75
76void loop() {
77  // Komutları satır satır topla / collect commands line by line
78  while (bt.available()) {
79    char ch = bt.read();
80    if (ch == '\n' || ch == '\r' || ch == ';') {
81      if (len > 0) { buf[len] = '\0'; handle(buf); len = 0; }
82    } else if (len < sizeof(buf) - 1) {
83      buf[len++] = ch;
84    }
85  }
86
87  static unsigned long last = 0, arrivedAt = 0;
88  if (millis() - last < 20) return;        // 50 Hz güncelleme / 50 Hz update
89  last = millis();
90
91  if (playing) {
92    if (!arrived()) arrivedAt = millis();  // hareket sürüyor / still moving
93    else if (millis() - arrivedAt > 300) { // her pozda 300 ms bekle / hold each pose 300 ms
94      for (byte i = 0; i < N; i++) target[i] = poses[playIndex][i];
95      playIndex = (playIndex + 1) % poseCount;   // sona gelince başa dön / wrap around
96      arrivedAt = millis();
97    }
98  }
99
100  for (byte i = 0; i < N; i++) {
101    float d = constrain(target[i] - current[i], -SPEED, SPEED);   // hız sınırı / speed limit
102    if (d != 0) {
103      current[i] += d;
104      writeServo(i, current[i]);
105    }
106  }
107}

このスケッチでは、サーボをArduinoに直接ではなくPCA9685につないでいます。これには理由があります。SoftwareSerialは1バイト受信するたびに約1 msのあいだ割り込みを止めます。サーボをServoライブラリでArduinoのピンから動かしていると、その間にパルスが伸びてしまい、コマンドのたびにアームがぴくっと動きます。PCA9685はパルスを自分で作るので、Bluetoothの通信がどれだけ忙しくても信号はまったく揺れません。記録と再生もスマートフォンから行えます。スライダーでアームをある姿勢に動かしてKを送ります。いくつか姿勢を保存したら、Pでアームがそれをループで繰り返し、Xで止まります。このバージョンでは姿勢はRAMに保存されるので、ボードの電源を切ると消えます。残しておきたい場合は、前のスケッチのEEPROMの方法を追加してください。

スマートフォン側は既製のアプリで十分です。AndroidにBluetoothシリアルターミナルか、スライダー付きのBluetoothコントローラーアプリをインストールし、各スライダーに「S」のような接頭辞と行末の改行を設定します。ボタンやスライダーのある自分だけの画面を作りたい場合は、MIT App InventorでBluetoothアプリを作るガイドでステップごとに解説しています。HC-05はクラシックBluetoothを使うのでiPhoneでは動きません。iPhoneにはESP32 BLEのようなBluetooth Low Energyの仕組みが必要です。

ESP32で作るワイヤレスロボットアーム

ESP32はクラシックBluetoothを内蔵しているので、HC-05は不要です。BluetoothSerialライブラリを使うと、ボードはスマートフォンに「BlueGrays-RobotKol」として表示され、同じコマンドを理解します。PCA9685をGPIO 21(SDA)とGPIO 22(SCL)につなぎ、VCCは3V3から給電します。このときサーボの信号は3.3 Vで出ますが、ほとんどのサーボは問題なく読み取れます。BluetoothSerialはクラシックESP32(ESP32-WROOM-32、DevKit)でしか動きません。ESP32-S2、S3、C3はクラシックBluetoothに対応していません。

C++ (Arduino IDE)
1#include <Wire.h>
2#include <BluetoothSerial.h>
3#include <Adafruit_PWMServoDriver.h>
4
5BluetoothSerial bt;                        // klasik Bluetooth SPP / classic Bluetooth SPP
6Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(0x40);
7
8const int N = 6;
9const char NAME[N] = {'B', 'S', 'E', 'W', 'R', 'G'};   // taban, omuz, dirsek, bilek, dönüş, kıskaç / base, shoulder, elbow, wrist, roll, gripper
10const int MIN_A[N] = {0, 15, 20, 10, 0, 40};
11const int MAX_A[N] = {180, 165, 160, 170, 180, 110};
12const int HOME[N]  = {90, 90, 90, 90, 90, 40};
13float current[N], target[N];
14String line;
15
16void writeServo(int ch, float a) {
17  pca.writeMicroseconds(ch, (uint16_t)(544 + a * (2400 - 544) / 180.0));   // 544–2400 µs
18}
19
20void handle(String cmd) {                  // ör. / e.g. "E45", "H"
21  cmd.trim();
22  if (cmd.length() == 0) return;
23  char c = toupper(cmd[0]);
24  if (c == 'H') {
25    for (int i = 0; i < N; i++) target[i] = HOME[i];
26    return;
27  }
28  int value = cmd.substring(1).toInt();
29  for (int i = 0; i < N; i++)
30    if (c == NAME[i]) target[i] = constrain(value, MIN_A[i], MAX_A[i]);
31}
32
33void setup() {
34  Serial.begin(115200);
35  bt.begin("BlueGrays-RobotKol");          // telefonda görünen ad / name shown on the phone
36  Wire.begin(21, 22);                      // SDA = GPIO 21, SCL = GPIO 22
37  pca.begin();
38  pca.setOscillatorFrequency(27000000);
39  pca.setPWMFreq(50);
40  for (int i = 0; i < N; i++) {
41    current[i] = target[i] = HOME[i];
42    writeServo(i, HOME[i]);
43    delay(300);
44  }
45  Serial.println("Bluetooth hazir / ready");
46}
47
48void loop() {
49  while (bt.available()) {
50    char ch = bt.read();
51    if (ch == '\n' || ch == ';') { handle(line); line = ""; }
52    else if (line.length() < 12) line += ch;
53  }
54
55  static unsigned long last = 0;
56  if (millis() - last < 20) return;        // 50 Hz güncelleme / 50 Hz update
57  last = millis();
58  for (int i = 0; i < N; i++) {
59    float d = constrain(target[i] - current[i], -1.5f, 1.5f);   // yumuşak hareket / smooth motion
60    if (d != 0) {
61      current[i] += d;
62      writeServo(i, current[i]);
63    }
64  }
65}

Raspberry Pi Picoで作るロボットアーム(MicroPython)

Raspberry Pi Picoでサーボを動かすのに追加のライブラリはいりません。PWMを50 Hzに設定し、duty_ns()でパルス幅をナノ秒単位で指定するだけです。PicoにはPWMチャンネルが16あるので、サーボ6個を直接動かせます。同じスライスを共有するピンのペアは周波数も共有しますが、どのサーボも50 Hzを使うので問題ありません。制約はアナログ側にあります。Picoで使えるADCピンはGP26、GP27、GP28の三つだけです。そのため下のコードでは、三つの関節をポテンショメーターで動かし、グリッパーはGP14のボタンで開閉します。Picoは3.3 Vで動くので、ポテンショメーターは3V3につなぎます。サーボはこれまでと同じく外部の5 V電源から給電します。

MicroPython
1from machine import Pin, PWM, ADC
2import time
3
4# Servo pinleri: taban, omuz, dirsek, kıskaç / servo pins: base, shoulder, elbow, gripper
5servos = [PWM(Pin(p)) for p in (10, 11, 12, 13)]
6for s in servos:
7    s.freq(50)                                  # 50 Hz servo sinyali / 50 Hz servo signal
8
9pots = [ADC(26), ADC(27), ADC(28)]              # Pico'da 3 ADC pini var / the Pico has 3 ADC pins
10button = Pin(14, Pin.IN, Pin.PULL_UP)           # kıskaç butonu GND'ye / gripper button to GND
11
12MIN_A = (0, 15, 20, 40)                         # güvenli sınırlar / safe limits
13MAX_A = (180, 165, 160, 110)
14
15def write(i, angle):
16    angle = max(MIN_A[i], min(MAX_A[i], angle))
17    servos[i].duty_ns(544000 + int(angle * 10311))   # 544–2400 µs aralığı / 544–2400 µs range
18
19smooth = [p.read_u16() for p in pots]
20grip_closed = False
21last_btn = 1
22write(3, MIN_A[3])                              # kıskaç açık başlar / gripper starts open
23
24while True:
25    for i in range(3):
26        smooth[i] += (pots[i].read_u16() - smooth[i]) * 0.2   # yumuşatma / smoothing
27        write(i, MIN_A[i] + (MAX_A[i] - MIN_A[i]) * smooth[i] / 65535)
28    b = button.value()
29    if b == 0 and last_btn == 1:                # yeni basış / new press
30        grip_closed = not grip_closed
31        write(3, MAX_A[3] if grip_closed else MIN_A[3])
32    last_btn = b
33    time.sleep_ms(20)                           # 50 Hz döngü / 50 Hz loop

さらに先へ:逆運動学

ここまでは各関節の角度を一つずつ指定してきました。これを順運動学と呼びます。逆運動学はその逆で、グリッパーを届かせたい点(x, y)を与えると、コードが必要な関節の角度を計算します。肩〜ひじの長さがL1、ひじ〜手首の長さがL2の2リンクアームでは、まずD = (x² + y² − L1² − L2²) / (2·L1·L2)を求めます。ひじの角度はθ2 = acos(D)、肩の角度はθ1 = atan2(y, x) + atan2(L2·sin θ2, L1 + L2·cos θ2)です(ひじを上にした解)。たとえばL1 = L2 = 12 cmで、肩の15 cm前方・5 cm上の点なら、D ≈ −0.13、θ2 ≈ 97.6°、θ1 ≈ 67.2°になります。上腕は水平から67度上を向き、前腕は上腕に対して97.6度下へ曲がります。Dの絶対値が1より大きい場合、その点はアームの届く範囲の外です。ベースの角度は、目標を上から見た位置から別に計算します。結果をサーボの角度に変換するときは、各サーボの取り付け向きと中央位置を考慮してください。

震えとリセット:よくある間違いと対策

症状考えられる原因対策
アームが止まっているのにサーボが震えてジーと鳴るポテンショメーターの読み取り値のノイズ、または電源不足平滑化フィルタと2°の不感帯を入れ、外部電源と1000 µFのコンデンサを使う
アームが動くとArduinoがリセットされ、HC-05の接続が切れるサーボの起動電流で電圧が下がるサーボを別の5〜6 V電源から給電し、GNDをつなぎ、電源投入時はサーボを一つずつ起動する
Bluetoothのコマンドが届くたびにサーボがぴくっと動くSoftwareSerialの割り込みがServoライブラリのパルスを乱しているサーボをPCA9685に移すか、ESP32の内蔵Bluetoothを使う
電源投入時にアームがどこかへ勢いよく動くサーボは最後の位置を覚えておらず、最初の指令で全速力で動く電源を切る前にアームをホーム姿勢にしておき、attach()の前にwrite()を呼ぶ
肩がアームを持ち上げられない、またはゆっくり垂れ下がるトルク不足トルク計算をする。MG996R、短いアーム、カウンターウェイト、ばねを使う
関節が片方の端でストールして熱くなるサーボが機械的なストッパーを押し続けているキャリブレーションで求めたMIN_AとMAX_Aの限界をコードに入れる
物をつかんでいる間にグリッパーのサーボが熱くなるグリッパーが閉じようとし続け、サーボがストールしたままになっている閉じ角度を物にちょうど触れる位置にし、指にゴムを貼る
PCA9685はつながっているのにサーボが一つも動かないV+端子に電源が来ていない、またはGNDが共通になっていないV+に5〜6 Vを供給し、I2Cアドレス(0x40)を確認する
負荷がかかると関節が滑るプラスチックのホーンの歯がつぶれた、または中央のねじがない金属ホーンを使い、中央のねじで軸に固定する
原因が電源かコードかをすばやく見分ける方法があります。サーボをつながずにキャリブレーション用のスケッチ(CENTER_ONLY = false)を動かし、ポテンショメーターを回しながらシリアルモニタで角度を確認します。値が正しく見えるなら、問題は電源です。アームを動かしながらテスターでサーボの電源ラインの電圧を測り、4.5 Vを下回るなら、電源か配線の能力が足りていません。リセットの原因をすべて知りたい場合はArduinoが何度もリセットされるのはなぜ?を参照してください。

ロボットアームのプロジェクトアイデア

  • 色を仕分けるアーム:TCS3200カラーセンサーで立方体の色を読み取り、アームが色ごとに別の箱へ入れます。
  • ジェスチャー操作:手袋にMPU6050を付けると、ロボットアームが手首と同じ向きに傾きます。
  • ワイヤレスのリモコン:2本のジョイスティックを別のArduinoにつなぎ、nRF24L01でコマンドを送れば、スマートフォンのいらないリモコンになります。
  • ブラウザから操作:ESP32でWebページにスライダーを作り、同じWi-Fiネットワーク上のどの端末からでもアームを操作します。ESP32のWebSocketを使えば遅延を最小限にできます。
  • コンベアからピッキング:ステッピングモーターで小さなベルトコンベアを作り、TCRT5000が物を見つけたら、アームがベルトから拾い上げます。
  • コンテストや学校のプロジェクト:記録と再生ができるアームは、機構、電子回路、ソフトウェアをまとめて見せられるので、Arduinoの科学コンテスト向けプロジェクトアイデアの中でも有力な選択肢です。

Arduinoロボットアームは、機構設計、トルク計算、電源管理、なめらかな動きのプログラミングを一度に学べるプロジェクトで、手をかけるほど大きく育ちます。まずはポテンショメーターで操作するサーボ4個のアームから始め、記録と再生を加え、それからPCA9685とBluetoothでサーボ6個へステップアップしましょう。BlueGraysアプリには、ポテンショメーターやジョイスティックでのサーボ操作、スマートフォンからのBluetooth操作、サーボ32個の振り付けといったプロジェクトが、回路図とステップごとのコード付きでそろっています。アームの関節を一つずつ先に試してみることができます。

よくある質問

Arduinoロボットアームにはどのサーボモーターを使えばいいですか?

ベース、肩、ひじのように負荷のかかる関節には、9〜11 kg·cmのトルクを持つ金属ギアのMG996Rがおすすめです。手首やグリッパーのような軽い関節には、MG90SかSG90で十分です。SG90だけで作る小型アームも短く軽くすれば動きますが、持ち上げられるのは数十グラムの物だけです。

ロボットアームにはどんな電源が必要ですか?

SG90またはMG90Sを4個使う小型アームなら、5 V 3 AのACアダプターで足ります。MG996R 3個と小型サーボ3個のサーボ6個のアームには、5 A以上、余裕を持たせるなら10 Aの5〜6 VスイッチングACアダプターを使ってください。ACアダプターのマイナスをArduinoのGNDにつなぐのを忘れないでください。

Arduinoで何個のサーボを制御できますか?

Servoライブラリは、Arduino Unoで最大12個、Megaで48個のサーボを動かせます。ただし本当の限界は電流で、サーボはすべて外部電源から給電する必要があります。PCA9685モジュールならI2Cのピン2本だけでサーボ16個を動かせ、最大62枚までつなげられます。

ロボットアームのサーボが震えるのはなぜですか?

よくある原因は、電源不足、共通GNDの未接続、ポテンショメーターの読み取り値のノイズです。外部電源を使ってサーボの電源ラインに1000 µFのコンデンサを入れ、コードには平滑化フィルタと不感帯を加えてください。SoftwareSerialとServoライブラリを一緒に使っている場合は、サーボをPCA9685に移すと震えが完全になくなります。

SG90のロボットアームはどれくらいの重さを持ち上げられますか?

SG90のストールトルクは約1.8 kg·cmで、安全に動かすにはその半分の0.9 kg·cmを目安にします。アームとサーボ自身の重さでその一部を使ってしまうため、長さ15 cmほどのSG90のアームが実際に運べるのは、ペットボトルのキャップや小さなスポンジの立方体のような20〜30 gの軽い物だけです。

スマートフォンからロボットアームを操作するにはどうすればいいですか?

ArduinoにHC-05 Bluetoothモジュールをつなぎ、スマートフォンのBluetoothシリアルターミナルかスライダー付きのコントローラーアプリで「S120」のような文字のコマンドを送ります。ESP32ならBluetoothを内蔵しているので追加のモジュールは不要です。HC-05はiPhoneでは動かないので、iPhoneにはBLEモジュールかESP32 BLEが必要です。

全プロジェクトを見る
ポテンショメーターでサーボモーターを操作
#36初級
プレミアム

ポテンショメーターでサーボモーターを操作

ポテンショメーターのつまみを回すと、サーボモーターのアームもそれに合わせて回ります。つまみの一方の端が0度、もう一方の端が半回転に対応します。Arduinoがつまみの位置を常に読み取り、サーボモーターが動きにすぐ反応します。ロボットアームや回転式カメラ台などのプロジェクトへの第一歩です。

4.6(37)
52
ジョイスティックでサーボモーターを制御
#60中級
プレミアム

ジョイスティックでサーボモーターを制御

防犯カメラの向きを変えるように、ジョイスティックで2つのモーターを操作します。スティックを左右に倒すと下側のサーボモーターがゆっくり回り、上下に倒すと上側のサーボモーターが上下に傾きます。手を離すと、どちらもその位置で止まります。カメラやレーザーの台座など、2方向に動く装置の基礎を作ります。

4.7(42)
58
サーボモーターの使い方
#35初級
プレミアム

サーボモーターの使い方

小型サーボモーターのアームが、一方の端、中央、もう一方の端の3つの位置へ順番に回り、それぞれの位置で2秒間止まります。Arduinoが保持する角度をモーターに指示すると、モーターはその位置まで動いて止まります。ロボットアームや自動開閉するふたが動く仕組みを学べます。

4.7(48)
67
ESP32サーボモーター(ポテンショメーター制御)
#118中級
プレミアム

ESP32サーボモーター(ポテンショメーター制御)

このプロジェクトでは、ポテンショメーターでSG90サーボモーターを制御します。ポテンショメーターから読み取ったアナログ値を0~180度の範囲に変換し、サーボの角度を調整します。ESP32Servoライブラリは、ESP32の出力に対応した適切なGPIOでサーボ信号を生成します。このプロジェクトではGPIO 18を使います。ロボットアームやカメラ用ジンバル、自動ドアの機構に使われる基本的な制御方法です。

4.3(29)
42
BluetoothでスマートフォンからLEDを操作
#34初級
プレミアム

BluetoothでスマートフォンからLEDを操作

スマートフォンで部屋の照明をつけたり消したりするための、小さな練習です。Bluetoothモジュールがスマートフォンから送られた文字をArduinoに伝えます。ある文字で赤いLEDが、別の文字で緑のLEDが点灯し、同じ文字を小文字で送ると消灯します。無線操作の仕組みを、ごくシンプルな形で学べます。

4.8(68)
89
L298Nで作るBluetooth操作の車
#80上級
プレミアム

L298Nで作るBluetooth操作の車

スマートフォンをリモコンにして、自作のロボットカーを操作できます。ArduinoはBluetoothモジュールを通じてスマートフォンのリモコンアプリから指令を受け取り、モータードライバーで車輪を回します。前進、後退、左右や斜めへの移動ができ、速度もスマートフォンから調整できます。停止ボタンを押すと、短いブレーキ動作でモーターがすぐに止まります。

5.0(38)
65
サーボの円形ダンス — 32個のサーボで動く模様
#102上級
プレミアム

サーボの円形ダンス — 32個のサーボで動く模様

Arduino Megaで、円形に並べた32個のサーボモーターを動かすビジュアルアニメーションのプロジェクトです。サーボは1つの配列で管理し、Megaの22–53番ピンに接続します。ランダムな角度でばらばらに振動する動き、全体が同時に動く同期スイープ、円周を滑らかに巡る波、左右に揺れる方位磁針の針という4種類の動きを見せます。回転する波の効果は、各サーボと動作中のサーボとの関係によって生まれます。

4.5(40)
62