Raspberry Pi Pico で薄型レバーレスコントローラーの自作
はじめに
素人ながらRaspberry Pi Pico[1]を使って,流行り?の薄型レバーレスコントローラーを作ってみました.
正面写真 筐体サイズは
右側面からの写真.厚み(足からボタン上面まで)はおよそ18mm
スペックは以下の通りです.
- 縦115mm 横209mm 厚み18mm (筐体の固定用ネジ足底面からボタン上面まで)
- アクリル板(2mm厚)で基盤を挟み込む形の筐体.四隅をM3ネジで留め,スペーサーを筐体の足に.
- 12ボタン + 4タクトスイッチ(オプションボタン用)
- スイッチはロープロファイルキースイッチ(Kailh Choc V1 Red Pro)を使用
- ボタンキャップは3Dプリンタで自作
- Raspberry Pi PicoをUSB HIDでGamepadに(Rustで実装)
- Steamでのみ動作確認
参考にした記事
制作においては以下の偉大なる先駆者様達の成果物を大いに参考にしています.
レバーレスコントローラーとは
少し前から格闘ゲーム界隈ではHit Boxを皮切りにアーケードスティックを排したコントローラー,いわゆるレバーレスコントローラーがアツいです.
https://www.hitboxarcade.com/products/ps4-pc-hit-box より引用
左手でスティックをガチャガチャしてコマンド入力する所をボタンに置き換えることで,より速く,より正確な入力が出来るようになると言われています.
動作検証
今回はUSBデバイスを作るためのボートとしてRaspberry Pi Picoを使うことにしました.主な決め手は以下の点です.
- USBデバイスコントローラー機能がある
- GPIOが沢山ある(ボタン配置的に最低16本)
- 初心者に優しい(ドキュメント・開発環境が充実している)
Raspberry Pi Pico
Raspberry Pi Picoとは,Raspberry Pi財団が開発したマイコンRP2040の評価ボード(のようなもの)で,豊富な機能に加え,充実したドキュメントや開発環境など,教育的な面も疎かにされてない点含め良さげなボードです.あとLチカ[2]用のLEDがボードに乗っており,すぐに動作確認できるようになっている点も良いです.
Raspberry Pi PicoのためのMicroPythonの開発環境からLチカまでの最低限の使い方は以下のチュートリアルを参考にしました.
また,データシート等の詳細情報はこちらから参照できます.
Raspberry Pi PicoでスイッチON/OFFの取得
今回作るコントローラーは,要するにスイッチのON/OFFが判別できれば良いものであるため,上記チュートリアルにあるようなLEDとスイッチの回路で簡易的に動作を確認できます.
https://projects.raspberrypi.org/en/projects/getting-started-with-the-pico/6 より引用
この回路では,端子をプルダウン(端子が接続されていないときはGNDに落とす)ことで,スイッチが押されると端子に3.3V電圧がかかるようになります.よって端子にかかる電圧を見ることで,スイッチが押された/押されていないかが分かります.
今回は設計上,電源配線を伸ばして各スイッチ端子に供給するより,GNDを配線するほうが容易であったため,各端子をプルアップしてスイッチのON/OFFを判別できるように設計しました.
キースイッチの選定
薄型のレバーレスコントローラーのキモでもあるスイッチについては,なるべくそれ自体の高さがない物が良いので,自作キーボード界隈で有名な遊舎工房へ探しに行きました.押した感触や高さから,今回はKailh Choc V1 Red Proを使うことにしました.
https://shop.yushakobo.jp/products/pg1350?variant=37665168228513 より引用
オプション用のボタン(スタートボタンやセレクトボタン用)は白色のタクトスイッチ を使用しました.
回路/基板設計
回路も基板も何も知らない状態ですが,調べてみるとKiCadを使うのが良さそうなので,今回はKiCadを使い設計しました.
基本的な操作やワークフローはここで確認しました.
なお,Raspberry Pi Picoのシンボルデータはデフォルトで存在していないので,以下からダウンロードし,KiCad用のライブラリに追加しました.[3]
回路設計
悪戦苦闘しながらRaspberry Pi Picoとスイッチを繋いだ回路を設計しました.
KiCadで書いた回路図
各ボタンを各端子に接続し,ボタンが押されたらGNDに落とす回路です.
基板設計
上記の回路図から,次は実際に作成する基板のレイアウトの調整,配線を行いました.
なお,キースイッチのフットプリント(SW_Kailh_Choc_V1)は以下のライブラリに含まれるものを使用しました.
レイアウト
表面の配線
裏面の配線
なるべく全体サイズを小型にしつつも,ボタンのサイズと配置間隔が小さくなりすぎないように気をつけながら設計しました.所持していたアケコンのボタンサイズ・配置等を参考にしながら,ほぼ各ボタンの配置間隔が30mmになるように設計しました.
発注
自作の基板を発注できる所はいくつかあるようですが,今回はJLCPCBで発注しました.決め手は,黒色の基板が追加料金無しで選べたからです.
ちなみに基板の製造番号はキースイッチのフットプリントの内側に小さくプリントされてました.特に位置の指定せずとも配慮してくれたみたいです.今回のケースでは結局基板は筐体に覆われ見えなくなるので,どこに書いてあっても大丈夫でしたが,気遣いに感謝です.
別に推してるわけではないですが,JLCPCBの製造工場の動画を見つけたので,置いときます.
以下が実際に届いた基板です.
届いた基板にキースイッチをはめた図.ツヤ消しの黒色でカッコいい気がする
発注後約1週間で届きました.
最低注文単位が5枚なので,1枚しか使わないのに5枚来る羽目になりましたが,初回割引のおかげでそれでも合計金額は諸々込みで2500円程度です.1枚500円.安い...
筐体/ボタン設計・発注
基板を発注して届くまでの間に,筐体とボタンの設計を行いました.普通こっちを設計してから基板を発注すべきところなんでしょうが,たとえ修正があったとしても,基板のサイズやボタンの配置を変えることはないだろうとして,後から設計しました.
筐体/ボタン設計
偉大なる先駆者様と同じく,筐体はアクリル板を複数枚用意し,基板を挟み込む形で設計しました.またボタンは3Dプリンタ(ナイロン)で印刷するものとして設計しました.
今回せっかくなのでFusion 360を使いました.非商用の個人製作なら無料ライセンスが使えるのがありがたいです.
筐体を設計するために,まずはKiCadで設計した基板からSTEPファイルを出力し,Fusion 360で読み込みました.
KiCadで出力した3DデータをFusion360に取り込むとこんな感じに
詳細は省きますが,この基板をベースに上からスケッチを重ねて,最終的に以下のような筐体/ボタンを設計しました.
完成図
右側面はRaspberry Pi Picoが収められるように,スペースを用意しています.
右側面図
ボタン付近の断面図は以下のようになっており,底面に1枚,上面に3枚のアクリル板(2mm)を使って,基板を挟み込む形で設計しています.
ボタン部分の断面図
ボタンは約厚さ2mmで設計していますが,強度的に少し心配な気もするので,もう少し厚いほうが良いかもしれません.
ボタンの側面スケッチ.上面は少し丸みを帯びさせ,内部はシェルで厚み2mmほど残す形に
ボタンの足(キースイッチと嵌め合わせる部分)は,キースイッチのデータシートと3Dプリントの加工精度を考慮して雰囲気小さめに作りましたが,結果として少しきつめ(押し込まないとはまらない)になりました.はまらないorスカスカでボタンを押すときに外れちゃうというのは避けられたので,運が良かったです.
ボタンの足.キースイッチのデータシートを元に設計
アクリル板の発注
アクリル板の加工発注はAnymanyで行いました.Fusion 360のCADデータそのままで発注できるわけではないので,Shaper Utilitiesを使い各面をSVGデータに変換し,カットするアウトラインを示した以下のようなデータで発注しました.
カットするアウトライン図.大きめのサイズから切り出すので,余ったスペースで予備を1枚作成
発注後3,4日くらいで来ました.合計費用は7,500円くらいでした.
ボタンの3Dプリント発注
DMM.makeで発注しました.材料は一番安いナイロンです.3Dプリント用に必要な数だけボタンを並べて,STLデータへ出力して発注しました.
ボタン(3Dプリント用)
こちらも発注後3,4日程度で届きました.合計費用は2500円くらいでした.
ファームウェアの開発
今回はRaspberry Pi PicoをUSBデバイス(HID)にするために,Rustでファームウェアを実装しました.C++でもよかったのですが,気分的にRustでやってみました.
rp-hal
といっても,0からベアメタルなプログラミングをするのが目的ではないので,今回はrp-halライブラリを使いました.
rp-halはRP2040向けソフトを開発するためのハードウェア機能の抽象化を担ってくれるライブラリであり,これを使うことで,レジスタを叩いたりするような低レベルプログラミングをせず,ライブラリによって提供される高レベルなAPIを使って開発出来るようになります.
今回は,更にこのrp-halのpico用USBマウスのサンプルコードを元に開発しました.
rp-halを用いたUSB HID用ファームウェアの開発
完成したコードはここに置いてます.
概ねサンプル通りなので,簡単に解説します.
まず,HIDレポートディスクリプタはマクロを使って簡単に書くことが出来ました.今回は移動ボタンもハットスイッチなどではなく,全て同じボタンで実装しています.
// HID descriptor for Gamepad
#[gen_hid_descriptor(
(collection = APPLICATION, usage_page = GENERIC_DESKTOP, usage = GAMEPAD, ) = {
(usage_page = BUTTON, usage_min = 1, usage_max = 16) = {
#[packed_bits 16] #[item_settings data,variable,absolute,not_null,no_wrap,linear] buttons = input;
};
}
)]
struct GamepadReport {
buttons: [u8; 2],
}
このGamepadReport
構造体は,全てのボタンの入力状態をビット配列として持っています.つまり,0番目のボタンが押されていれば0番目のビットが1になります.このGamepadReport
へデータを入れてホスト側に渡すデータを用意しているのがmainのloop部分で,実際のスイッチに対応するピンの入力状態をgamepad
のget_input()
メソッドで取得しています.
let gamepad = GamePad {
btnl: pins.gpio11.into_pull_up_input(),
btnd: pins.gpio10.into_pull_up_input(),
btnr: pins.gpio9.into_pull_up_input(),
btnu: pins.gpio8.into_pull_up_input(),
btn1: pins.gpio3.into_pull_up_input(),
btn2: pins.gpio2.into_pull_up_input(),
btn3: pins.gpio1.into_pull_up_input(),
btn4: pins.gpio0.into_pull_up_input(),
btn5: pins.gpio7.into_pull_up_input(),
btn6: pins.gpio6.into_pull_up_input(),
btn7: pins.gpio5.into_pull_up_input(),
btn8: pins.gpio4.into_pull_up_input(),
opt1: pins.gpio15.into_pull_up_input(),
opt2: pins.gpio14.into_pull_up_input(),
opt3: pins.gpio13.into_pull_up_input(),
opt4: pins.gpio12.into_pull_up_input(),
};
loop {
let report = GamepadReport {
buttons: gamepad.get_input(),
};
push_gamepad_movement(report).ok().unwrap_or(0);
}
このget_input
は以下のように実装されており,get_hat_input()
, get_btn_input()
, get_opt_input()
内でそれぞれ対応するボタンが入力されているか確認し,されていれば対応するbitを立てています.
fn get_input(&self) -> [u8; 2] {
let mut state: u16 = 0;
state |= self.get_hat_input();
state |= self.get_btn_input();
state |= self.get_opt_input();
// convert u16 -> u8;2
state.to_le_bytes()
}
例えば,get_hat_input
の処理を見ると,各スイッチの入力状態は,スイッチが入力されていたら電圧がかからなくなる(==low)ので,is_low
メソッドで入力を確認し,対応する位置にbitを立てています.
fn get_hat_input(&self) -> u16 {
let mut state: u16 = 0;
if self.btnl.is_low().unwrap() {
state |= 1_u16;
}
...
ちなみにget_hat_input
のみ,同時に複数の移動ボタンが押されているときに,
- 左右どちらも押されていれば左右はニュートラルに
- 上下どちらも押されていれば上のみ有効に
しています.いわゆるSOCDクリーナーと呼ばれるものの実装になるはずです.
// SOCD cleaner
fn socd_cleaner(&self, hat_state: u16) -> u16 {
let mut state = hat_state;
// left and right, up and down
let lr = 1_u16 | (1_u16 << 2);
let ud = (1_u16 << 1) | (1_u16 << 3);
// if left and right are pressed at the same time, ignore both of them.
if (state & lr) == lr {
state &= !lr;
}
// if up and down are pressed at the same time, apply only up.
if (state & ud) == ud {
state &= !(1_u16 << 1);
}
state
}
VIDとPIDの設定について
デバイスのVID, PIDを指定する部分にて,今回試した限りVID, PIDによってはSteamでコントローラーとして認識されない(=使えない)現象が起きたため,現状VID, PIDは手元でコントローラーとして認識されたHORIのRAP3コントローラーと同じにしています.このあたり,何かご存知の方いましたらお教えてください.
let usb_dev = UsbDeviceBuilder::new(bus_ref, UsbVidPid(0x0F0D, 0x0011 /* HORI RAP3 */))
完成
ファームも焼き終わり,動作も確認が終わったら,最後に組み立て完成です.
組み立て途中の図
完成図
費用
- Raspberry Pi Pico 600円
- 基板 2500円
- 筐体(レーザーカット&材料代) 7500円
- ボタン(3Dプリント) 2500円
- その他(ネジや工具など) 3000円
と1万6千円くらいかかりました.買ったほうが安いかもです...
まとめ
10時間ぐらいストリートファイター5で使用してますが,今の所特に問題なく,快適に使用できています.
素人ながらなんとか作れましたが,1点心残りなこととして,USB Type Cにすればよかったかなとか思っています.いまどき Micro USBっていうのもなんだか残念です.
あと,予想通り,レバーレスコントローラーは操作が難しいです.練習します.
(追記) Raspbeery Pi Pico を用いた製作例
軽く調べた所,海外でもレバーレスコントローラーの自作はそれなりにされているようで,更にはRaspberry Pi Picoを用いた事例もいくつかあるようです.例えば,[4] と同じ用に使うための基板とファームウェアが公開されています.
https://github.com/FeralAI/PicoFightingBoard より引用
また作ることが有れば,利用してみたいなと思います.
-
LEDを点滅させること.マイコン界のHello, worldと勝手に思ってる. ↩︎
-
今回使用したシンボルデータは,https://datasheets.raspberrypi.com/rp2040/hardware-design-with-rp2040.pdf の 「Chapter 3. The VGA, SD Card & Audio Demo board for Raspberry Pi Pico」
にて紹介されているものです. ↩︎
Discussion