ゆるエンジニアはいろいろ遊びたい

FAエンジニアが週末にいろいろ遊ぶブログです

クモ型ロボを作っていたらカメ型ロボになってた話

Miuzeiの10個入りマイクロサーボと、3Dプリンタを購入したのでロボットを自作してみようと思います。
2足歩行ロボットは難易度が高そうだったので今回は2関節4脚のクモ型ロボットを作ります。
ハードウェアの構成は以下の通り

  • マイコン RaspberryPi Zero 2W ¥3,190(2025/12 購入) 秋月電子
  • マイクロサーボ Miuzei マイクロサーボ 9g 180° 10個入り @260×4個 ¥1,040 Amazon
  • マイクロサーボ YFFSFDC マイクロサーボ SG90型 4個入り ¥999 Amazon
  • サーボドライバ KKHMF PCA9685 ¥999 Amazon
  • モバイルバッテリー 5000mha ¥700 ダイソー
  • USBケーブル2個(タイプB) ¥100 ダイソー

合計:7,028
こう見てみるとラズパイが高いですね。金額の半分を占めています。
秋月電子さんでは現在入荷未定となっており、Amazonでは¥700ほど高い状況です。
サーボについては、Miuzeiのマイクロサーボは重量がかかる部分での定常状態のジッターが激しかったため、
急遽以前購入していたSG90を使用しました。
電源はダイソーのモバイルバッテリーですが、ラジコン用のリポバッテリーの方が出力的には安心だと思います。
今回は安価に済ませるためにダイソーさんを使用しました。
設計はAutoDeskのFusionを使います。個人利用に限り無料で使用できます。Fusionを使用して利益を出す場合は使えません。
当ブログは収益化していませんし、図面及び製作物の販売等はしませんのでOKだと思います。
脚部の構造はこちら

青いモーターがSG90で、旋回用です。Hipと呼びます。
白いモーターはMiuzeiで、足の上げ下げ。Kneeと呼びます。
ベースの構造はこちら

モーターとバッテリーを取り付けます。
この上にラズパイとドライバを取り付けるプレートが乗ります。

組立図はこちら

なかなかいい感じです。
実は最初はプロトタイプを設計し、作ってみたのですが全ての足をMiuziのサーボにすると先ほど申し上げたジッターが発生したので、再設計しました。
その際に脚部の位置がセンターからオフセットしていることに気づき、ついでにモーターの取り付け方法から見直しました。
プロトタイプはこちら

Hipのモーターの取り付け方法が異なっています。
これはこれでかっこいい。
これを歩かせる訳ですが、4脚ロボットは1脚上げるととたんに不安定になることがわかりました。
3つの足が接している点を結んだ三角形が、重心の半分以上を占めていないと上げた足側に倒れてしまいます。
そのため、1本の足を上げるためにはまず隣の足を動かす必要がありました。
そして、進行方向に1本の足を移したら、他3本の足を滑らせるように後ろに移動させて前方に状態を移します。
これを左右交互に繰り返すと、カメが這いずるような動きで歩く事ができます。
この動きをクロール歩容と呼ぶらしいです。


これはプロトタイプで、HipにMiuzeiのサーボを使っています。足を動かす時は発生していないのですが、止めた時にジッターが発生しました。
SG90に変えたところジッターは発生しなくなりました。
完成までの道のりをショートムービーにしてみました。
ダンスさせたりもできました。個人的に伏せのモーションがツボです。
構想時はクモ型ロボだったのですが、もうどう見てもカメです。
でも、かわいいからOK。

Bambu lab A1 miniを買ってみた

電子工作を幾つかやるようになると、サーボなどの駆動部品をどうやって取り付けるかという悩みが発生します。
産業用のモーターは主軸が丸シャフトで、取り付け穴も軸と並行なものが多く、そこまで取り付けに悩むという事は発生しません。
初めてSG90のようなマイクロサーボを見たとき、シャフトはプラスチックでギザギザが付いていて、取り付け穴は変な位置にあるのでなんと使いにくいことかと嘆いたものです。
3Dプリンタがあればもっと自由に設計できるのになぁ。と思っていたところ、BambuLab A1miniという商品が初心者にオススメだという話を聞き、この度購入と至りました。
セールで3万円以内で買えるという事で3Dプリンタも気軽に変えるいい時代になったのだなと感心しております。

