見出し画像

ロボット製作日記3 PS4コントローラでラズパイをモーター制御してみた

こんばんは、横山です。

なんかあっという間にGW:ゴールデンウィークが終わってしまいましたね。

皆さんはいかがお過ごしでしたでしょうか?

僕は仲間内で飲みに行ったり、実家に帰省したり、本を読んだり、筋トレしたり、バーに行ったり。

めちゃくちゃ充実しておりました。

その分、仕事を頑張らねば!っと気合い入れて仕事始めております。

さてGW中にロボット製作についても頑張ってまいりました。

pythonでかなり苦戦しましたが、下記がやっとできるようになりました。

  1. ラズパイ→PCA9685→モーター(SG-90)制御

  2. ラズパイ⇔PS4コントローラ(Dual Shock 4)をBluetooth接続

  3. Dual Shock 4(Bluetooth)⇔ラズパイ→PCA9685→SG-90でモーター制御

こんな感じで動作します。

後述に詳細記載しますが、1, 2についてはそこそこ簡単にできます。

ただ3が僕的にはかなり厄介でした。
何しろPythonを触ったことがないからです。

そのためPythonの本を読み漁り、色々と試行錯誤で試しまくった結果、なんとか制御できました。

今回はそのまとめになります。

まずは今回のハードウェア構成から見ていきましょう!

ハードウェア

今回使用した部品は、電子工作にスタンダードなものばかりを揃えています。

部品は大きく分けると4つあります。

ラズパイ、Dual Shock 4、PCA9685、サーボモーター(SG90)

ハードウェア 全体写真

PCA9685はラズパイのI2C信号を受けて、サーボモーターをPWM駆動できるドライバです。

I2C信号の詳細説明はここでは割愛します。
ラズパイとPCA9685を繋ぐ信号ぐらいに考えてもらえればOKです。

興味のある人は説明リンクも参考がてら貼っておきます。

サーボモーターは電子工作にど定番のSG-90を買いました。

こちらは5V電源(Vcc)で動くモーターです。

全体ブロック図

全体ブロック図

これらの部品を上図のような構成で接続します。

部品間をジャンパー線で接続していきます。

ラズパイとPCA9685は①〜④の信号線で接続します。

ラズパイ自身は3.3Vの電源で動いており、ラズパイからのI2C制御信号の電圧は3.3Vです。

そのためPCA9685を制御する際は、ラズパイの3.3Vの電圧をPCA9685に供給する必要があります。

モーターは5V電源で動くため、ACアダプタから⑤5V電源を供給します。

ラズパイにも5V電源は存在しているのですが、モーターを複数駆動するだけの電流量が足りません。

そのため別途ACアダプタから電源を供給する必要があります。

ラズパイの5V電源(②、⑧)はCPU冷却用のファンに使用するぐらいが丁度いいです。

PCA9685とモータ(SG-90)はSG-90に同接されているコネクタを直接PCA9685に差し込みます。

ここで注意する必要があるのが、SG-90の3本の線(②茶:GND、⑤赤:5V、⑦黄:PWM)とPCA9685の接続です。

上図のブロック図のように、左から②茶:GND、⑤赤:5V、⑦黄:PWMと繋いでください。

仮に逆に繋いでしまっても壊れないとは思いますが、PWMとGNDが逆になるのでモーターは動かないです。

部品リスト(参考)

ご参考までに部品リストも下記に記載します。

・ジャンパー線

・Dual Shock 4 CUH-ZCT2J

・サーボ&PWM駆動キット PCA9685

・マイクロサーボモーター SG90

・2.1mm標準DCジャック ⇔ 端子台

・スイッチングACアダプター
   5V4A AD-A50P400

・RIGOL DS1054Z
※あったらいい物:オシロスコープ
ラズパイやPCA9685からの制御信号が正常に出力されているかを確認するのに便利ですね!

電子工作が好きな方は、お金に余裕があったら購入した方がいい一品です。

オシロスコープは各社ピンキリなんですが、RIGOL社のオシロは性能が高く、費用対効果が大きいです。

