Edited at

*PiをUSBゲームパッドとして動作させる

More than 1 year has passed since last update.


はじめに


この記事の位置づけ

この記事は以下の記事の続編です。

今までの記事で、既存のコントローラ、あるいはゼロから作ったオリジナルのコントローラをOrange PiやRaspberry Piに接続し、ゲームコントローラとして利用する事ができるようになりました。せっかくだから、このコントローラを汎用のUSBゲームコントローラとして利用しちゃいましょう。

Orange PiやRaspberry Pi ZeroにはUSBホストとしての機能の他に、USB OTGを使ってUSBデバイスとして動作させる機能が備わっています。この機能を用いて、パソコン等に接続した際にOrange PiをUSBのゲームコントローラとして認識させる事にしましょう。


利用する技術

Linux-USB Gadget API Frameworkを使います。このフレームワークを使うことで、ドライバ等を書くこと無く、OTG機能の備わったLinuxマシンをUSBシリアルやUSB MIDIなどのデバイスとして動作させる事ができます。HIDとして動作させるためには、少しだけカーネルモジュールを書く必要がありますが、基本のロジックはユーザーランドで書けるので恐れる必要はありません。


多少必要になるけど詳細は説明しない技術


  • Linuxカーネルモジュール開発

  • USB Protocol全般

  • USB HID Classの詳細


準備


g_serialモジュールの無効化

Armbianを利用している場合には、Linux-USB Gadget API Frameworkはすでに動作中です。USB OTGの口からPCにUSBで繋げた際、標準でシリアルポートとして認識、ログインが可能なのは、この機能のおかげです。g_serialというモジュールがこのフレームワーク上で動作しています。

(2017/11/14 この段落の情報が間違っていたので訂正)複数のガジェットモードを動かそうとした際には、compositeの仕組みを使って実現する事は可能です。が、composite部分は独立したモジュールとしては書かれておらず、事前に必要なfunctionとcompositeのソースを組み合わせてコンパイルする必要があります。残念ながら個別に用意したモジュールを実行時に動的に組み合わせることはできないようです。


/etc/modules

#w1-sunxi

#w1-gpio
#w1-therm
#sunxi-cir
#xradio_wlan
#g_serial

/etc/modulesに起動時に読み込むモジュールが書かれているので、g_serialをこのようにコメントアウトした後、rebootすると良いかと思います。


Linux-USB Gadget API Framework

/lib/modules//kernel/drivers/usb/gadgetを覗いてみると、いくつかのモジュールが確認できるかと思います。


  • g_cdc.ko

  • g_ether.ko

  • g_hid.ko

  • g_mass_storage.ko

  • g_multi.ko

  • g_ncm.ko

  • g_printer.ko

  • g_serial.ko

HIDとして動作させるには、この中のg_hidというモジュールを使います。g_serialなど多くのモジュールはそれ単体で動作するのですが、g_hidに関してはその下で動くモジュールを別途作成する必要があります。まずはこのモジュールを作ることにします。

ちなみに、いくつかのサイト・フォーラムではg_hidのソースにパッチをあててカーネル再構築をする必要がある、みたいに書かれていましたが、その必要はありません。パッチ部分をモジュールとして作り、g_hidより先に動作させればOKです。


HIDの仕組み

HIDは、提供するインタフェース情報をレポートデスクリプタにより提示し、その後はホスト側が一定間隔でインタフェース情報をレポートとして吸い上げる事で動作します。様々なデバイスをサポートするためにレポートデスクリプタの仕様は拡張性に飛んでいるというか、わりと何でもアリなカオスな世界です。全てを理解する事は諦め、一般的なゲームパッドで利用される機能を押さえておけば良いでしょう。

レポートデスクリプタは、レポートのフォーマットを規定すると同時に、値の取りうる範囲やその値をどう解釈するかのヒントを与えます。具体的なフォーマットはソースの中で改めて解説します。


g_hidの仕組み

ユーザが記述するモジュールの中でレポートデスクリプタを定義します。USBのプロトコルに則った細かい処理はg_hidがユーザの記述したモジュールとLinux-USB Gadget API Frameworkを橋渡しして、自動的にこなしてくれます。