箱はちょっと大きいなという感じ。

いざオープン。
おお、ロゴが出てきた。この辺の購入者を盛り上げる感じはいいと思います。

発砲スチロールを使わない梱包。好きです。再現できないと思うけど。

箱から出すと思ったより小さい。
付属品はいろいろありますが、スタートガイドがあるのでそれを見ながら立ち上げました。
気を付ける点は、

  • 電源プラグがアース極がある3Pタイプなので、変換するか適合するタップが必要
  • 公式アプリをダウンロードして、あらかじめログインしておくと吉
  • 起動時のサウンドの音量がバカでかい
  • タッチパネルの反応は悪いのでミスタイプ頻発。wifiのパスワード入力が大変だった。
  • PTFEチューブを差す穴が4つあるがどこに差してもOK
  • セットアップしていくとY軸にオイルを塗れと言われる。Y軸とは下側の前後に動くテーブルのスライダで、電源OFFで動かせる。オイルは超垂れるから気を付けよう
  • ビルドプレート(プリントされる部分の板)は素手で触ると油分が悪さするらしいので手袋しよう
  • キャリブレーションは長い。時間のある時にセットアップしよう
  • 本体は超揺れる。安定した台に置こう
  • サンプルフィラメントはすぐ無くなる。PTFEチューブ内まで飲み込まれる前に外そう。

サンプルフィラメントがついているので、それを使って本体に記憶されているワークを造形してみました。
超揺れました。


出来上がり

横穴の造型は不得意のようです。穴の頂点が雑になります。
Bambu labのアプリなどでいろんなモデルが公開されているので、それだけでもいろいろ遊べて楽しいです。
3Dプリンタで部品を作り、生成AIでプログラムを作れる。すごい時代になったという事を今さら感じております。

Miuzeiのサーボモーター10個入りについてレビュー

アマゾンでマイクロサーボを探していたら、Miuzeiというメーカーのサーボが10個で2599円ですごく安かったので買ってみました。
メタルギヤと表記されているのですが、先端の取り付け部分はプラスチックだったり、200°制御可能とか書いているけどどう見ても200°開いていない商品画像だったりと非常に怪しい商品です。
最近色々買いすぎて金欠気味で高価なモーターには手が出ないので、とりあえず動けばいいの精神で購入してみました。
パッケージはこちら

仕様は
サイズ:22.4*12.5*22.8
ホーンギアスプライン:20T
重量:10g
直径:4.9mm
負荷電流:90mA
ギアタイプ:5プラスチック
なんと、SG90よりちょっと小さいです。つまり、コンパチできません。
サーボホーンは共通で使えたのでそれはまぁ良かった。
ケースをばらしてみると、

ちゃんとギヤはプラスチックでした。メタルギヤではありません。
動作はどうなのか、サーボドライバPCA9685を使って確認しました。
電源はダイソーのモバイルバッテリーです。

import time
import argparse
import Adafruit_PCA9685


def parse_args():
    p = argparse.ArgumentParser(description='PCA9685 10ch サーボ動作確認')
    p.add_argument('--channels', '-c', nargs='+', type=int, default=list(range(10)),
                   help='テストするチャンネル番号のリスト(デフォルト 0-9)')
    p.add_argument('--min', type=int, default=140, help='サーボ最小パルス(デフォルト150)')
    p.add_argument('--center', type=int, default=350, help='サーボ中央パルス(デフォルト375)')
    p.add_argument('--max', type=int, default=600, help='サーボ最大パルス(デフォルト600)')
    p.add_argument('--delay', type=float, default=0.1, help='各位置での待機秒数')
    p.add_argument('--cycles', type=int, default=3, help='各サーボ繰り返し回数')
    p.add_argument('--address', type=lambda x: int(x,0), default=0x40, help='PCA9685 I2Cアドレス(16進も可)')
    return p.parse_args()