しょぼいオシロは、壊れやすいし、波形が正常に読み取れなかったり、対応周波数がしょぼかったりするので買った後で後悔することが結構あります。

1. ラズパイ→PCA9685→モーター(SG-90)制御

まずはDual Shock 4を接続せずに、ラズパイからI2C信号を経由してPCA9685を制御し、モーターを動かしていきます。

改めて全体ブロック図を見てみましょう。

全体ブロック図

ラズパイからI2C信号(③SDA,④ SCL)でPCA9685に指令が送られます。

その指令に合わせて、PCA9685はモーターへ⑦PWM信号を送信し、モーターを回転させます。

ソフトウェア設定

まずはラズパイ上のI2C信号を有効化する必要があります。

ラズパイの「設定」→「Raspberry Piの設定」のインターフェース画面からI2Cを有効化します。

ラズパイの「設定」→「Raspberry Piの設定」のインターフェース画面

次にPCA9685制御用のPythonライブラリをインストールします。

$ sudo apt-get install git build-essential python-dev
$ git clone https://github.com/adafruit/Adafruit_Python_PCA9685.git
$ cd Adafruit_Python_PCA9685
$ sudo python setup.py install

たまに上記でもインストールできない場合があるようです。
その場合は下記コマンドも試してみましょう。

$ sudo pip install adafruit-pca9685

これで問題なくインストールできるはずです。

※ 2024/1/6更新
最新OS Book wormからpipインストール時にexternally managed errorが発生します。

対策方法は色々あるようですが、僕も正解は分かっていません。

下記記事に対策サイトや内容をまとめています。

次にモーター制御のPythonを組み込んでいきます。

こちらのサイトのモーター制御を参考にしました。

作成したPythonのプログラムがこちら

※ 2024/1/6更新
①たまにRuntimeエラーでプログラムが実行できない場合があります。

その際は下記サイトを参考にI2C.pyファイルのaddressの後に、"1"を設定してみてください。

※ 2024/1/6更新
② pythonコードでTabとスペースで行間が混在しているとindexエラーがでます。
そのままコピペいただいた後に、必要に応じてプログラムの行間をTabかスペース(半角or全角)に統一するようにお願いします。

import Adafruit_PCA9685
from time import sleep

# 設定周波数(Hz)
set_freq = 50

# 動作角度からPCA9685に渡す値を変換
def convert_deg(deg,freq):

	# 分解能(ステップ数)
	step = 4096

	# 接続サーボモーターの最大最小角度時のパルス間隔(ms)
	# この数値はSG90の仕様に基づきますが、必要に応じて調整してください。
	max_pulse = 2.4 # 90°時
	min_pulse = 0.5 # -90°時

	# サーボモーターの0°時のパルス間隔(ms)
	center_pulse = (max_pulse - min_pulse) / 2 + min_pulse

	#サーボモーター1°あたりのパルス間隔(ms)
	one_pulse = round((max_pulse - min_pulse) / 180, 2)

	# 要求角度のパルス間隔(ms)を算出しPCA9685に渡す値を算出
	deg_pulse = center_pulse + deg * one_pulse
	deg_num = int(deg_pulse / (1.0 / freq * 1000 / step))

	# デバッグ
	print('deg:' + str(deg) + '(' + str(deg_num) + ')')

	return deg_num

pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(set_freq)

try:
	while True:
		pwm.set_pwm(0, 0, convert_deg(0,set_freq))
		sleep(1)
		pwm.set_pwm(0, 0, convert_deg(45,set_freq))
		sleep(1)
		pwm.set_pwm(0, 0, convert_deg(0,set_freq))
		sleep(1)
		pwm.set_pwm(0, 0, convert_deg(-45,set_freq))
		sleep(1)

except KeyboardInterrupt:
	pwm.set_pwm(0, 0, 0)
	print("KeyboardInterrupt")
	pass

Pythonのプログラムを説明します。

set_freqにて、PWM信号の周波数を設定。
今回使用するモーターはSG-90なので50Hz(20ms)に設定します。

SG-90の制御仕様図

SG-90の制御仕様図に記載があるように50Hzの⑦PWM信号(Orange)をモーターに入力すると回転します。

