編集履歴一覧に戻る
tktk360のアイコン画像

tktk360 が 2025年12月26日23時44分27秒 に編集

初版

タイトルの変更

+

SpresenseTank

タグの変更

+

SPRESENSE

+

CAR

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

ライセンスの変更

+

(MIT) The MIT License

本文の変更

+

この作品は、「SPRESENSEを使った小型タンクを開発する」ことを目的としたものです。 柔軟に改造が効きやすく、部品は入手性及び、低コストのものを使用しています。 2025年 SONY SPRESENSEコンテストで提供いただいたモニター品を活用し制作しています。 通信機能として、SPRESENSE対応のWifiボードを活用し、タンクの操縦、カメラ映像の遠隔確認を考えていました。 残念ながら、モニター品としては、SPRESENSEのメインボードと拡張ボードでした。 そのため、通信機能については、カメラなしのタンクの操縦のみとなっています。 @[youtube](https://youtu.be/yUTFHTU4Xoc) # 目的・方針 ・操作可能で小型な車であること ・極力、お金をかけずに制作できること ・既製品との差別化ができること(機能アップができること) # 遊び方 タンクの通信には、BLE(SPP:Serial Port Profile)を利用しています。 送信機は、対応したアプリケーションの作成が必要になります。 タンクを操作するのにデータを送信する簡単なものとして、Androidスマートフォンアプリの [Serial Bluetooth Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_bluetooth_terminal&hl=ja)がおすすめです。 事前に、Androidスマートフォンと、タンクのBLEチップはペアリングが必要です。 HC-05のペアリングパスワードは、「1234」です。 | 値 | 機能 | |:---:|:---| | 1 | 前進 | | 2 | 後退 | | 3 | 右回転 | | 4 | 左回転 | | 5 | 停止 | # 部品 - 作成に使用したパーツは下記となります。 | NO | 品目 | 価格 | |:---:|:---:|:---| | 1 | [SONY SPRESENSE メインボード](https://akizukidenshi.com/catalog/g/gM-14584/) | 6,050 | | 2 | [SONY SPRESENSE 拡張ボード](https://akizukidenshi.com/catalog/g/gM-14585/) | 3,850 | | 3 | [L293D motor control shield v1](https://www.amazon.co.jp/dp/B01FWH8Q56/) | 398 | | 4 | [マイクロスライドスイッチ](https://www.amazon.co.jp/dp/B0FB3JVMH4/) | 460 | | 5 | [HC-05](https://ja.aliexpress.com/item/32990347631.html?spm=a2g0o.productlist.main.6.2cc24977i8IThA&aem_p4p_detail=202512212205259214510938859800003001788&algo_pvid=1876a418-d718-4782-a87b-e7a760828ea0&algo_exp_id=1876a418-d718-4782-a87b-e7a760828ea0-5&pdp_ext_f=%7B%22order%22%3A%2210%22%2C%22eval%22%3A%221%22%2C%22fromPage%22%3A%22search%22%7D&pdp_npi=6%40dis%21JPY%21611%21159%21%21%213.81%210.99%21%40212e4e5517663835253247283eba84%2166909440209%21sea%21JP%210%21ABX%211%210%21n_tag%3A-29910%3Bd%3Afc6b0ab%3Bm03_new_user%3A-29895%3BpisId%3A5000000187476132&curPageLogUid=xNETqukG8rG7&utparam-url=scene%3Asearch%7Cquery_from%3A%7Cx_object_id%3A32990347631%7C_p_origin_prod%3A&search_p4p_id=202512212205259214510938859800003001788_2) | 611 | | 6 | [HC-SR05](https://ja.aliexpress.com/item/1005007532986846.html?spm=a2g0o.productlist.main.2.31ebLAoxLAoxLg&aem_p4p_detail=202512212219365910568694520560003578590&algo_pvid=2c6d6c48-6a05-462b-adbb-6e021286e2c9&algo_exp_id=2c6d6c48-6a05-462b-adbb-6e021286e2c9-1&pdp_ext_f=%7B%22order%22%3A%2215%22%2C%22spu_best_type%22%3A%22order%22%2C%22eval%22%3A%221%22%2C%22fromPage%22%3A%22search%22%7D&pdp_npi=6%40dis%21JPY%21359%21359%21%21%2115.74%2115.74%21%402101364d17663843766341527ef42d%2112000041197368853%21sea%21JP%210%21ABX%211%210%21n_tag%3A-29910%3Bd%3Afc6b0ab%3Bm03_new_user%3A-29895&curPageLogUid=i0puHBrMt7pm&utparam-url=scene%3Asearch%7Cquery_from%3A%7Cx_object_id%3A1005007532986846%7C_p_origin_prod%3A&search_p4p_id=202512212219365910568694520560003578590_1) | 359 | | 7 | [マイクロ減速モーターギヤモーター](https://www.amazon.co.jp/dp/B0FJ82GX1M/) | 1,266 | | 8 | [9Vバッテリースナップ](https://www.amazon.co.jp/dp/B0F48CNCYP/) | 445 | | 9 | [9V 充電式 バッテリー](https://www.amazon.co.jp/dp/B0C1GWY826/) | 1,680 | | 10 | 3Dプリンタによる筐体 | プライスレス | | 11 | ジャンパ線 | プライスレス | | | 合計| 15,119(モニター品以外:5,219) | # 設計図 部品を元に、下記配線を行います。 ![配線図](https://camo.elchika.com/3c6d49c8bd27e0d6ad9caff4b7f0e39f4c545cdb/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f66353637396630372d646432612d343733322d383832622d386366643739623939343961/) SPRESENSEの拡張ボードは、ArduinoUnoと互換があります。 そのため、そのまま接続することができます。 ※注意点:SPRESENSEは、ArduinoUnoである所のVINピンがなく、Resevedになっているため、 モーターシールドからの直接給電は残念ながらできません。 - 外装を3Dプリンタで印刷します。 ![3Dプリンタの印刷:筐体](https://camo.elchika.com/e9c10af83cd3a6500a27b9005be620d648e7c06f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f62333537646562662d353539362d346138372d386361622d326331323466663237633437/) Banbulab A1 Miniで、PLA樹脂を使い印刷しました。 筐体のSTLデータは[こちら](https://www.thingiverse.com/thing:2662828/files)を使わせてもらいました ![パーツ](https://camo.elchika.com/d83677777488e8fc0f66f02dc0e8317c953b3cef/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f33356461313430352d613139312d346363652d623530342d353533356136316464396334/) 組み立てにネジは不要です。キャタピラの接続には、フィラメントを切って、接続しています。 ![組み立て](https://camo.elchika.com/a3418ffee08192bdd79536a69a54bef7e6a53b8b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f62386637346233302d313666652d346464342d623336312d6463653831333164303134392f31316233663362382d623361302d343366392d393261332d346237656663323633386162/) # プログラム ==使用ライブラリ== Arduino Library Manager ・ボードマネージャ-Spresense Commuity 3.4.5 ==プログラム== プログラム全体を下記にのせています。 ※モーターシールドのArudinoライブラリは、SPRESENSEに対応していません。そのため、作成しています。 ```arduino:モータードライバ(AFMotorSpresense.h) #ifndef _AFMotorSpresense_h_ #define _AFMotorSpresense_h_ #include <inttypes.h> #include <Arduino.h> // Spresense用ダミー定義 #define DC_MOTOR_PWM_RATE 0 #define FORWARD 1 #define BACKWARD 2 #define BRAKE 3 #define RELEASE 4 // Arduinoピン定義 (Motor Shield V1) #define MOTORLATCH 12 #define MOTORCLK 4 #define MOTORENABLE 7 #define MOTORDATA 8 class AFMotorController { public: AFMotorController(void); void enable(void); void latch_tx(void); uint8_t TimerInitalized; }; class AF_DCMotor { public: AF_DCMotor(uint8_t motornum, uint8_t freq = DC_MOTOR_PWM_RATE); void run(uint8_t); void setSpeed(uint8_t); private: uint8_t motornum; }; // ステッパーは今回DCモータ用のみ実装を簡略化 #endif ``` ```arduino:モータードライバ(AFMotorSpresense.cpp) #include "AFMotorSpresense.h" static uint8_t latch_state; AFMotorController::AFMotorController(void) { TimerInitalized = false; } void AFMotorController::enable(void) { pinMode(MOTORLATCH, OUTPUT); pinMode(MOTORENABLE, OUTPUT); pinMode(MOTORDATA, OUTPUT); pinMode(MOTORCLK, OUTPUT); latch_state = 0; latch_tx(); digitalWrite(MOTORENABLE, LOW); } void AFMotorController::latch_tx(void) { digitalWrite(MOTORLATCH, LOW); delayMicroseconds(1); // Spresenseの高速動作に合わせるための微小な待ち時間 shiftOut(MOTORDATA, MOTORCLK, MSBFIRST, latch_state); delayMicroseconds(1); digitalWrite(MOTORLATCH, HIGH); } static AFMotorController MC; AF_DCMotor::AF_DCMotor(uint8_t num, uint8_t freq) { motornum = num; MC.enable(); // 各モータのPWMピン初期化 uint8_t pwmpin; if (num == 1) pwmpin = 11; else if (num == 2) pwmpin = 3; else if (num == 3) pwmpin = 6; else if (num == 4) pwmpin = 5; pinMode(pwmpin, OUTPUT); analogWrite(pwmpin, 0); } void AF_DCMotor::setSpeed(uint8_t speed) { uint8_t pwmpin; if (motornum == 1) pwmpin = 11; // Spresenseの11はPWM不可のためデジタル動作になる else if (motornum == 2) pwmpin = 3; else if (motornum == 3) pwmpin = 6; else if (motornum == 4) pwmpin = 5; analogWrite(pwmpin, speed); } void AF_DCMotor::run(uint8_t cmd) { uint8_t a, b; switch (motornum) { case 1: a = 2; b = 3; break; // MOTOR1_A, B case 2: a = 1; b = 4; break; // MOTOR2_A, B case 3: a = 5; b = 7; break; // MOTOR3_A, B case 4: a = 0; b = 6; break; // MOTOR4_A, B default: return; } if (cmd == FORWARD) { latch_state |= (1 << a); latch_state &= ~(1 << b); } else if (cmd == BACKWARD) { latch_state &= ~(1 << a); latch_state |= (1 << b); } else { latch_state &= ~(1 << a); latch_state &= ~(1 << b); } MC.latch_tx(); } ``` ```arduino:メインプログラム(SpresenseTank.ino) #define DC_MOTOR_PWM_RATE 1 #include "AFMotorSpresense.h" #include <SoftwareSerial.h> #define SELF_UART_BAUDRATE 9600 #define TGT_SERIAL_BAUDRATE 9600 // HC-06 Pass 1234 SoftwareSerial mySerial(2, 3); // RX | TX AF_DCMotor R_motor(4); // defines Right motor connector AF_DCMotor L_motor(3); // defines Left motor connector void motorStop() { Serial.println("motorStop"); R_motor.run(RELEASE); L_motor.run(RELEASE); } void motorFoward() { Serial.println("motorFoward"); L_motor.run(FORWARD); R_motor.run(FORWARD); } void motorBack() { Serial.println("motorBack"); R_motor.run(BACKWARD); L_motor.run(BACKWARD); } void motorLeft() { Serial.println("motorLeft"); R_motor.run(FORWARD); L_motor.run(BACKWARD); } void motorRight() { Serial.println("motorRight"); R_motor.run(BACKWARD); L_motor.run(FORWARD); } void setupLight(bool led1, bool led2, bool led3, bool led4) { if (led1) { digitalWrite(LED0, HIGH); } else { digitalWrite(LED0, LOW); } if (led2) { digitalWrite(LED1, HIGH); } else { digitalWrite(LED1, LOW); } if (led3) { digitalWrite(LED2, HIGH); } else { digitalWrite(LED2, LOW); } if (led4) { digitalWrite(LED3, HIGH); } else { digitalWrite(LED3, LOW); } } void setup() { // put your setup code here, to run once: pinMode(LED0, OUTPUT); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); pinMode(LED3, OUTPUT); Serial.begin(SELF_UART_BAUDRATE); mySerial.begin(TGT_SERIAL_BAUDRATE); //モータースピードを設定(0~255の範囲) L_motor.setSpeed(200); R_motor.setSpeed(200); R_motor.run(RELEASE); L_motor.run(RELEASE); Serial.println("setup"); } void loop() { if (mySerial.available()) { char data = mySerial.read(); switch(data) { case '1': Serial.println("foward"); setupLight(true, false, false, false); motorFoward(); break; case '2': Serial.println("back"); setupLight(false, true, false, false); motorBack(); break; case '3': Serial.println("right"); setupLight(false, false, true, false); motorRight(); break; case '4': Serial.println("left"); setupLight(false, false, false, true); motorLeft(); break; case '0': Serial.println("stop"); setupLight(false, false, false, false); motorStop(); break; default: //Serial.println(data); break; } delay(150); } } ``` 送信機は、Androidスマートフォン用のアプリを作成しました。 開発にはUnityを使っています。 根幹となる通信プログラムをもとに自由にUXを作成してみてください。 ```CS:BLE SPP(BluetoothSerialSender.cs) using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.Android; public class BluetoothSerialSender : MonoBehaviour { private AndroidJavaObject _bluetoothAdapter; private AndroidJavaObject _bluetoothSocket; private AndroidJavaObject _outputStream; // 標準的なSPP(シリアルポートプロファイル)のUUID private const string SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB"; // 接続先デバイス名(HC-06など) public string targetDeviceName = "HC-06"; public string LastSentMessage { get; private set; } = ""; public Text logText; void Start() { // アプリ起動時に権限をチェック CheckBluetoothPermissions(); } public void Connect() { try { var bluetoothAdapterClass = new AndroidJavaClass("android.bluetooth.BluetoothAdapter"); _bluetoothAdapter = bluetoothAdapterClass.CallStatic<AndroidJavaObject>("getDefaultAdapter"); if (_bluetoothAdapter == null) { SetLog("Bluetooth not supported"); return; } // ペアリング済みデバイスを取得 AndroidJavaObject pairedDevices = _bluetoothAdapter.Call<AndroidJavaObject>("getBondedDevices"); if (pairedDevices == null) { SetLog("device list error"); return; } AndroidJavaObject iterator = pairedDevices.Call<AndroidJavaObject>("iterator"); AndroidJavaObject targetDevice = null; while (iterator.Call<bool>("hasNext")) { AndroidJavaObject device = iterator.Call<AndroidJavaObject>("next"); string name = device.Call<string>("getName"); if (name == targetDeviceName) { targetDevice = device; break; } } if (targetDevice != null) { // ソケットの作成 AndroidJavaClass uuidClass = new AndroidJavaClass("java.util.UUID"); AndroidJavaObject uuid = uuidClass.CallStatic<AndroidJavaObject>("fromString", SPP_UUID); _bluetoothSocket = targetDevice.Call<AndroidJavaObject>("createRfcommSocketToServiceRecord", uuid); // 接続 _bluetoothSocket.Call("connect"); _outputStream = _bluetoothSocket.Call<AndroidJavaObject>("getOutputStream"); SetLog($"{targetDeviceName} connect success!"); } else { SetLog("No devices found, please check pairing."); } } catch (Exception e) { SetLog("ERROR: " + e.Message); } } private void SetLog(string message) { LastSentMessage = message; if (logText != null) { logText.text = message; } Debug.LogWarning(message); } public void Disconnect() { if (_bluetoothSocket != null) { _bluetoothSocket.Call("close"); _bluetoothSocket = null; _outputStream = null; SetLog("Disconnect"); } } private void OnApplicationQuit() { if (_bluetoothSocket != null) _bluetoothSocket.Call("close"); } public void SendNumber(int value) { if (_outputStream == null) return; try { string message = value.ToString() + "\n"; byte[] bytes = System.Text.Encoding.ASCII.GetBytes(message); // Javaのbyte型に変換して送信 sbyte[] sbytes = Array.ConvertAll(bytes, b => (sbyte)b); _outputStream.Call("write", sbytes); _outputStream.Call("flush"); SetLog($"write: {value}"); } catch (Exception e) { SetLog("error: " + e.Message); } } public void CheckBluetoothPermissions() { #if UNITY_ANDROID // Android 12 (API 31) 以上かどうかを判定 if (GetAndroidSDKInt() >= 31) { string[] permissions = { "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT" }; List<string> permissionsToRequest = new List<string>(); foreach (string permission in permissions) { if (!Permission.HasUserAuthorizedPermission(permission)) { permissionsToRequest.Add(permission); } } if (permissionsToRequest.Count > 0) { // まとめて権限をリクエスト Permission.RequestUserPermissions(permissionsToRequest.ToArray()); } } else { // Android 11以前は位置情報の権限が必要な場合が多い if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation)) { Permission.RequestUserPermission(Permission.FineLocation); } } #endif } // AndroidのAPIレベルを取得するヘルパー private int GetAndroidSDKInt() { #if UNITY_EDITOR return 31; // エディタでは0を返す #endif using (var version = new AndroidJavaClass("android.os.Build$VERSION")) { return version.GetStatic<int>("SDK_INT"); } } } ``` ==最後に== - まだまだ機能が少ないため、改良の余地があります。 筐体は3Dプリンタのため、壊れても直しやすく、自由に改造ができるので、試行錯誤がしやすいです。 ==今後の応用== ・SPRESENSEのWifiチップとの連動 ・SPRESENSEのカメラ機能との連動 ・ROS対応