定期的に報告するレポートの内容は、/dev/hidg*を通してユーザーランドから更新できます。


必要となるカーネルモジュールを書く

まずは完全なソースを提示しつつ、詳細を個別に解説。


サンプルコードと実行方法


hid.c

#include <linux/module.h>

#include <linux/platform_device.h>
#include <linux/usb/g_hid.h>

MODULE_LICENSE("Dual BSD/GPL");

static struct hidg_func_descriptor hid_data = {
.subclass = 0,
.protocol = 0,
.report_length = 4,
.report_desc_length = 69,
.report_desc = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Gamepad)

0xa1, 0x01, // COLLECTION (Application)

0x05, 0x09, // USAGE_PAGE (Button)
0x15, 0x00, // LOGICAL MINIMUM (0)
0x25, 0x01, // LOGICAL MAXIMUM (1)
0x19, 0x01, // USAGE MINIMUM (1)
0x29, 0x0b, // USAGE MAXIMUM (11)
0x75, 0x01, // REPORT SIZE (1)
0x95, 0x0b, // REPORT COUNT (11)
0x81, 0x02, // INPUT (DATA, VARIABLE, ABS)

0x95, 0x01, // REPORT COUNT (1)
0x81, 0x01, // INPUT (CONSTANT, ARRAY, ABS)

0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x39, // USAGE (Hat switch)
0x25, 0x07, // LOGICAL MAXIMUM (7)
0x35, 0x00, // PHYSICAL MINIMUM (0)
0x46, 0x3b, 0x01, // PHYSICAL MAXIMUM (315)
0x75, 0x04, // REPORT SIZE (4)
0x95, 0x01, // REPORT COUNT (1)
0x65, 0x14, // UNIT (Degrees)
0x81, 0x42, // INPUT (DATA, VARIABLE, ABS, NULL)

0x09, 0x01, // USAGE (Pointer)
0x15, 0x81, // LOGICAL MINIMUM (-127)
0x25, 0x7f, // LOGICAL MAXIMUM (127)
0x35, 0x81, // PHYSICAL MINIMUM (-127)
0x45, 0x7f, // PHYSICAL MAXIMUM (127)
0xa1, 0x00, // COLLECTION (Physical)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x75, 0x08, // REPORT SIZE (8)
0x95, 0x02, // REPORT COUNT (2)
0x81, 0x02, // INPUT (DATA, VARIABLE, ABS, NULL)
0xc0, // END_COLLECTION

0xc0, // END_COLLECTION
}
};

static void hid_release(struct device *dev) {}

static struct platform_device hid = {
.name = "hidg",
.id = 0,
.num_resources = 0,
.resource = 0,
.dev.platform_data = &hid_data,
.dev.release = &hid_release,
};

static int hid_init(void) {
return platform_device_register(&hid);
}

static void hid_exit(void) {
platform_device_unregister(&hid);
}

module_init(hid_init);
module_exit(hid_exit);



Makefile

obj-m := hid.o

all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean


makeするとhid.koというモジュールができます。実際に動作させるにはrootで以下の手順を踏みます。

% insmod hid.ko

% modprobe g_hid iProduct='Hori Fighting Stick EX2 (Virtual)'
% echo 2 > /sys/bus/platform/devices/sunxi_usb_udc/otg_role


ドライバモジュール部

static void hid_release(struct device *dev) {}

static struct platform_device hid = {
.name = "hidg",
.id = 0,
.num_resources = 0,
.resource = 0,
.dev.platform_data = &hid_data,
.dev.release = &hid_release,
};

static int hid_init(void) {
return platform_device_register(&hid);
}

static void hid_exit(void) {
platform_device_unregister(&hid);
}

module_init(hid_init);
module_exit(hid_exit);

ここは定型文だと思ってもらえれば良いのですが、簡単に説明すると、module_initで登録しているhid_initがモジュールload時に実行されます。module_exitで登録しているhid_exitはunload時に実行されます。

このモジュールはplatform_deviceとして登録されます。g_hidはplatform_deviceに登録されたデバイスから"hidg"の名前で登録されたモジュールを探し、連携動作しようとしますので、この部分の名前は変更できません。nameとidはレポート用のデバイスを作る際にも利用され("/dev/%s%d", name, id)の形でデバイス名に反映されます。

