はじめに
当研究所はMakerとしていろいろオリジナルデバイスを作成していますが、最近ではオリジナルのコントローラを使用したゲームの開発に注力を行っています。このようなゲームを作成するには、コントローラデバイスとゲームが動いているPCで通信を行う必要があります。しかし、これが地味に面倒くさく、またこのようなシステムを作成しようとしている初心者に対して高いハードルとなっているかと思います。
本記事では、この通信部分をRaspberry Pi Picoを使用することで容易にする提案をしています(実際に動くサンプルあり)。
概要
オリジナルコントローラを使用したゲームを作成する場合、多くの場合以下のような構成になるかと思います。
![]() |
既存構成 |
- ゲームはPCで動作させる
Unityなどで作成したゲームを想定しています - コントローラデバイス側で入力された情報はシリアル通信でPCに送信する
物理的にはUSBケーブルで接続します - PC側ではシリアル通信で情報を受信してゲームの操作を行う
課題
通信にシリアル通信を使用するということが面倒の元凶となっています。面倒くさい部分を以下に記載します。
- コントローラデバイス側でシリアル⇔USB変換が必要となる
デバイス側の構成にもよりますが、たいてい必要です - PC側のゲームのインタフェースにシリアル通信の受信機能を実装しなければならない
COMポートを開くなどの処理の実装が必要です
そもそもシリアル通信はかなり低レベル(単純)な通信方法なので、PCでは逆に扱いにくいのです(その代わり応用が効く)。どうせUSBで接続するならUSBの通信方法でデータ通信ができればいいのに…。ということでRaspberry Pi Picoの登場となります。
Raspberry Pi Pico
![]() |
Picoシリーズ(RaspberryPi公式HPより引用) |
最近、Raspberry Pi Picoを使用しているのですが、かなり便利です。個人的に便利な点を以下に記載します。
- 1,000円弱で低価格
- C++で開発可能
Pythonでも可能だが現時点では一部機能制限がある模様 - 開発環境はPC1台で完結
ライタ等不要でWindowsでも開発可能 - ADCが搭載
もちろんI2CやSPIといった基本的なペリフェラルも搭載 - USBによるシリアル通信が標準搭載
デバッグがしやすい - USBデバイスとしての動作が可能
オープンソースのTinyUSBに対応している
![]() |
IOピン一覧(RaspberryPi公式HPより引用) |
個人的にはADCが搭載されたことで、Raspberry Piシリーズの欠点を完全にカバーできている感じがしています。もうマイコンいらないですね…、安いし…。
提案
さて話を戻して、ここで提案するのは以下のような内容です。
- コントローラデバイスのインタフェースをRaspberry Pi Picoにする
- Raspberry Pi Picoを仮想キーボードとして動かし、PCへの情報送信をキーボード入力にしてしまう
提案する構成例を以下に示します。
![]() |
提案構成 |
上記の構成ではコントローラデバイスからPC側のゲームへ情報を送信するとキーボードからの入力となります。このときゲーム側では標準のキーボード入力ライブラリが使用できるため、シリアル通信用のインタフェース実装が不要となります。
ただし、これはコントローラデバイス→PCへの単方向のみの送信となりますので、ゲームの操作結果をコントローラデバイスへフィードバックするような必要がある場合には向いていません。この条件が許容できるのであれば実装がかなり容易になるはずです。
動作例
具体的にはどういうことなの?ということで動作例を用意しました。ブレッドボードでRaspberry Pi Picoと2つのボタンを接続した例なのですが、下の回路図と等価の構成になっています。
![]() |
ブレッドボード実装 |
![]() |
等価回路 |
この回路をPCと接続し、(中央付近の赤い)左のボタンを押すとPC側には"a"が入力されます(キーボードの"a"キーを押したときと同じ動作)。同じく右のボタンを押すと"B"が入力されます。なんとなくわかると思いますが、このボタン入力の部分を好きなメカニズムに変更することでPCにUSBキーボードとして認識されるオリジナルコントローラが作成できます。
ソースコード
上記の動作例で使用しているRaspberry Pi Picoのサンプルをここに置いておきます。ビルド方法などはリンク先のREADMEを参照してください。雛形的なプログラムになっているので、各自で改良することで割と容易に使用できるかと思います。ちなみにRaspberry Pi PicoのUSBサンプルプログラムを改変しているだけのものです。
サンプル解説
このサンプルを雛形に開発を行う場合、修正するのは"main.c"のみでOKです(その他のコードはそのままでOK)。また、"main.c"のすべてを把握する必要はなく、修正する必要のある箇所は主に以下の2点です。
- main関数
文字通りのメイン関数で、プログラムはここから開始されます。ここではPCと通信を行うUSBの無限ループ処理が開始されます。そのため、GPIOなどの設定はこのループに入る前に行っておきます。 - hid_task関数
この関数の中ではPC側に送信するキーの情報を設定しています。このサンプルではGPIOの状態をリードして送信するキーコードを設定しています。
具体的な処理内容はコード内にコメントを埋めていますので参考にしてください。以下に変更対象の範囲のコードを掲載しておきます。
main関数
- /*------------- MAIN -------------*/
- int main(void)
- {
- board_init();
- tusb_init();
- // **********************************************
- // GPIOの設定
- // - ピンの初期化
- // - 入力/出力の設定
- // - プルアップ設定
- // ※ADCなどの機能を使用する場合もここで設定する
- // **********************************************
- gpio_init(kGPIOpin0);
- gpio_set_dir(kGPIOpin0, GPIO_IN);
- gpio_pull_up(kGPIOpin0);
- gpio_init(kGPIOpin1);
- gpio_set_dir(kGPIOpin1, GPIO_IN);
- gpio_pull_up(kGPIOpin1);
- while (1)
- {
- tud_task(); // tinyusb device task
- led_blinking_task();
- hid_task();
- }
- }
hid_task関数
- // Every 10ms, we will sent 1 report for each HID profile (keyboard, mouse etc ..)
- // tud_hid_report_complete_cb() is used to send the next report after previous one is complete
- void hid_task(void)
- {
- // Poll every 10ms
- const uint32_t interval_ms = 10;
- static uint32_t start_ms = 0;
- if ( board_millis() - start_ms < interval_ms) return; // not enough time
- start_ms += interval_ms;
- // *******************************************************
- // GPIOのリード
- // プルアップ設定なのでボタンを押していると"0"が取得される
- // *******************************************************
- bool button_0 = !gpio_get(kGPIOpin0);
- bool button_1 = !gpio_get(kGPIOpin1);
- // Remote wakeup
- if ( tud_suspended() && (button_0 || button_1) )
- {
- // Wake up host if we are in suspend mode
- // and REMOTE_WAKEUP feature is enabled by host
- tud_remote_wakeup();
- }
- else
- {
- // skip if hid is not ready yet
- if ( !tud_hid_ready() ) return;
- // use to avoid send multiple consecutive zero report for keyboard
- static bool has_keyboard_key = false;
- if ( button_0 )
- {
- uint8_t keycode[6] = { 0 };
- keycode[0] = HID_KEY_A; // "a"のキーコードを格納
- tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, keycode); // キーコードを送信
- has_keyboard_key = true;
- }
- else if ( button_1 )
- {
- uint8_t keycode[6] = { 0 };
- keycode[0] = HID_KEY_SHIFT_LEFT; // shiftキーのキーコードを格納
- keycode[1] = HID_KEY_B; // "b"のキーコードを格納
- tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, keycode); // キーコードを送信
- has_keyboard_key = true;
- }
- else
- {
- // send empty key report if previously has key pressed
- if (has_keyboard_key) tud_hid_keyboard_report(REPORT_ID_KEYBOARD, 0, NULL); // 空のキーコードを送信
- has_keyboard_key = false;
- }
- }
- }
以下に処理の解説を箇条書きで記載しておきます。
- hid_task関数は10ms毎の実行(ポーリング)が想定されています。関数の最初にそのための計測処理があります。
- 使用しているGPIOはプルアップに設定しているので、ボタンを押していない(GNDに接続していない)ときは"1"、ボタンを押している(GNDに接続している)ときは"0"になります。
- tud_remote_wakeupのような関数がありますが、これはUSBデバイスが(接続しているPCが休止状態などになり)休止状態になっている場合に再開時に実行される処理です(要は休止状態時にボタンを押すと再開するという処理が実装されています)。PCが動きっぱなしであれば休止状態は考慮しなくても良いので、このあたりは無視しても問題ありません。
- 送信するキーボードのキーコード(HID_KEY_Aのような)の定義はこちらを参照してください。
- キーボードの同時押し(shiftキー+文字キーのような)は、押したいキーコードを配列に格納して関数に渡します。ただし、格納する順番が重要なようでshiftキーと文字キーを同時押しする場合はshiftキーの方を先に格納する必要があるようです。
- USBでキーボードの状態を送信する場合はリポートデスクリプタというデータで送信されます。ボタンを押したときは、ボタンを押しているというリポートデスクリプタを送信すればいいのですが、ボタンを離したときは何も押されていないというリポートデスクリプタを送信する必要があります。NULLの情報を送信しているところがあるのはそのためです。
まとめ
Raspberry Pi PicoにはADCだけではなくI2CやSPIも標準で搭載されているので、大抵のセンサが接続できると思います。そして、その値をどのように使用するかの処理を実装すればもうオリジナルコントローラができてしまいます。ADCなどを使用する場合にはCMakeLists.txtの変更が必要ですが、Raspberry Pi Picoにはサンプルが充実しているのでそちらを参照すれば比較的容易に実装できるかと思います。
以上がRaspberry Pi Picoをインタフェースに使用したオリジナルコントローラとPC上のゲームを接続する方法の提案となります。皆さんのアイデアを具体化する手助けになれば幸いです。
その他
- 使用しているRaspberry Pi Pico
ここで使用しているのはスイッチサイエンスが卸しているヘッダピン実装済みの「Raspberry Pi Pico H」で、大阪日本橋のシリコンハウスで979円で購入しています。 - 当研究所の開発環境
Windows10のWSL2でUbuntu20.04をインストールしてビルドを行っています。 - 仮想ゲームパッドも実はいける
Raspberry Pi Picoは仮想ゲームパッドとして動かすこともできます。キーボードかゲームパッドのどちらを採用すべきかというのはコントローラの仕様によるのでなんとも言えませんが、とりあえず今回はキーボードの方を紹介しました。