Edited at

GPIOに繋げたコントローラをゲームコントローラとして認識させる

More than 1 year has passed since last update.


はじめに

Xbox360のコントローラをばらして*Piに繋げるの続編として書かれています。GPIOに直接接続したXbox360のコントローラを、ゲームコントローラとして認識させます。

技術的にはLinux Input Subsystemのuinput moduleを使い、ユーザーランドから仮想的な入力デバイスを作成して実現します。


実践


オリジナルと同等の仮想デバイスを得る

まずは前の記事で利用したHori Fighting Stick EX2と同等の仮想デバイスを作る事にします。いきなりコードを示しつつ……

#include <fcntl.h>

#include <linux/input.h>
#include <linux/uinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int js_setup() {
int fd = open("/dev/uinput", O_WRONLY);
if (fd < 0)
return fd;

ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_KEYBIT, BTN_A); // A
ioctl(fd, UI_SET_KEYBIT, BTN_B); // B
ioctl(fd, UI_SET_KEYBIT, BTN_X); // X
ioctl(fd, UI_SET_KEYBIT, BTN_Y); // Y
ioctl(fd, UI_SET_KEYBIT, BTN_TL); // LB
ioctl(fd, UI_SET_KEYBIT, BTN_TR); // RB
ioctl(fd, UI_SET_KEYBIT, BTN_TL2); // LT
ioctl(fd, UI_SET_KEYBIT, BTN_TR2); // RT
ioctl(fd, UI_SET_KEYBIT, BTN_SELECT); // BACK
ioctl(fd, UI_SET_KEYBIT, BTN_START); // START
ioctl(fd, UI_SET_KEYBIT, BTN_MODE); // Xbox
ioctl(fd, UI_SET_KEYBIT, BTN_THUMBL);
ioctl(fd, UI_SET_KEYBIT, BTN_THUMBR);

ioctl(fd, UI_SET_EVBIT, EV_ABS);
ioctl(fd, UI_SET_ABSBIT, ABS_X);
ioctl(fd, UI_SET_ABSBIT, ABS_Y);
ioctl(fd, UI_SET_ABSBIT, ABS_RX);
ioctl(fd, UI_SET_ABSBIT, ABS_RY);
ioctl(fd, UI_SET_ABSBIT, ABS_HAT0X); // Stick L/R [-32767, 32767]
ioctl(fd, UI_SET_ABSBIT, ABS_HAT0Y); // Stick U/D [-32767, 32767]

// for kernel 3.4.113-sun8i (Orange Pi)
struct uinput_user_dev setup;
memset(&setup, 0, sizeof(setup));
snprintf(setup.name, UINPUT_MAX_NAME_SIZE,
"Hori Fighting Stick EX2 (Virtual)");
int i;
for (i = 0; i < 6; ++i) {
setup.absmax[i] = 32767;
setup.absmin[i] = -32767;
}
write(fd, &setup, sizeof(setup));

ioctl(fd, UI_DEV_CREATE);

return fd;
}

int main() {
int js_fd = js_setup();
if (js_fd < 0)
exit(EXIT_FAILURE);

for (;;) usleep(5000);
}

jstest-gtkで本物と疑似デバイスの情報を比べてみたのがこちらのスクリーンショット。実際にボタンが存在しないアナログレバーなども360準拠の入力は全て上がっており、6軸13ボタンのコントローラとして見えます。各ボタンが内部的にどの軸やボタンにアサインされているかもMappingで確認できます。


セットアップコードの詳細


ボタンと軸の列挙

int fd = open("/dev/uinput", O_WRONLY);

// 利用可能なボタンの列挙
ioctl(fd, UI_SET_EVBIT, EV_KEY);
ioctl(fd, UI_SET_KEYBIT, BTN_A);
...

// 利用可能な軸の列挙
ioctl(fd, UI_SET_EVBIT, EV_ABS);
ioctl(fd, UI_SET_ABSBIT, ABS_X);
...


/dev/uinputを開き、そこにコマンドを送り込む事で仮想デバイスを作成できます。まずはioctlを使って、利用可能なボタンと軸を列挙します。

登録されたデバイスは/dev/input/event*として公開されます。また、登録された内容からキーボードやマウス、ゲームコントローラと推測される場合には、/dev/input/js*などの形で別名が割り当てられます。ゲームの場合、こちらを見ている事も多いと思うので、EV_KEYからBTN_A、BTN_B、EV_ABSからABS_X/ABS_Yなどは最低限登録してあげると間違いありません。


名前とレンジの登録

// for kernel 3.4.113-sun8i (Orange Pi)