def set_servo(pwm, ch, pulse):
    pwm.set_pwm(ch, 0, int(pulse))


def test_servo_smin(pwm, ch, smin, scenter, smax, delay, cycles):
    print(f"smin Testing channel {ch}: min={smin} center={scenter} max={smax} (cycles={cycles})")
    try:
        for i in range(cycles):
            set_servo(pwm, ch, smin)
            time.sleep(delay)

    except KeyboardInterrupt:
        raise

def test_servo_smax(pwm, ch, smin, scenter, smax, delay, cycles):
    print(f"smax Testing channel {ch}: min={smin} center={scenter} max={smax} (cycles={cycles})")
    try:
        for i in range(cycles):
            set_servo(pwm, ch, smax)
            time.sleep(delay)

    except KeyboardInterrupt:
        raise

def test_servo_center(pwm, ch, smin, scenter, smax, delay, cycles):
    print(f"centerTesting channel {ch}: min={smin} center={scenter} max={smax} (cycles={cycles})")
    try:
        for i in range(cycles):
            set_servo(pwm, ch, scenter)
            time.sleep(delay)

    except KeyboardInterrupt:
        raise

def sweep_servo(pwm, ch, smin, scenter, smax, delay, cycles):
    # 滑らかに往復させる(小刻み)
    steps = 20
    for _ in range(cycles):
        for t in range(steps + 1):
            v = smin + (smax - smin) * (t / steps)
            set_servo(pwm, ch, v)
            time.sleep(delay / steps)
        for t in range(steps + 1):
            v = smax - (smax - smin) * (t / steps)
            set_servo(pwm, ch, v)
            time.sleep(delay / steps)


def main():
    args = parse_args()

    pwm = Adafruit_PCA9685.PCA9685(address=args.address)
    pwm.set_pwm_freq(60)

    try:
        for ch in args.channels:
            # まず中心へ
            set_servo(pwm, ch, args.center)
            time.sleep(0.2)

        # 各チャネル順にテスト
        for ch in args.channels:
            test_servo_smin(pwm, ch, args.min, args.center, args.max, args.delay, args.cycles)
        for ch in args.channels:
            test_servo_smax(pwm, ch, args.min, args.center, args.max, args.delay, args.cycles)
        for ch in args.channels:
            test_servo_center(pwm, ch, args.min, args.center, args.max, args.delay, args.cycles)

        print('Sweep test (all channels)')
        for ch in args.channels:
            sweep_servo(pwm, ch, args.min, args.center, args.max, args.delay, 1)
            time.sleep(0.2)


        print('Done. Returning all servos to center')
        for ch in args.channels:
            set_servo(pwm, ch, args.center)
            time.sleep(0.2)

    except KeyboardInterrupt:
        print('\nInterrupted by user')
    finally:
        # 停止: 0パルスをセットして停止
        for ch in args.channels:
            try:
                pwm.set_pwm(ch, 0, 0)
                time.sleep(0.5)
            except Exception:
                pass


if __name__ == '__main__':
    main()

copilotに書いてもらったら、argparseという今まで使ったことのないモジュールを使ってくれました。
これはコマンドラインで実行する際に、引数を与える事ができるツールらしいです。
まぁ、気にせず実行してみると10個全て動きました。


さすがにダイソーのモバイルバッテリーだと10個一気に動かすとラズパイが落ちましたが、別々なら大丈夫そうです。
これでいろいろ遊べそう。

ラズパイカーにLEDライト実装

ラズパイカーにLEDライトを実装してみました。
暗い場所とか、カメラモジュールをナイトモードにしてもあまり効果が感じられないので、ヘッドライトが欲しいなと思っていました。
使ったのはダイソーの小さいライト。これを分解してLED部分だけ使います。

回路図はこちら