dev.releaseはunregister時に呼ばれますが今回は何も書きません。登録しなくても良いのですがsyslogに警告が出て気持ち悪いので空関数でも良いので登録しておく方が無難です。

dev.platform_dataに登録するhid_dataが問題のレポートデスクリプタです。詳しく見てみましょう。


レポートデスクリプタの登録

static struct hidg_func_descriptor hid_data = {

.subclass = 0,
.protocol = 0,
.report_length = 4,
.report_desc_length = 69,
.report_desc = {
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x05, // USAGE (Gamepad)

0xa1, 0x01, // COLLECTION (Application)
/* 中略 */
0xc0, // END_COLLECTION
}
};

subclass/protocolはUSBの概念です。g_hid側がclassはHIDを指定するので、ここではsubclass/protocolのみを指定します。ゲームパッドの場合にはこのままで構いません。

report_lengthはレポートのサイズをバイト単位で指定します。USBのバージョンによって8なり64なりの上限があると思ったのですが、今回は詳しく確認してません。

report_desc_lengthはreport_descのサイズになります。数えるの面倒なので、工夫して書きたい人はそうしてください。

report_descはまずトップレベルの情報について説明します。まずデバイス種別をUSAGE_PAGE/USAGEのペアとして登録します。USAGEは全体で32-bitの名前空間があり、上位16-bitがページと呼ばれています。

最初の0x05は、これからUSAGE_PAGEを登録します。登録するデータの長さは1バイトです、という意味です。もう少し詳しく説明すると、レポートデスクリプタは知らない情報が出てきても読み飛ばせるように設計されており、下位2-bitが続くデータのサイズを示しています。0/1/2ならそのままバイト数、3なら4バイトです。今回、0x04がUSAGE_PAGEを示しており、下位2-bitを1とした0x05を用いることでデータ1バイトを持つエントリとして認識されます。USAGE_PAGEは16-bit空間なので、0x06を使うこともあり、例えばベンダ固有のページは0xFF00に割り当てられているため、0x06, 0x00, 0xffといったシーケンスを登録する事になります。Little Endianなので注意。

同様にして0x09は1バイトでUSAGEを登録しています。ゲームパットの場合、0x04のjoystickか、0x05のgamepadが使えます。ここで違う値を入れるとWindowsなどで認識したとしてもコントロールパネルのgamepadから見えないので注意。

最後にCOLLECTION/END_COLLECTIONで実際のレポートデータを囲みます。COLLECTIONにはいくつかのタイプが定義されているのですが、ここではApplicationのCOLLECTIONを使います。ここでも0xa0がCOLLECTION、+1でデータサイズを示し、0x01がApplicationです。END_COLLECTIONは引数がないので0xc0+0ですね。

以下、この中に含まれる定義の説明です。今回は作成中のコントローラで実際に利用可能なボタンだけを定義したいと思います。多くのゲームに対応できるよう、方向レバーは十字ボタン、アナログレバー両方で認識されるようにしたいと思います。


ボタンの定義

0x05, 0x09,             // USAGE_PAGE (Button)

0x15, 0x00, // LOGICAL MINIMUM (0)
0x25, 0x01, // LOGICAL MAXIMUM (1)
0x19, 0x01, // USAGE MINIMUM (1)
0x29, 0x0b, // USAGE MAXIMUM (11)
0x75, 0x01, // REPORT SIZE (1)
0x95, 0x0b, // REPORT COUNT (11)
0x81, 0x02, // INPUT (DATA, VARIABLE, ABS)

0x95, 0x01, // REPORT COUNT (1)
0x81, 0x01, // INPUT (CONSTANT, ARRAY, ABS)

まず、ボタンを11個定義します。A/B/X/Y/LB/RB/LT/RT/BACK/START/Xboxの11個分です。ここでは名前とか順番は気にしていません。

最後のINPUTで登録されるのですが、それより前に必須項目を登録します。


  • Usage Page

  • Usage

  • Logical Minimum

  • Logical Maximum

  • Report Size

  • Report Count

