先日Infineon様のご提供により動画を公開したInfineon製ホールセンサを利用した自作ゲームパッドについて、これからホールセンサ式のジョイスティックを作ってみようとお考えの方、自作入力デバイスにご興味のある方、そして本動画のプレゼント企画に当選された方向けに解説します。
まだ動画をご覧になっていない方は、是非先に動画をご覧ください。
ハードウェア構成
ジョイスティックについて
ジョイスティックの動作原理については動画内でも解説しているので割愛します。
ジョイスティックは主に3つの3Dプリント部品でできています。
それぞれ支柱、磁石ホルダ部分、レバー部分です。
動画内で使用したものは充填率40%、ウォールライン数4でPLAで印刷しています。
それぞれのstlファイルはGithubにて公開しているので、ぜひご確認ください。
レバーと磁石ホルダ部分は上下どちら向きでもオーバーハングが発生するため、お使いの3Dプリンタに適した向きに印刷してください。
ネジ、ナットや磁石の座面にはサポート必須です。
L3/R3の判定、つまりZ軸方向の押し込みを実現するため、ある程度支柱がたわむ必要がある点に注意が必要です。
カッチリした操作感が欲しい、あるいは市販のジョイスティックのように強く高精度な復元力が欲しい場合はZ軸方向の判定機能をオミットして支柱を硬く作るのも選択肢の1つです。
支柱部分には、市販のボールジョイントが固定され、ジョイスティックの可動軸となります。
ボールジョイントはラジコンのステアリングのリンク機構などに使用される小型のもので、ボール部分にはM3のネジが通ります。
今回購入したボールジョイントはこちらのものになります。
磁石ホルダとレバー部分にはM3のナットが圧入されています。
ナットをはんだごてやリフロープレートで90℃程度に加熱してから押し込むと入りやすいです。
直径5mm、厚さ3mmの磁石も同様に圧入するのですが、磁石を加熱してよいものかはよくわかりません(とりあえず問題なく磁力を発しているようですが)。
動画内でも紹介したレバーをニュートラル位置に戻すためのOリングは色々試した結果、NBRの内径3mm、線径2mm、外径7mmのものが最適でした。
これらの部品を組み立てていきます。
レバー、ワッシャ、Oリング、ボールジョイント、磁石ホルダの順で、M3*20mmのネジで締結します。
ワッシャは、Oリングの潰れ方を一定にする役割と、レバー基部と支柱の干渉を避ける役割があり、ジョイスティックの可動範囲を真円に近づけるために重要な部品になっています。
ボールジョイントは支柱に3*8mmのタッピングビスで固定します。
プッシュスイッチ
スイッチはオムロンのB3W-9000シリーズを採用しました。
LEDを内蔵していること、耐久性の面で信頼がおけるメーカであることが理由です。
スイッチの脇に配置された抵抗はプルアップではなく、LED駆動用です。
スイッチが押されると、マイコンの入力ピンと3.3Vに繋がったLEDがGNDに落ちるようになっています。
プルアップはマイコン内蔵のため必要ありません。
マイコン
ピン数の都合でRaspberryPi Picoを採用しました。
スイッチをキーマトリクス配線にすれば、もっと少ないピン数のマイコンでもゲームパッド自体の実現は可能でしたが、どうしてもボタンを光らせたかったため、各ボタンに入力ピンを1つ割り当てられるだけのピン数があるRasPicoを選択しました。
OLEDディスプレイ
両ジョイスティックや各種入力の状態を表示するインジケータとして、OLEDディスプレイを搭載した点は最大のこだわりポイントの一つです。
起動時のオープニング演出やデバッグ作業にも使用します。
基板設計
基板は、接続用のフレキシブル基板と併せてJLCPCBに製造から表面実装部品の実装までを発注しました。
人差し指が当たるトリガスイッチの裏には演出用のLEDと駆動用MOSFETが実装されています。
製造コストを下げるために表面実装は片面だけに実装するように設計しました。
つまり、本体基板とトリガスイッチ基板を1枚の基板として発注し、後でトリガ側のみを切り離して裏返してスイッチを実装してから組み立てる構造になっています。
ハード面での反省点など
動画内でも言及しましたが、1箇所だけパターンがジョイスティックのナットの座面の下になる箇所があります。完全に見落としていました。
ボタンのLEDには10kΩ抵抗を使っていますが、何故か緑だけLEDが暗いことが発注した基板が届いてからわかりました。
なので、1台あたり4個ある緑ボタンだけ抵抗を1kΩに載せ替えることになってしまいました。
それを8台分。
また、RasPicoのコネクタが基板端であればRasPicoを直接基板上にマウントすることができたのですが、RasPicoのコネクタ側を迂回するようにパターンが通っているので、RasPicoを少し内側に引っ込めざるを得ませんでした。そのため、USBケーブルのコネクタが干渉するようになり、ピンヘッダで下駄を履かせてマウントするようになっています。
ソフトウェアの詳細
Githubリポジトリはこちら
Infineon謹製ライブラリについて
当初はInfineonが公開しているArduino向けライブラリを利用しようとしていたのですが、うまくいきませんでした。
使用しているセンサTLE493D-P2B6はI2Cのアドレス違いでA0~A3のラインナップがあるのですが、A0以外のセンサに対してbegin()メソッドを実行した場合、なぜかアドレスがA0と同じ0x35に書き換わってしまう不具合が発生し、解決することができませんでした。
ユーザマニュアルを読むと、モードなどを指定するときに書き込みが発生するMOD1レジスタに対して、特定のビットの値でA0~A3のいずれかのアドレスに書き換えることが可能であるという仕様は存在していることがわかります。
当該リポジトリの過去のissueを読むと過去のバージョンで同様の問題があったようですが、それが現行のバージョンで意図せず発生する理由は、限られた開発時間では究明できませんでした。
仕方が無いので、今回製作したゲームパッドでは、センサとの通信部分をArduino標準ライブラリのWire.hを利用してフルスクラッチしています。
どなたか原因が分かった方は修正して、本家Infineon様のリポジトリににプルリクを送ってあげてください。
RP2040をHIDとして動作させるには
RasPicoをHID(Human Interface Device)としてPCに認識させるため、Adafruit_TinyUSB.hとAdafruit_GFX.hというライブラリを使用しています。
プログラムの書き込みにはArduino IDEでUSB Stackの切り替えが必要になります。
[Tools]>[USB Stack]>[Tiny USB]
切り替え忘れてもエラーの文面で教えてくれる親切設計です。
一度HIDとして動作し始めると、PCにUSBで接続しただけではプログラムの再書込ができなくなります。
その場合はプログラムの再書込の必要が発生した場合は、Bootselボタンを押しながらUSBを接続することで書き込みが可能になります。
本ライブラリはゲームパッド以外にもマウスやキーボードとして振る舞うことも可能なようなので、色々工作の幅が広がりそうです。
Z軸押し込みの判定について
ジョイスティックがXY軸方向に動くと、磁石はボールジョイントを中心とした球面上を動くため、そこから押し込まれる量が一定だとしても、XYの中心付近と、入力範囲の端の方ではZの値が変わってきます。
そのため、Z軸押し込みの閾値をXYの値によって二次関数的に可変させる処理を追加してあります。
//-----------------------------ジョイスティック押し込み-----------------------------
//XY入力範囲端にいくほどZ押し込みの閾値が下がる二次関数
if (JoyR.z > -1.0/1100*pow(JoyR.r,2)+joyPushStroke) {
updateBtn(&btnFlag, 11, 1);
} else {
updateBtn(&btnFlag, 11, 0);
}
if (JoyL.z > -1.0/1100*pow(JoyL.r,2)+joyPushStroke) {
updateBtn(&btnFlag, 10, 1);
} else {
updateBtn(&btnFlag, 10, 0);
}
ここでジョイスティッククラスのメンバ変数rは中心(0,0)から(x,y)の距離をfloat型で表します。
rとZ押し込み閾値の関係をグラフで可視化すると以下の通りです。
ジョイスティックのデジタルデッドゾーンについて
動画内でも解説した通り、ホールセンサを利用した非接触の入力検知によってセンサの摩耗による不具合は構造上発生しませんが、ジョイスティックに可動部品が存在する以上、その部分の摩耗は避けられません。
また、樹脂パーツの劣化による不具合も考えられます。
現状のOリングでも、レバーの復元力には限界があるため、完全に中央に戻らない場合があります。
これが現在公開しているソースコードでの入力カーブです。
横軸が実際のセンサの値で、縦軸がPCに入力される値です。
点fの値が変数asobiに相当し、点aはfの1/2の値です。点aより0に近い側がデッドゾーンになります。
const int asobi = 180;
int Tle493d::asobiAdj(int axis){
int axisAdj;
if(r < asobi/2){
axisAdj = 0;
}else if(r < asobi){
axisAdj = axis * (2 - asobi/r);
}
return axisAdj;
}
また他にも、ソフトウェア的なローパスフィルタや入力カーブの設定により、指を離した状態で中心付近で値が安定するようになっています。
if (r < (float)asobi){//アソビの範囲内なら
lpf = 0.2 + 0.8*(r/asobi);//中心に近いほどLPFが強くかかる
x = (int)(xPrev*(1-lpf) + asobiAdj(xRaw)*lpf);//LPFに加え、0に近いほど0に向かおうとする
y = (int)(yPrev*(1-lpf) + asobiAdj(yRaw)*lpf);//LPFに加え、0に近いほど0に向かおうとする
}else{
x = xRaw;
y = yRaw;
}
ソースコード内ではTle493d::updateやTle493d::asobiAdjにあたります。
詳細はソースコード内コメントに記載がありますので、もしご自身でホールセンサ式ジョイスティックを開発される場合は、この辺りのパラメータを調整してみてください。
また、他にもっと優れたアルゴリズムを実装することができましたら、ぜひコメント欄で教えてください。
自動連打機能
ゲームパッドをせっかく自前で設計するならハードウェアチートもやり放題ということで自動連打機能を実装しました。
Start,Selectと、XYABのうち目的のボタンの同時押しで自動連打が有効になり、ボタンが押されたときに表示されるインジケータが白抜き表示になります。
厳密にはStart,Selectを離した瞬間に押されていたボタンの自動連打を有効にするので、自動連打したいボタンを先に離してしまうと自動連打が有効にならない点には改善の余地ありです。
まとめ
今回のゲームパッド開発は、Infineon様から「3Dホールセンサのプロモーションのために、これを使って何か面白いものを作ってほしい」というお声をかけて頂いたのがきっかけでした。
2023年11月に開催されたRigolの展示会のイチケンブースにお越し頂いた方はお気づきかと思いますが、あの場に展示されていたのが、動画内でも登場した板バネ式ジョイスティックの試作機です。
あの頃から現在に至るまで試作を重ねて開発を続けて、ここまで形にすることができました。
今回の動画が3Dホールセンサの新たな市場開拓に繋がることを、そして自作入力デバイス界隈のさらなる発展に繋がることを切に願っています。
余談
↑↑↓↓とくれば…?
コメント