パワーのあるLEDを直接ON,OFFするとラズパイがぶっ飛ぶかもしれないので、トランジスタを使用して開閉します。
ちょうど初心者ラズパイキットの中にSS8050というトランジスタがあったので、これに同じく初心者キットに入っていた抵抗10Ωを直列に2つつなげて20ΩにしたものをLEDに接続します。
ラズパイのGPIOはピン番号22番のGPIO25を使用します。トランジスタのベースから1kΩの抵抗をかませています。
トランジスタの動作に関しては20年以上前に学校で習ったような気がしますが、すっかり忘れています。
確かベース-エミッタ間に少量の電流を流す事で、コレクタ-エミッタ間に電流が流れる・・・みたいな感じだったような・・・
配線は小型のブレッドボードを使ってやりました。
LEDのサンプルコードはこちら

from gpiozero import PWMLED
from signal import pause
import time


led = PWMLED(25)  # GPIO25番ピンに接続されたLED

try:
    while True:
        print("50% brightness")
        led.value = 0.5  # 明るさ50%
        time.sleep(2)

        print("LED OFF")
        led.value = 0.0  # LED消灯
        time.sleep(2)

        #フェードイン
        print("Fading in")
        led.pulse(fade_in_time=2, fade_out_time=2, n=1, background=False)
except KeyboardInterrupt:
    led.off()
    print("Program stopped")

ソフトウェアPWMを使う事で明るさの調整ができます。
これをラズパイカーに実装して、PS4コントローラのイベントを取得してONさせれば完成です。


これで暗くても安心!

ラズパイカーにカメラ上下機構を追加した

ラズパイゼロ2Wを使ってラズパイカーを製作しています。
前回、カメラをブラウザでモニターしながらPS4コントローラでタイヤの操作ができるようになりました。
今回はカメラを上下に首振りする機能を追加します。
電子工作ではおなじみの、マイクロサーボSG90とサーボドライバPCA9685を使用しました。
PCA9685ドライバは、I2Cという通信でサーボを16台制御することができるようです。
そんなにたくさん制御する気はありませんが、360°サーボを使えばL298Nを使わなくてもラジコンが作れそうです。
あいにく360°サーボは持っていないので、首振り機能だけのためにドライバーを使います。
回路図は以下

ラズパイ側は、事前にI2Cを有効にしておきます。デスクトップからラズベリーパイの設定の項目で変更するのが簡単です。
Pythonプログラムは、adafruit-pca9685というモジュールを使うと動きました。
似たようなものに、Adafruit-CircuitPythonというモジュールもあるようで、我が家のチャッピーはこちらを使ったコードを書いてくれたのですが、全然動きませんでした。デューティー比関係の書き方がおかしかったみたいですが、よくわからなかったので動いた方を使います。
まずは、モジュールのインストール。

sudo pip install adafruit-pca9685

サンプルコードは以下

import Adafruit_PCA9685
import time

# PCA9685初期設定
pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(60)


def main():
    while True:
        pwm.set_pwm(0, 0, 150)
        time.sleep(1)
        pwm.set_pwm(0, 0, 650)
        time.sleep(1)


if __name__ == '__main__':
    main()

サーボモーターのPWM制御については、過去にrpi_hardware_pwmを使用したことがあります。
slowtech.hateblo.jp
この時は、デューティー比を直接入力したのですが、今回は少し違っています。

pwm.set_pwm(チャンネル番号, ON, OFF)

この部分がPWM制御の文ですが、「指定したチャンネル番号のPWM出力を、ONで指定したカウント目でHIにし、OFFで指定したカウントでLOWにする」
という意味です。PCA9695の内部クロックは1周期が0~4095カウントあるので、その中でどのくらいの時間ONにするかを設定するイメージです。

1カウント = 20ms / 4096 ≒ 0.00488ms
角度 パルス幅 カウント
0°  0.5ms   102
90°  1.5ms    307
180° 2.5ms   512

なんかきれいな数字にならないのはモヤっとしますが、気にしないでおきます。
ラズパイカーに実装する際は、PS4コントローラーのL2,R2トリガーで上下できるようにしてみました。


