PS2ワイヤレスジョイスティックで操作するArduinoロボット戦車
部品と材料
| 部品 | 数量 | 備考 |
|---|---|---|
| Arduino UNO | 1 | メイン制御ボード |
| SparkFun デュアルHブリッジ L298 | 1 | DCモーター駆動用 |
| DIYロボット戦車キット | 1 | DCモーター×2、ギア、キャタピラ、ネジ等含む |
| Sony PS2ワイヤレスコントローラー | 1 | 受信機付き |
| 18650 3.7Vバッテリー | 3 | 直列接続で11.1V(12Vモーター駆動用) |
| 3S 18650バッテリーホルダー | 1 | 戦車後部に取付 |
| 18650バッテリー充電器 | 1 | |
| ジャンパーワイヤー | 1セット | オス-メス×6、オス-オス×2 等 |
| Type-B USBケーブル | 1 | 書き込み用 |
必要な工具
- はんだごて・はんだ線(モーター配線の修理用)
- EVAフォームシート(アルミシャーシと基板の絶縁用)
- 両面テープ(フォーム固定・Hブリッジ固定用)
- ハサミ(フォームカット用)
アプリとオンラインサービス
- Arduino IDE
プロジェクト概要
このチュートリアルでは、ワイヤレスPlayStation 2(PS2)ジョイスティックでロボット戦車を操作する方法を解説します。Arduino Unoを核として、ワイヤレスコントローラーからのコマンドを受信し、モーターの回転数を制御します。NodeMCUやFirebeetleなど他の開発ボードでも応用可能で、ここで紹介する原理は他のロボットやガジェットにも転用できます。
筆者はこれまでに、Blynk経由でWi-Fi制御する戦車(こちら)、Googleアシスタントで音声制御するロボット(こちら)、自作シャーシのFPVロボット(こちら)も制作しています。
ステップ1: 工具と材料の準備
主要パーツの詳細:
- Arduino Uno系ボード - メインCPU、Arduino IDEで簡単にプログラミング可能、初心者向け
- L298NデュアルHブリッジモジュール - 3.3V/5V信号を12Vモーター駆動電圧に昇圧、正逆転制御可能
- DIYロボットシャーシ戦車キット - 全パーツ同梱、組立工具(六角レンチ、ドライバー)も付属、初心者に最適
- PS2ワイヤレスリモコン - 20個のデジタルボタンと2本のアナログスティックを搭載、シリアル通信でマイコンと接続
- 18650電池3本直列 - 11.1V出力で12Vモーターを駆動
※アルミシャーシは導電性があるため、基板との間にEVAフォームを挟んでショート防止しています。
ステップ2: ロボットの組み立て
キットの組み立てマニュアルは中国語のスプレッドシートのみで不親切ですが、パーツ品質は良好でスペア部品も同梱されています。組み立て過程は動画で記録しています。
ステップ3: 配線
バッテリーパック(3直列)を後部に搭載。L298N Hブリッジでモーターを制御します。Arduinoの5V信号を受け取り、モーターに高電圧を供給し、入力信号の組み合わせで正逆転を切り替えます。
ピン接続一覧
| Arduinoピン | 接続先 |
|---|---|
| Digital 3 | Hブリッジ ENA |
| Digital 5 | Hブリッジ IN1 |
| Digital 4 | Hブリッジ IN2 |
| Digital 8 | Hブリッジ IN3 |
| Digital 7 | Hブリッジ IN4 |
| Digital 6 | Hブリッジ ENB |
| 5V | Hブリッジ 5V |
| GND | Hブリッジ GND |
| Digital 10 | PS2受信機 pin 6 (Attention) |
| Digital 11 | PS2受信機 pin 2 (Command) |
| Digital 12 | PS2受信機 pin 1 (Data) |
| Digital 13 | PS2受信機 pin 7 (Clock) |
| 3.3V | PS2受信機 pin 5 (Power) |
| GND | PS2受信機 pin 4 (Ground) |
Hブリッジ側の接続:
- ENA → Arduino D3, IN1 → D5, IN2 → D4, IN3 → D8, IN4 → D7, ENB → D6
- 5V → Arduino 5V, GND → Arduino GND
- GND → バッテリー負極, 12V → バッテリー正極
- OUT1 → 右モーター負極, OUT2 → 右モーター正極
- OUT3 → 左モーター正極, OUT4 → 左モーター負極
ステップ4: Arduino IDEのセットアップ
ステップ5: PS2ワイヤレスコントローラーライブラリの解説
Bill Porter氏のPS2Xライブラリを使用します。主な機能:
PS2X ps2x;- コントローラーオブジェクト生成ps2x.config_gamepad(clock, command, attention, data, pressures?, rumble?)- ピン割り当てと初期化、エラーコードを返すps2x.Button(button)- デジタルボタン読み取り(押下時true)ps2x.Analog(stick_axis)- アナログスティック読み取り(0〜255)
ボタン定数
- 方向キー:
PSB_PAD_UP,PSB_PAD_DOWN,PSB_PAD_LEFT,PSB_PAD_RIGHT - アクション:
PSB_GREEN/TRIANGLE,PSB_RED/CIRCLE,PSB_BLUE/CROSS,PSB_PINK/SQUARE - トリガー:
PSB_L1,PSB_R1,PSB_L2,PSB_R2 - スティック押し込み:
PSB_L3,PSB_R3 - メニュー:
PSB_SELECT,PSB_START
アナログスティック軸
PSS_LX,PSS_LY- 左スティック X/YPSS_RX,PSS_RY- 右スティック X/Y
ステップ6: スケッチ #1 - 方向キー制御(レトロゲーム風)
十字キーで基本動作(前進・後退・左旋回・右旋回)を制御。ボタンはオン/オフのみなので、モーターは全速で動作します。
コードのポイント
- 起動時にENA/ENBをLOWにしてモーター無効化(暴走防止)
ps2x.config_gamepad(13,11,10,12, true, true)で初期化(clock, command, attention, data, pressures, rumble)- メインループで各方向キーをチェックし、モーター制御ピンを設定
- PWM出力(analogWrite)でENA/ENBを制御、1023=全速
ステップ7: スケッチ #2 - 左アナログスティック + L2/R2ボタン(レーシングゲーム風)
左スティックX軸でステアリング、R2で前進、L2で後退。マリオカート型の操作感覚です。
実装のポイント
- 左スティックX値(0〜255)を -1023〜1023 にマッピング
- 左右モーターの基準速度1023からスティック値を加減算して差速を生成
- R2押下で前進方向、L2で後進方向、どちらも押されていなければ停止
ステップ8: スケッチ #3 - 左右両アナログスティック(差動駆動・実戦車風)
左スティックで左キャタピラ、右スティックで右キャタピラを独立制御。実戦車の操作感に近く、筆者お気に入りの方式です。
実装のポイント
- 左右スティックY軸を読み取り、それぞれ -1023〜1023 にマッピング(左右で符号反転)
- デッドバンド ±50 を設定し、微小入力ではモーター停止
- 各モーターの正逆転をスティックの正負で判定、ENA/ENBは固定値1023(改良余地あり)
ステップ9: スケッチ #4 - 左アナログスティックのみ(ミキシング制御)
1本のスティックで前進/後進と旋回を同時制御。ボタンを節約できるが操作は難しい。差動ステアリングの数学モデルに基づく実装で、こちらの解説を参考にしています。
ステップ10: まとめ
4種類の制御方式を実装しました。用途や好みに合わせて選択・改良してください。ご質問はコメント欄へどうぞ。他のロボットプロジェクトも公開していますので、興味があればご覧ください。
ソースコード(4種類のスケッチ)
スケッチ #1: 方向キー制御
// PS2 Tank by Igor Fonseca @2019
// D-pad buttonsで戦車を制御
#include <PS2X_lib.h>
#define ENA 3
#define MOTORA_1 4
#define MOTORA_2 5
#define MOTORB_1 8
#define MOTORB_2 7
#define ENB 6
PS2X ps2x;
int error = 0;
byte type = 0;
byte vibrate = 0;
void setup() {
pinMode(ENA, OUTPUT); pinMode(MOTORA_1, OUTPUT); pinMode(MOTORA_2, OUTPUT);
pinMode(ENB, OUTPUT); pinMode(MOTORB_1, OUTPUT); pinMode(MOTORB_2, OUTPUT);
digitalWrite(ENA, 0); digitalWrite(ENB, 0);
Serial.begin(57600);
error = ps2x.config_gamepad(13, 11, 10, 12, true, true);
if (error == 0) Serial.println("Found Controller, configured successful");
else if (error == 1) Serial.println("No controller found, check wiring or reset the Arduino");
else if (error == 2) Serial.println("Controller found but not accepting commands");
else if (error == 3) Serial.println("Controller refusing to enter Pressures mode, may not support it.");
type = ps2x.readType();
if (type == 1) Serial.println("DualShock Controller Found");
else if (type == 2) Serial.println("GuitarHero Controller Found");
else Serial.println("Unknown Controller type");
}
void loop() {
if (error == 1) return;
ps2x.read_gamepad(false, vibrate);
if (ps2x.Button(PSB_PAD_UP)) {
digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, HIGH);
digitalWrite(MOTORB_1, HIGH); digitalWrite(MOTORB_2, LOW);
analogWrite(ENB, 1023); analogWrite(ENA, 1023);
} else if (ps2x.Button(PSB_PAD_RIGHT)) {
digitalWrite(MOTORA_1, HIGH); digitalWrite(MOTORA_2, LOW);
digitalWrite(MOTORB_1, HIGH); digitalWrite(MOTORB_2, LOW);
analogWrite(ENB, 1023); analogWrite(ENA, 1023);
} else if (ps2x.Button(PSB_PAD_LEFT)) {
digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, HIGH);
digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, HIGH);
analogWrite(ENB, 1023); analogWrite(ENA, 1023);
} else if (ps2x.Button(PSB_PAD_DOWN)) {
digitalWrite(MOTORA_1, HIGH); digitalWrite(MOTORA_2, LOW);
digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, HIGH);
analogWrite(ENB, 1023); analogWrite(ENA, 1023);
} else {
analogWrite(ENB, 0); analogWrite(ENA, 0);
}
delay(50);
}
スケッチ #2: 左スティック + L2/R2
// ...(共通部分省略)...
int motor_right_speed = 0, motor_left_speed = 0;
void loop() {
if (error == 1) return;
ps2x.read_gamepad(false, vibrate);
int nJoyL = ps2x.Analog(PSS_LX);
nJoyL = map(nJoyL, 0, 255, 1023, -1023);
motor_right_speed = 1023; motor_left_speed = 1023;
if (nJoyL < 0) motor_right_speed += nJoyL;
if (nJoyL > 0) motor_left_speed -= nJoyL;
if (ps2x.Button(PSB_R2)) {
digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, HIGH);
digitalWrite(MOTORB_1, HIGH); digitalWrite(MOTORB_2, LOW);
analogWrite(ENA, motor_right_speed); analogWrite(ENB, motor_left_speed);
} else if (ps2x.Button(PSB_L2)) {
digitalWrite(MOTORA_1, HIGH); digitalWrite(MOTORA_2, LOW);
digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, HIGH);
analogWrite(ENA, motor_right_speed); analogWrite(ENB, motor_left_speed);
} else {
analogWrite(ENA, 0); analogWrite(ENB, 0);
}
delay(50);
}
スケッチ #3: 左右両スティック(差動駆動)
void loop() {
if (error == 1) return;
ps2x.read_gamepad(false, vibrate);
int nJoyL = ps2x.Analog(PSS_LY);
int nJoyR = ps2x.Analog(PSS_RY);
nJoyL = map(nJoyL, 0, 255, 1023, -1023);
nJoyR = map(nJoyR, 0, 255, -1023, 1023);
// 右モーター (ENA)
if (nJoyR > 50) { digitalWrite(MOTORA_1, HIGH); digitalWrite(MOTORA_2, LOW); analogWrite(ENA, 1023); }
else if (nJoyR < -50) { digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, HIGH); analogWrite(ENA, 1023); }
else analogWrite(ENA, 0);
// 左モーター (ENB)
if (nJoyL > 50) { digitalWrite(MOTORB_1, HIGH); digitalWrite(MOTORB_2, LOW); analogWrite(ENB, 1023); }
else if (nJoyL < -50) { digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, HIGH); analogWrite(ENB, 1023); }
else analogWrite(ENB, 0);
delay(50);
}
スケッチ #4: 左スティックのみ(ミキシング)
void loop() {
if (error == 1) return;
ps2x.read_gamepad(false, vibrate);
int nJoyX = ps2x.Analog(PSS_LX);
int nJoyY = ps2x.Analog(PSS_LY);
nJoyX = map(nJoyX, 0, 255, -1023, 1023);
nJoyY = map(nJoyY, 0, 255, 1023, -1023);
// 差動ステアリング計算(ImpulseAdventure方式)
float fPivYLimit = 1023.0;
float nMotPremixL, nMotPremixR;
if (nJoyY >= 0) {
nMotPremixL = (nJoyX >= 0) ? 1023.0 : (1023.0 + nJoyX);
nMotPremixR = (nJoyX >= 0) ? (1023.0 - nJoyX) : 1023.0;
} else {
nMotPremixL = (nJoyX >= 0) ? (1023.0 - nJoyX) : 1023.0;
nMotPremixR = (nJoyX >= 0) ? 1023.0 : (1023.0 + nJoyX);
}
nMotPremixL *= nJoyY / 1023.0;
nMotPremixR *= nJoyY / 1023.0;
int nPivSpeed = nJoyX;
float fPivScale = (abs(nJoyY) > fPivYLimit) ? 0.0 : (1.0 - abs(nJoyY) / fPivYLimit);
int nMotMixL = (1.0 - fPivScale) * nMotPremixL + fPivScale * nPivSpeed;
int nMotMixR = (1.0 - fPivScale) * nMotPremixR + fPivScale * (-nPivSpeed);
// モーター出力(デッドバンド ±50)
if (nMotMixR > 50) { digitalWrite(MOTORB_1, HIGH); digitalWrite(MOTORB_2, LOW); }
else if (nMotMixR < -50) { digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, HIGH); }
else { digitalWrite(MOTORB_1, LOW); digitalWrite(MOTORB_2, LOW); }
if (nMotMixL > 50) { digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, HIGH); }
else if (nMotMixL < -50) { digitalWrite(MOTORA_1, HIGH); digitalWrite(MOTORA_2, LOW); }
else { digitalWrite(MOTORA_1, LOW); digitalWrite(MOTORA_2, LOW); }
analogWrite(ENA, abs(nMotMixL));
analogWrite(ENB, abs(nMotMixR));
delay(50);
}
回路図
プロジェクトページのSchematicsセクションに配線図が掲載されています。
製造プロセス
- Arduino & Raspberry Pi でインターネット制御ビデオストリーミングロボットを構築
- Unopad: Ableton Live連携シームレスなArduino MIDIコントローラー
- ArduinoとHC-SR04で作るサーボ駆動障害物回避ロボット
- ArduinoとAndroidでRoomba Create 2をリモコン操作 ─ Bluetooth経由のステップバイステップガイド
- Arduinoベースのヌンチャク操作6自由度ロボットアームキット
- Arduino Nanoとジョイスティックで2軸ステッピングモーターを制御:Visuinoで簡単プログラミング
- ゲーム用カスタムArduinoジョイスティックハンドルの製作
- PS2コントローラーで操作するリモコンバトルボットの作り方
- Arduinoロボットアーム「LittleArm」をAndroidアプリでBluetooth制御
- Arduino Nanoで作る「食べさせロボット」:超音波センサーで食べ物検知し口を開閉
- 4輪ArduinoロボットをDabbleアプリでBluetooth制御しよう