Duty Cycleを0.5ms〜2.4msに変更すれば回転角度を決定できます。

そのためmax_pulse =  2.4(ms)、min_pulse = 0.5(ms)に設定します。

max_pulse = 2.4msの時は、90℃回転します。min_pulse = 0.5msの時は-90℃回転します。

モータードライバであるPCA9685は12bit(2の12乗) = 4096でPWM出力を刻める性能を持っています。

そのためstep = 4096を入力します。

モーターの0℃地点は、center_pulseの項目で導くことができます。

1℃ステップでモーターを動かしたい時は、one_pulseの項目で算出できるようにしています。

set_pwm関数は左から順に
チャンネル番号
②PWM信号の開始値(常時0設定でOK)
③サーボモーターを回転させたい位置のPWM値

を表しています。

※②については、僕もパラメーターを色々変更してみたのですが、結局何を制御しているのかよく分かりませんでした。

While true:の次の行のpwm.set_pwm(0, 0, convert_deg(0,set_freq))という関数は、PCA9685の0チャンネル目、PWM開始値0、0°の位置へ回転させることを意味します。

例えばこの関数をpwm.set_pwm(2, 0, convert_deg(45,set_freq))に変更すると、PCA9685の2チャンネル目、PWM開始値0、45度の位置へモーターを回転させることができます。

この関数を増やすことで最大16チャンネルのモーターを制御することが可能です。

このコードを動作させると、このコードを実行すると、1秒毎に0チャンネル目に接続されたモーターが0°→45°→0°→-45°の位置へ回転する動きをします。

Ctrl + Cでプログラムを中断するまで繰り返します。

ぜひ試してみてください!

2. ラズパイ⇔PS4コントローラ(Dual Shock 4)をBluetooth接続

さてラズパイとPS4コントローラの接続方法について記載していきます。

まずラズベリーパイにGithubからソフトウェアをインストールする必要があります。

インストールややり方については、こちらのメタエレさんのブログが分かりやすいので参照ください。

これで問題なく、ラズパイとDual Shock 4をBluetooth接続できます。

諸々インストールした後に、ターミナル or Python上で下記コードを入力すると、Dual Shock 4のキーボタンが表示されるはずです。

ここまで動作確認できればOKです。

動作確認コード

from pyPS4Controller.controller import Controller


class MyController(Controller):

    def __init__(self, **kwargs):
        Controller.__init__(self, **kwargs)


controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen()

3. Dual Shock 4(Bluetooth)⇔ラズパイ→PCA9685→SG-90でモーター制御

さていよいよ大詰めです。

Dual Shock 4からボタン入力すると、モーターが指定の位置へ回転するコードを記載していきます。

今回は、Dual Shock 4のL3/R3のアナログスティックの傾きに応じて、モーターの回転位置を変更するコードを作成しました。

まずは下記コードを示します。

動作コード

# -*- coding: utf-8 -*-
from __future__ import division
import Adafruit_PCA9685
from time import sleep
from pyPS4Controller.controller import Controller

# 設定周波数(Hz)
set_freq = 50

# 動作角度からPCA9685に渡す値を変換
def convert_deg(deg,freq):

	# 分解能(ステップ数)
	step = 4096

	# 接続サーボモーターの最大最小角度時のパルス間隔(ms)
	# この数値はSG90の仕様に基づくものですが、必要に応じて調整してください。
	max_pulse = 2.4 # 90°時
	min_pulse = 0.5 # -90°時

	# サーボモーターの0°時のパルス間隔(ms)
	center_pulse = (max_pulse - min_pulse) / 2 + min_pulse

	#サーボモーター1°あたりのパルス間隔(ms)
	one_pulse = round((max_pulse - min_pulse) / 180, 2)

	# 要求角度のパルス間隔(ms)を算出しPCA9685に渡す値を算出
	deg_pulse = center_pulse + deg * one_pulse
	deg_num = int(deg_pulse / (1.0 / freq * 1000 / step))

	# デバッグ
	print('deg:' + str(deg) + '(' + str(deg_num) + ')')

	return deg_num