カメラを上下に動かせるようになったので、より周りを良く見れるようになりました。
とてもいい感じです。

やっぱりラズパイは手返しが早くて良い。
Arduinoはいちいちコンパイルしなくてはならず、その分待ち時間が発生してしまいますからね。

ラズパイカーを作った

ラズパイゼロ2Wでカメラのストリーミングとモーターのコントロールができたので、ラジコンカーを作ってみました。
操作はPS4コントローラで、ブラウザでカメラの映像を見ながらリモートコントロールできます。
電源はダイソーで購入したモバイルバッテリーです。
t=3mmの塩ビプレートに全て載せてみました。

重量があるため2WDだと少し力が弱いですが、モニターを見ながら操作するとなかなか楽しいです。
カメラモジュールV2は、ターミナルで以下のコマンドを入力すると設定値が見れます。

v4l2-ctl -d /dev/video0 --all

defaultが初期値で、valueが現在の設定値です。
ラズパイを再起動すると初期値に戻るようなので、systemdに設定値を入れておくと便利です。

[Unit]
Description=MJPG Streamer (Night Mode)
After=network.target
Wants=network.target

[Service]
Type=simple

# === カメラ初期化(超重要)===
ExecStartPre=/usr/bin/v4l2-ctl -d /dev/video0 --set-ctrl=auto_exposure=0
ExecStartPre=/usr/bin/v4l2-ctl -d /dev/video0 --set-ctrl=iso_sensitivity_auto=1
ExecStartPre=/usr/bin/v4l2-ctl -d /dev/video0 --set-ctrl=white_balance_auto_preset=1
ExecStartPre=/usr/bin/v4l2-ctl -d /dev/video0 --set-ctrl=scene_mode=8

# === mjpg-streamer 本体 ===
ExecStart=/usr/local/bin/mjpg_streamer \
  -i "input_uvc.so -d /dev/video0 -r 640x480 -f 15" \
  -o "output_http.so -p 8080 -w /usr/local/share/mjpg-streamer/www"

Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

auto_exposureは自動露出の設定で、0で自動になります。
iso_sensitivity_autoはISOの自動設定で、1で自動
white_balance_auto_presetはホワイトバランスで1で自動
scene_modeは0が昼、8が夜、11がスポーツモード
今回はナイトモードにしてみました。
あまり変化がわからなかったので、また今度検証してみようかな。

RaspberryPi Zero2Wでモーターをゲームパッドで操作してみた

倒立振子を作るためにFEETECHのFM90を買ったのですが、4個セットだったので2個余っていました。
ラズパイゼロ2Wでも使ってみたかったので、モータードライバL298Nを購入し、ゲームパッドで動かしてみました。

アマゾンで2個750円くらい。安いのですがアマプラに加入していないと送料を取られます。
L298Nは最大電圧12V、駆動電流2Aで、入力の電源により出力電圧が変わるようです。
今回はドライバの電源は単3電池4本の6Vでモーターを動かします。ラズパイの電源は別途USBから給電しています。
配線図は以下

ラズパイ5ではハードウェアPWMを使うためにconfig.txtをいじる必要がありましたが、ゼロ2wではpigpioが使えるのでいらないみたいです。
今回はPS4のコントローラをBluetoothで接続して動かしてみました。
まずは必要モジュールのインストールから。ラズパイのターミナルで実行します。

sudo apt update
sudo apt install pigpio
sudo systemctl enable pigpiod
sudo systemctl start pigpiod
sudo apt install python3-evdev

pigpioはすでにインストールされてました。
evdevはコントローラを使うために必要みたいです。
PS4コントローラとの接続はラズパイのデスクトップ上でBluetoothのアイコンからデバイスを追加することでできました。
バイスの番号を調べるには、

ls /dev/input

でわかります。event1がパッドのジャイロ、event2がパッドの操作です。
プログラムは以下の通り

from gpiozero import Motor, PWMOutputDevice
from gpiozero.pins.pigpio import PiGPIOFactory
from evdev import InputDevice, ecodes
from threading import Thread
import time

