miyavit0のアイコン画像
miyavit0 2025年01月31日作成 (2025年01月31日更新)
製作品 製作品 閲覧数 175
miyavit0 2025年01月31日作成 (2025年01月31日更新) 製作品 製作品 閲覧数 175

SPRESENSEで倒立振子に挑戦してみた話

SPRESENSEで倒立振子に挑戦してみた話

SPRESENSEで倒立振子

elchikaでSPRESENSEを使った倒立振子作品がなかったので、挑戦してみました。
キャプションを入力できます

やりたいこと

・倒立振子のハード製作
・SPRESENSEでのモータの駆動
・カメラ映像の表示/撮影
・IMUセンサで姿勢制御
・WIfi/LTEで遠隔操作(次回)

構成

部品は安く手に入るものを中心に構成を組み立てました
キャプションを入力できます

部品

部品名 型式など
SPRESENSEメインボード
SPRESENSE LTE拡張ボード
SPRESENSE HDRカメラボード
WiFiAddonボード is110B
Lipoバッテリ 402030
バッテリ充電ボード TP4056
エンコーダ付きギアードモータ JGA25-371
モータドライバ TB6612FNG
加速度センサ GY-521

ハード設計

倒立振子でよくあるパターンがユニバーサル基板をフレームに、タミヤのギアモータなどを取り付けるものでした。
外装もなく縦長で見た目が微妙なので、簡易に3Dプリンタでケース・フレーム設計を行います。
全高を低く抑えたものを目標とします。

スタック方式

内蔵する各基板サイズも形状も違い、上部からピンヘッダで配線をするので、全高を抑えるには少し工夫が必要そうです。
機能ごとにケースを分け、上部に積み上げて取り付ける方式としました。
キャプションを入力できます
これで、機能を追加したい場合も新たなモジュールケースを挟み込むことで、簡単に追加できる構造とします。
前後方向と上下方向を固定するために、左右から爪で押さえつける構造とすることで、ケースごとのねじなどの締結部品の数を省いています。
キャプションを入力できます

モータスペック確認

使用するギアードモータは定格12Vなのですが、12V電源を持ってくるのは結構大変です。
3~5Vで駆動ができればLipoバッテリやモバイルバッテリなどでも問題がないので、モータを低電圧で駆動ができるか確認してみます。
キャプションを入力できます

モータ電圧を変化させて、電流がどの程度必要かをプロットしました。
キャプションを入力できます
動き出しの電圧は1.3Vくらいでした。
この結果であればLipoバッテリなどでもいけそうです。

配線

IMU

IMUpin SPRESENSE
VCC JP1 10
GND JP2 10
SCL JP2 11
SDA JP2 12

モータドライバ

モータドライバは、SPRESENSEとモータ用外部電源(バッテリ)、モータと接続します。

モータドライバpin
PWMA CN9 9
AIN2 CN9 7
AIN1 CN9 8
STBY JP1 10
BIN1 CN9 10
BIN2 CN9 12
PWMB CN9 11
GND
VM 外部電源+
VCC JP1 10
GND 外部電源GND
AO1 モータ1 赤
AO2 モータ1 白
BO2 モータ2 赤
BO1 モータ2 白
GND CN9 2

ハード製作

3Dプリンタで印刷しました。

配線/はんだ

モジュールのピンヘッダのはんだづけと、充電モジュールにLipoを繋ぎます。
キャプションを入力できます

加速度センサ・モータドライバも同様にピンをはんだ付けします

組立

モータとベース部品を繋げます。
キャプションを入力できます

1段目のケースを取り付けます。
IMUは前方中心に設置しました。
キャプションを入力できます

ケースを積み上げて、全部品を組付けました。
キャプションを入力できます

SPRESENSEには、電力供給+通信用に脇と正面から有線でアクセス可能にしました。
キャプションを入力できます

カメラ撮影

以下のコードでカメラ撮影が可能なことを確認しました。非常に簡単です。
キャプションを入力できます

カメラ撮影

#include <SDHCI.h> #include <stdio.h> #include <Camera.h> #define BAUDRATE (115200) #define TOTAL_PICTURE_COUNT (10) SDClass theSD; int take_picture_count = 0; void printError(enum CamErr err) { Serial.print("Error: "); switch (err) { case CAM_ERR_NO_DEVICE: Serial.println("No Device"); break; case CAM_ERR_ILLEGAL_DEVERR: Serial.println("Illegal device error"); break; case CAM_ERR_ALREADY_INITIALIZED: Serial.println("Already initialized"); break; case CAM_ERR_NOT_INITIALIZED: Serial.println("Not initialized"); break; case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break; case CAM_ERR_CANT_CREATE_THREAD: Serial.println("Failed to create thread"); break; case CAM_ERR_INVALID_PARAM: Serial.println("Invalid parameter"); break; case CAM_ERR_NO_MEMORY: Serial.println("No memory"); break; case CAM_ERR_USR_INUSED: Serial.println("Buffer already in use"); break; case CAM_ERR_NOT_PERMITTED: Serial.println("Operation not permitted"); break; default: break; } } void CamCB(CamImage img) { /* Check the img instance is available or not. */ if (img.isAvailable()) { img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); Serial.print("Image data size = "); Serial.print(img.getImgSize(), DEC); Serial.print(" , "); Serial.print("buff addr = "); Serial.print((unsigned long)img.getImgBuff(), HEX); Serial.println(""); } else { Serial.println("Failed to get video stream image"); } } void setup() { CamErr err; Serial.begin(BAUDRATE); while (!Serial) { ; } while (!theSD.begin()) { Serial.println("Insert SD card."); } Serial.println("Prepare camera"); err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { printError(err); } Serial.println("Start streaming"); err = theCamera.startStreaming(true, CamCB); if (err != CAM_ERR_SUCCESS) { printError(err); } Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { printError(err); } Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { printError(err); } } void loop() { sleep(1); if (take_picture_count < TOTAL_PICTURE_COUNT) { Serial.println("call takePicture()"); CamImage img = theCamera.takePicture(); if (img.isAvailable()) { char filename[16] = {0}; sprintf(filename, "PICT%03d.JPG", take_picture_count); Serial.print("Save taken picture as "); Serial.print(filename); Serial.println(""); theSD.remove(filename); File myFile = theSD.open(filename, FILE_WRITE); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); } else { Serial.println("Failed to take picture"); } } else if (take_picture_count == TOTAL_PICTURE_COUNT) { Serial.println("End."); theCamera.end(); } take_picture_count++; }

制御

I2Cで取得したIMUのデータをもとにモータの回転を制御します。
ここは先人の知恵(制御計算式)を借りて、バランスを取る制御を検討することにします。

モータの回転= k1 × 躯体の傾き(角度)+k2 × 躯体の傾きの変化率(角速度)
          +k3 × 車輪の移動速度 +k4 × 車輪の移動距離

その場で姿勢制御させるのみであれば、IMUの角速度のデータと変数パラメータを適切にすることで倒立振子が可能です。
あとはk1,k2のパラメータを適切に設定すればOKです。
k3,k4のパラメータを使って移動させる場合は、エンコーダのデータ取得が必要になります。

今後の課題

姿勢制御のパラメータ調整と
倒立振子の移動+遠隔操作に、次は挑戦したいと思います。

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