Usage PageとUsageについては説明した通り。最初の指定は全体の利用方法を示していましたが、ここから先は登録するインタフェースの機能を指定します。ボタンを登録するので、ページとしてButtonを指定しました。Usageについては、かわりにUsage MinimumとUsage Maximumを使っています。これは複数のUsageを登録したい際、連続する番号だった場合に利用できる方法です。ここでは1から11の合計11個をまとめて登録しました。Button 1からButton 11のUsageを登録した事になります。

Logical Minimum/Maximumで各ボタンに対して報告する値の下限/上限を指定します。Button PageでOn/Off Controls(OOC)として登録するには0/1を指定します。

Report Sizeは各ボタンの報告する値をビット数でサイズ指定します。0/1なので1-bitです。Report Countはレポートの個数。11個登録するので11を指定します。これで合計1×11-bits確保されます。

ここまでで必須項目が登録されたので、最後にINPUTを指定します。0x81に続けて指定している0x02は絶対値の変数が報告される、という意味です。基本0x02を使います。

また、続けて1-bitのパディング領域を定義しています。11-bitsに続けて方向キーの4-bitsを定義すると合計15-bitsで半端なので、区切りの良いこの場所で1-bitの空き領域を足しています。最終的なレポートサイズの合計が8-bitsの倍数になるように調整します。

パディングではReport Countした登録せずにINPUTで登録しています、これはどういう事かというと、レポートデスクリプタ内では、一度登録された値は以降上書きされるまでずっと有効になります。従って同じページ内、同じ数値を報告するインタフェースを続けて登録する事でレポートデスクリプタをコンパクトに書けます。


デジタル方向キーの定義

0x05, 0x01,             // USAGE_PAGE (Generic Desktop)

0x09, 0x39, // USAGE (Hat switch)
0x25, 0x07, // LOGICAL MAXIMUM (7)
0x35, 0x00, // PHYSICAL MINIMUM (0)
0x46, 0x3b, 0x01, // PHYSICAL MAXIMUM (315)
0x75, 0x04, // REPORT SIZE (4)
0x95, 0x01, // REPORT COUNT (1)
0x65, 0x14, // UNIT (Degrees)
0x81, 0x42, // INPUT (DATA, VARIABLE, ABS, NULL)

Hat switchという分かりにくいUSAGEを使っていわゆる十字ボタンを登録します。これがボタンでも軸でもなくて分かりにくいのですが……回転方向の角度をレポートする事になっているようです。詳しく見てみましょう。

Logical Minimum/MaximumはMaximumだけ上書きしているので、0から7の値を取ることになります。つまり8方向レバー。ここで新しくPhysical Minimum/Maximumという設定が出てきました。Logical値で報告されたレポートはPhysicalの範囲に収まるようにスケールした後に利用されます。ここでは0-7の値を0-315に再マップする旨を登録しています。UNIT登録がある事に注目してください。これはPhysicalに変換された値の単位系を指定するものでDegrees、つまり度数を指定しています。360度になっていないのは、360度=0度だからですね。45度手前の315度が最大値というわけです。

また、INPUTに0x40が加えられた0x42が登録されている点も注意。コメントにNULLと書いてありますが、これはLogical Minimum/Maximum範囲外の値をレポートしたら入力がなかったものとみなせ、という意味です。十字ボタンを角度で報告する以上、押していない時の状態を示すのにNULLが必要、という事です。その為、報告値が0-7と3-bitsで収まる値にも関わらず、レポートサイズは4になっています。最上位ビットが立っていればボタンは押されていない、と判断されます。


アナログスティックの定義

0x09, 0x01,             // USAGE (Pointer)

0x15, 0x81, // LOGICAL MINIMUM (-127)
0x25, 0x7f, // LOGICAL MAXIMUM (127)
0x35, 0x81, // PHYSICAL MINIMUM (-127)
0x45, 0x7f, // PHYSICAL MAXIMUM (127)
0xa1, 0x00, // COLLECTION (Physical)
0x09, 0x30, // USAGE (X)
0x09, 0x31, // USAGE (Y)
0x75, 0x08, // REPORT SIZE (8)
0x95, 0x02, // REPORT COUNT (2)
0x81, 0x02, // INPUT (DATA, VARIABLE, ABS)
0xc0, // END_COLLECTION