# ---------- pigpio ----------
factory = PiGPIOFactory()

# ---------- GPIO設定 ----------
LEFT_EN  = 18
LEFT_IN1 = 23
LEFT_IN2 = 24

RIGHT_EN  = 19
RIGHT_IN1 = 27
RIGHT_IN2 = 22

# PWMは低め(重要)
left_pwm  = PWMOutputDevice(LEFT_EN,  frequency=200, pin_factory=factory)
right_pwm = PWMOutputDevice(RIGHT_EN, frequency=200, pin_factory=factory)

left_motor  = Motor(forward=LEFT_IN1,  backward=LEFT_IN2,  pin_factory=factory)
right_motor = Motor(forward=RIGHT_IN1, backward=RIGHT_IN2, pin_factory=factory)

# ---------- PS4コントローラ ----------
gamepad = InputDevice('/dev/input/event2')  # 必要なら変更
print(gamepad)

# ---------- 状態 ----------
LEFT_GAIN  = 1.0
RIGHT_GAIN = 0.69

left_val  = 0.0
right_val = 0.0
running   = True

DEADZONE = 10
MAX_PWM  = 0.7   # 最大速度制限(安全)

# ---------- ユーティリティ ----------
def clamp(v, min_v=0.0, max_v=1.0):
    return max(min(v, max_v), min_v)

def stick_to_power(value):
    if value is None:
        return 0.0

    center = 128
    diff = value - center

    if abs(diff) < DEADZONE:
        return 0.0

    power = diff / 127.0
    power = max(min(power, 1.0), -1.0)

    # 前倒しで前進にする
    return -power

def drive(left, right):
    # 左
    if left >= 0:
        left_motor.forward()
        left_pwm.value = clamp(left * MAX_PWM)
    else:
        left_motor.backward()
        left_pwm.value = clamp(-left * MAX_PWM)

    # 右
    if right >= 0:
        right_motor.forward()
        right_pwm.value = clamp(right * MAX_PWM)
    else:
        right_motor.backward()
        right_pwm.value = clamp(-right * MAX_PWM)

def stop():
    left_pwm.value = 0
    right_pwm.value = 0
    left_motor.stop()
    right_motor.stop()

# ---------- モーター制御スレッド(50Hz) ----------
def motor_loop():
    while running:
        drive(left_val, right_val)
        time.sleep(0.02)   # 50Hz

motor_thread = Thread(target=motor_loop, daemon=True)
motor_thread.start()

# ---------- メイン(入力処理のみ) ----------
try:
    print("PS4 Tank Drive Start")
    
    for event in gamepad.read_loop():
        
        if event.type == ecodes.EV_ABS:
            if event.code == ecodes.ABS_Y:
                power = stick_to_power(event.value)
                left_val  = power * LEFT_GAIN
            elif event.code == ecodes.ABS_RY:
                power = stick_to_power(event.value)
                right_val = power * RIGHT_GAIN
        '''左右1スティック動作
        if event.code == ecodes.ABS_Y:
            power = stick_to_power(event.value)
            left_val  = power * LEFT_GAIN
            right_val = -power * RIGHT_GAIN
        '''  


except KeyboardInterrupt:
    print("Exit")

finally:
    running = False
    stop()
    left_pwm.close()
    right_pwm.close()
    left_motor.close()
    right_motor.close()

実行するとPS4コントローラで動かせました。
左右のモーターで出力差があったため、遅い方のモーターに合わせて出力調整をしています。


やっぱり精密な制御の場合はエンコーダがあるモーターじゃないと無理っぽいです。
今までFA機器を使っていてここまで個体差がある部品を使った事がなかったので、やはり産業用機器は精度が全然違うんだなぁ。と思いました。
もう少し慣れてきたらエンコーダ付きのモーターを使ってみたいです。
前回ブラウザでカメラモジュールのストリーミングができるようになったので、これでモニター付きラジコンカーを作れるようになりました。