struct uinput_user_dev setup;
memset(&setup, 0, sizeof(setup));
snprintf(setup.name, UINPUT_MAX_NAME_SIZE,
"Hori Fighting Stick EX2 (Virtual)");
int i;
for (i = 0; i < 6; ++i) {
setup.absmax[i] = 32767;
setup.absmin[i] = -32767;
}
write(fd, &setup, sizeof(setup));

この部分は最近のkernelだと違うので注意が必要です。上記コードはAllwinner H2/H3系で使われている3.4.113で有効なコードになります。最近のkernelではstruct uinput_setupをioctlのUI_DEV_SETUPで送りつけることになっています。まぁ、そっちの方が素直ですね。

(2017/11/13更新)軸に関してはここで最小値、最大値を指定しないと、読み出し側で思わぬ不具合が発生するので注意。わかり易い例では0除算で落ちる、とか。また、一部のアプリでHAT0X/Yの時のみ上下左右が反転して読み取られる現象に遭遇してるのですが、まだ解決できていません。


仮想デバイスの生成

ioctl(fd, UI_DEV_CREATE);


最後に仮想デバイスの生成です。生成したユーザープロセスが終了するとデバイスも消えます(正確にはfdを閉じたら、かもしれませんが詳細未調査)。基本的には仮想デバイスを生成するプロセスは、この後ループでGPIOの情報を読み取りつつ、変化情報をfdから送り続ける事になります。なので、*Piで使う分には、rc.localからbackgroundで走らせる感じになりますね。

海外のサイトで比較が紹介されていますが、RAP V4 隼クラスで5ミリ秒の遅延があるので、usleepで5ミリ秒程度寝かしてループさせてあげれば十分かと思います。Dualshock 4がUSBデスクリプタで要求してくるポーリング感覚も5ミリ秒かな?360は一番短い間隔のエンドポイントが2ミリ秒に設定されてるっぽい。


入力データの反映

その前に、ひとまず前回のGPIOのセットアップコードを関数化した物がこちら。今回説明するつもりのないコードは外に出します。


GPIOのセットアップ

#define GPIO_PA_BASE 0x01c20000

#define GPIO_PA_OFFSET 0x0800

struct pio_cfg {
volatile uint32_t cfg[4];
volatile uint32_t dat;
volatile uint32_t drv[2];
volatile uint32_t pul[2];
}* pa;

int gpio_setup() {
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0)
return fd;

int prot = PROT_READ | PROT_WRITE;
size_t size = sysconf(_SC_PAGE_SIZE);
char *pa_base = mmap(NULL, size, prot, MAP_SHARED, fd, GPIO_PA_BASE);
pa = (struct pio_cfg*)&pa_base[GPIO_PA_OFFSET];

pa->cfg[0] &= 0x00ff0000; // PA07-PA00: input [7:6,3:0]
pa->cfg[0] |= 0x10000000; // PA07-PA00: output [7]

pa->cfg[1] &= 0x000000ff; // PA15-PA08: input [15:10]

pa->cfg[2] &= 0xffff00f0; // PA21-PA16: input [19:18,16]
pa->cfg[2] |= 0x00001000; // PA21-PA16: output [19]

pa->pul[0] &= 0x000f0f00; // PA15-PA00: pull-* disabled [15:10,7:6,3:0]
pa->pul[0] |= 0x55505055; // PA15-PA00: pull-up [15:10,7:6,3:0]

pa->pul[1] &= 0xffffff0c; // PA21-PA16: pull-* disabled [19:18,16]
pa->pul[1] |= 0x00000051; // PA21-PA16: pull-up [19:18,16]

return 0;
}


で、全体をまとめたコードがこちら。


コード全体像

#include <fcntl.h>

#include <linux/input.h>
#include <linux/uinput.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

...
int gpio_setup() { /* 省略 */ return 0; }
int js_setup() { /* 省略 */ return 0; }

void emit(int fd, int type, int code, int val) {
struct input_event e;
e.type = type;
e.code = code;
e.value = val;
e.time.tv_sec = e.time.tv_usec = 0; // to be ignored
write(fd, &e, sizeof(e));
}