アナログスティックに関してはポインターとして定義するのが一般的なようです。名前から想像できるように、マウスポインタの座標報告でも使われるUSAGEです。signed 16-bitsで登録される事も多いようですが、元がデジタル方向レバーなので、ここはsigned 8-bitsで済ませることにしました。縦と横の二軸のアナログ値として報告するのですが、この対となる軸はCOLLECTION Physicalでまとめると、同じスティックの対になる軸として認識されるようです。


ドライバ組み込み


要root権限

% insmod hid.ko

% modprobe g_hid iProduct='Hori Fighting Stick EX2 (Virtual)'
% echo 2 > /sys/bus/platform/devices/sunxi_usb_udc/otg_role

まず、作成したモジュールをinsmodで組み込みます。続けてmodprobeでg_hidを有効にするのですが、ここではiProductを引数に入れてみました。これによりUSBデバイスの名前が指定した物にかわります。ただ、g_hidがこのデバイス内に作るインタフェースとして"HID Gadget"という名前を登録するため、Windowsがゲームコントローラとして表示する名前は"HID Gadget"になってしまいました、残念。最近のkernelだともっと色々とできるようになってる可能性があるのですが、いかんせん3.4.113なんですよね、ここで相手にしてるの。

otg_roleに2を書き込んでいますが、これによりOTG対応のUSBポートがデバイスモードに切り替わります。このタイミングで接続先のPCに認識されるかと思います。

停止させるには反対に以下の順で登録解除していきます。


要root権限

% echo 0 > /sys/bus/platform/devices/sunxi_usb_udc/otg_role

% modprobe -r g_hid
% rmmod hid.ko

それと、モジュールの書き方に不備があるのか、hid.koを外さずに再度g_hidを有効にするとkernel panicが発生します。自分は危険回避のために上記手順を自動化して作業していましたが、詳しい人がいましたらコメントをいただきたいです。


レポートする


フォーマット

さて、ここまでのレポートデスクリプタで登録したレポートのフォーマットは以下のようになっています。

+---------+-----+-----+-----+-----+-----+-----+-----+-----+

|Byte\Bits| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
+---------+-----+-----+-----+-----+-----+-----+-----+-----+
| +0 | RB | LB | RT | LT | Y | X | B | A |
+---------+-----+-----+-----+-----+-----+-----+-----+-----+
| +1 | Hat switch | - |Xbox |BACK |START|
+---------+-----------------------+-----+-----+-----+-----+
| +2 | X axis |
+---------+-----------------------------------------------+
| +3 | Y axis |
+---------+-----------------------------------------------+

各種ボタンは押された時に1、押されていない時は0です。

Hat switchの角度指定は、0度は上、そこから1増える毎に45度ずつ時計回りに回転します。8以上の値が指定されれば押されていない状態です。

X/Y軸は0を原点として、左・上がマイナス、右・下がプラスの値となる符号付きの値です。


報告

/dev/hidg0 を開き、上記4バイトのデータをwriteで一気に書き込む事で反映されます。簡単ですね。このプログラムはユーザーランドの一般アプリで構いません。コマンドラインだけでテストする簡単な例を紹介すると、

% su

% echo -ne "\x01\0x20\0x00\0x00" > /dev/hidg0

これでボタン1がON、十字ボタンが右になるはずです。

これで例えば、前回のuinputのプログラムを流用して、uinputにレポートするモードと、hidg0にレポートするモード、それぞれをXboxボタンで切り替えるようにすれば、ボード内部での利用、USBゲームパットとしての利用を切り替えながら使えます。せっかくLEDも4つ付いているので、モードに合わせて点灯パタンを変えればバッチリですね。


後記

HIDは思った以上に闇が深く、少し間が空いてしまいました。利用例もあまり多くなく、kernelも古めなので、本当に動くか手探り状態だったのもありますね。普通のHIDならg_hidで十分に作り込めそうです。

ただ今時のゲーム機に繋げようとすると、認証を突破するのに少し特殊な手順が必要なため、g_hidの枠組みでは無理そうです。Linux-USB Gadget API Frameworkの上で直接動かせばなんとななりそうな気がしているので、また気が向いたら試してみます。今はとりあえず単体で遊べて、PCやjammaにも気軽に接続できるコントローラが手に入っただけで満足してます。