pwm = Adafruit_PCA9685.PCA9685()
pwm.set_pwm_freq(set_freq)

# Translate controller input into motor output values

def transf(raw):
    temp = (raw+32767)/65534
    # Filter values that are too weak for the motors to move
    if abs(temp) < 0.9:
        return 0
    # Return a value between 0.3 and 1.0
    else:
        return round(temp, 1)

def transf1(raw):
    temp = (abs(raw)+32767)/65534
    # Filter values that are too weak for the motors to move
    if abs(temp) < 0.9:
        return 0
    # Return a value between 0.3 and 1.0
    else:
        return round(temp, 1)

class MyController(Controller):

    def __init__(self, **kwargs):
        Controller.__init__(self, **kwargs)
    
    def on_L3_down(self, value):
        value = transf(value)
        if value == 0:
            pwm.set_pwm(0, 0, convert_deg(0,set_freq))
            pwm.set_pwm(15, 0, convert_deg(0,set_freq))
            print(value)
        else:
            pwm.set_pwm(0, 0, convert_deg(90,set_freq))
            pwm.set_pwm(15, 0, convert_deg(90,set_freq))
            print(value)
            
    def on_L3_up(self, value):
        value = transf1(value)
        if value == 0:
            pwm.set_pwm(0, 0, convert_deg(0,set_freq))
            pwm.set_pwm(15, 0, convert_deg(0,set_freq))
            print(value)
        else:
            pwm.set_pwm(0, 0, convert_deg(-90,set_freq))
            pwm.set_pwm(15, 0, convert_deg(-90,set_freq))
            print(value)

    def on_R3_down(self, value):
        value = transf(value)
        if value == 0:
            pwm.set_pwm(1, 0, convert_deg(0,set_freq))
            print(value)
        else:
            pwm.set_pwm(1, 0, convert_deg(90,set_freq))
            print(value)
            
    def on_R3_up(self, value):
        value = transf1(value)
        if value == 0:
            pwm.set_pwm(1, 0, convert_deg(0,set_freq))
            print(value)
        else:
            pwm.set_pwm(1, 0, convert_deg(-90,set_freq))
            print(value)

    def on_x_press(self):
       pwm.set_pwm(0, 0, convert_deg(90,set_freq))

    def on_x_release(self):
       pwm.set_pwm(0, 0, convert_deg(-90,set_freq))

controller = MyController(interface="/dev/input/js0", connecting_using_ds4drv=False)
controller.listen()

Dual Shock 4のアナログスティックは上下目一杯まで倒すと、アナログ値としては±32767を出力します。

アナログスティックを傾けた時にどのような値を出力するかは、2項で説明したコードを入力すれば、Python or ターミナル上で確認することができます。

今回は単純な制御を行ってみたかったので、アナログスティックを目一杯倒した時は90°、デフォルト位置の時は0°に回転するように設定してみました。

例えば def on_L3_up(self, value):の変数は、Dual Shock 4のL3スティックを上に倒した時の制御を表しています。PCA9685に接続された0チャンネルと15チャンネルのモーターを制御しています。

アナログスティックがデフォルト(中心)にあるときは、モーターも0°設定です。
上に目一杯倒すと、90°の位置に回転し、スティックをデフォルト(中心)に戻すと、モーターも0°位置に回転します。

このように、各ボタンにそれぞれの変数を割り当てていけば、簡単にDual Shock 4からモーター制御できるようになる訳です。

動作確認

こちらがPythonで作ったコードを実行した時の動作になります。

やっと動いた〜〜!っと感動した瞬間でした(笑)

オシロスコープが家にあるとどんな波形が出力されているかが分かるのでデバックがしやすいです。

総評

ハードの繋ぎは簡単なんですが、いかんせんそふとをやったことがないので時間がかかりました(泣)

だけど無事に動作させることができて嬉しかったです!

あとは細かいチューニングをしながら色々試していこうと思います。

次回はスピーカーとアンプの制御を試してみようと思います!

ここまで読んでいただきありがとうございました!

この記事が気に入ったらサポートをしてみませんか?