この作品は、「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プリンタで印刷します。
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対応
投稿者の人気記事





-
tktk360
さんが
2025/12/26
に
編集
をしました。
(メッセージ: 初版)
-
tktk360
さんが
2025/12/26
に
編集
をしました。
ログインしてコメントを投稿する