int main() {
if (gpio_setup()) {
perror("open /dev/mem");
exit(EXIT_FAILURE);
}

int js_fd = js_setup();
if (js_fd < 0)
exit(EXIT_FAILURE);

int state = 0x3fffff; // 初期値として何も押していない情報を設定
for (;;) {
// 5ミリ秒間隔でループ
usleep(5000);

// 新しい情報を読み取り、前回の値と比較して差分がなければ再度ループ先頭へ
int new_state = pa->dat;
int changed_state = (state ^ new_state) & 0x0dfccf;
state = new_state;

if (!changed_state)
continue;

// 差分のあったボタンに応じて、ボタンのON/OFFを伝えるイベントを書き込む
if (changed_state & (1 << 6))
emit(js_fd, EV_KEY, BTN_A, ((state >> 6) & 1) ? 0 : 1);
if (changed_state & (1 << 10))
emit(js_fd, EV_KEY, BTN_B, ((state >> 10) & 1) ? 0 : 1);
if (changed_state & (1 << 13))
emit(js_fd, EV_KEY, BTN_X, ((state >> 13) & 1) ? 0 : 1);
if (changed_state & (1 << 2))
emit(js_fd, EV_KEY, BTN_Y, ((state >> 2) & 1) ? 0 : 1);

if (changed_state & (1 << 11))
emit(js_fd, EV_KEY, BTN_TL2, ((state >> 11) & 1) ? 0 : 1);
if (changed_state & (1 << 12))
emit(js_fd, EV_KEY, BTN_TR2, ((state >> 12) & 1) ? 0 : 1);

if (changed_state & (1 << 14))
emit(js_fd, EV_KEY, BTN_SELECT, ((state >> 14) & 1) ? 0 : 1);
if (changed_state & (1 << 16))
emit(js_fd, EV_KEY, BTN_START, ((state >> 16) & 1) ? 0 : 1);
if (changed_state & (1 << 18))
emit(js_fd, EV_KEY, BTN_MODE, ((state >> 18) & 1) ? 0 : 1);

// 差分のあった軸の読み取りはちょっと面倒
// まず、レバーの左右どちらかで変化があったか調べて
if (changed_state & 0x000003) {
// 最新の値で左が押されていれば-32767を、右が押されていれば32767を、
// どちらも押されていなければ0をイベントとして書き込む
int val = !(state & 1) ? -32767 : !(state & 2) ? 32767 : 0;
emit(js_fd, EV_ABS, ABS_HAT0X, val);
}
// 上下レバーも同様
if (changed_state & 0x008008) {
int val = !(state & 0x8000) ? -32767 : !(state & 8) ? 32767 : 0;
emit(js_fd, EV_ABS, ABS_HAT0Y, val);
}

// 最後に同期イベントを送ります。
// これ忘れるとpanic起こすかも。何度か強制reboot喰らいました。
emit(js_fd, EV_SYN, SYN_REPORT, 0);
}
return 0;
}


コメントでだいたい説明は済んでる気がするけど、もう少し説明すると


変化の検出

int changed_state = (state ^ new_state) & 0x0dfccf;


前の状態と現在の状態でXORをとって変化のあったbit一覧を得る、というのはBASIC時代からの常套手段だけど、知らない人は知らないし、初めて見たら混乱するかも。最後のANDは、コントローラに割り当てて無いポートからゴミが見えると嫌なのでふるい落としているだけ。


イベントを送るコード

void emit(int fd, int type, int code, int val) {

struct input_event e;
e.type = type;
e.code = code;
e.value = val;
e.time.tv_sec = e.time.tv_usec = 0; // to be ignored
write(fd, &e, sizeof(e));
}

kernel.orgのドキュメントに書かれたサンプルのemit関数ほぼそのまま。


ボタン入力イベントの生成

if (changed_state & (1 <<  6))

emit(js_fd, EV_KEY, BTN_A, ((state >> 6) & 1) ? 0 : 1);

これも、各ボタンに対応したビットに変化があったか調べて、変化があればボタンに対応するイベントを送っているだけ。現在のビット値が1なら0を0なら1を送ってます。


軸のイベントを生成

if (changed_state & 0x000003) {

int val = !(state & 1) ? -32767 : !(state & 2) ? 32767 : 0;
emit(js_fd, EV_ABS, ABS_HAT0X, val);
}

オリジナルが-32767から32767の値を取っていたので、同じ値を使いました。まず、左右どちらかでも変化したらイベントを生成します。実際に送る値は、左が押されていれば優先的に-32767を、右が押されていれば32767を。どちらも押されていなければ0を送っています。

静止画では何やってるのかわかりませんが。ざっくりこれで完成。デバイスをコントローラに内蔵させてゲームを起動するようにしておけば、コントローラ型おれおれゲーム機の出来上がり。

(2017/11/13更新)こんなざっくりしたコードですが、実際に使ってみると、USBで繋いだコントローラとの反応の違いはハッキリとわかりました。1フレーム以上の差は出てるようです。


まとめ

クラシックミニやXbox One Xをもっと出荷してください。