tktk360のアイコン画像
tktk360 2025年12月26日作成 (2025年12月26日更新) © MIT
製作品 製作品 閲覧数 76
tktk360 2025年12月26日作成 (2025年12月26日更新) © MIT 製作品 製作品 閲覧数 76

SpresenseTank

SpresenseTank

この作品は、「SPRESENSEを使った小型タンクを開発する」ことを目的としたものです。
柔軟に改造が効きやすく、部品は入手性及び、低コストのものを使用しています。
2025年 SONY SPRESENSEコンテストで提供いただいたモニター品を活用し制作しています。

通信機能として、SPRESENSE対応のWifiボードを活用し、タンクの操縦、カメラ映像の遠隔確認を考えていました。
残念ながら、モニター品としては、SPRESENSEのメインボードと拡張ボードでした。
そのため、通信機能については、カメラなしのタンクの操縦のみとなっています。

ここに動画が表示されます

目的・方針

・操作可能で小型な車であること
・極力、お金をかけずに制作できること
・既製品との差別化ができること(機能アップができること)

遊び方

タンクの通信には、BLE(SPP:Serial Port Profile)を利用しています。
送信機は、対応したアプリケーションの作成が必要になります。
タンクを操作するのにデータを送信する簡単なものとして、Androidスマートフォンアプリの
Serial Bluetooth Terminalがおすすめです。

事前に、Androidスマートフォンと、タンクのBLEチップはペアリングが必要です。
HC-06のペアリングパスワードは、「1234」です。

機能
1 前進
2 後退
3 右回転
4 左回転
5 停止

部品

  • 作成に使用したパーツは下記となります。
NO 品目 価格
1 SONY SPRESENSE メインボード 6,050
2 SONY SPRESENSE 拡張ボード 3,850
3 L293D motor control shield v1 398
4 マイクロスライドスイッチ 460
5 HC-06 611
6 HC-SR05 359
7 マイクロ減速モーターギヤモーター 1,266
8 9Vバッテリースナップ 445
9 9V 充電式 バッテリー 1,680
10 3Dプリンタによる筐体 プライスレス
11 ジャンパ線 プライスレス
合計 15,119(モニター品以外:5,219)

設計図

部品を元に、下記配線を行います。

配線図

SPRESENSEの拡張ボードは、ArduinoUnoと互換があります。
そのため、そのまま接続することができます。
※注意点:SPRESENSEは、ArduinoUnoである所のVINピンがなく、Resevedになっているため、
モーターシールドからの直接給電は残念ながらできません。

  • 外装を3Dプリンタで印刷します。
    3Dプリンタの印刷:筐体
    Banbulab A1 Miniで、PLA樹脂を使い印刷しました。
    筐体のSTLデータはこちらを使わせてもらいました

パーツ
組み立てにネジは不要です。キャタピラの接続には、フィラメントを切って、接続しています。

組み立て

プログラム

使用ライブラリ
Arduino Library Manager
・ボードマネージャ-Spresense Commuity 3.4.5

プログラム
プログラム全体を下記にのせています。
※モーターシールドのArudinoライブラリは、SPRESENSEに対応していません。そのため、作成しています。

モータードライバ(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

モータードライバ(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(); }

メインプログラム(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を作成してみてください。

BLE

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対応

ログインしてコメントを投稿する