はじめに
モチベーション
周囲にRaspberry Piを勧めておきながらOrange Piで遊んでる今日このごろです。安価かつマルチコアでCPUパワー高めなのと、その気になればUSBデバイスとして動作させる事ができるのが魅力です。
今回はXbox One Xの発売を記念して、360のコントローラをばらし、Orange Piに繋げてみました。360のコントローラは中古で安価に入手できる上にLEDもついているためLチカで遊べます。またホリのファイティングスティックEX2とその亜種は市場に大量に出回っており、1000円前後で入手可能。三和やセイミツのスティック・ボタンを使ったコントローラと比べれば見劣りするかもしれませんが、数万円クラスのコントローラとの比較である事を考えると、コストパフォーマンスは抜群です。また、A/Bボタンの故障が多く報告されていますので、どうせなら千石電商にでも足を運び、三和のボタンに交換しても良いかと思います。ボタン全部交換しても1000円ちょっと。個人制作では難しいケース代だと思っても安いのではないかと。
この記事のカバーする範囲と今後の展望
この記事ではひとまずコントローラをOrange PiのGPIOに繋ぎ、コントローラの値の読み取り、LEDの制御を行えるようにします。
ここまででは実用性はありませうんが、次のステップとして、uinputを使ってLinux Input Subsystemにジョイスティックとして/dev/js*へ公開します。これによりジョイスティックに対応しているLinuxアプリからコントローラが利用可能となります。Orange Piをコントローラ内に格納して、Retrorange Piあたりを動かすと、ちょっとしたコントローラ型のゲーム機の完成です。
さらにはUSB Gadget Driverを通じてOrange PiをUSB HIDデバイスとして動作させ、PC等に繋いだ時にUSBのゲームコントローラとして認識できるようにしたいと思います。(記事を書き続ける気力が続くことに)乞うご期待。
コントローラの調査
解体
底板を外して裏返した写真になります。中央のメイン基板にMS製のUSBコントローラが載ってます。今回は360への接続はひとまず置いとして、この基板は取っ払います。ただし、A/Bボタンが直接はんだ付けされているため、ボタンからの切り離しに苦労するかと思います。コツとしては環境に優しくない鉛フリーではない半田を流し込んであげて、古い半田を溶かしやすくします。基板の再利用を考えないのなら、力づくで外すのもありですが、高確率でA/Bボタンも壊れるので覚悟しましょう。逆にボタン交換するつもりなら力づくでGo!
配線
写真で見て本体から下に出ている配線は今回は不要なので無視。本体に繋がるケーブルも不要です。残る配線は
- スティックから四方に2本ずつ伸びる8本の配線
- ボタンを支えている基板からの8パラの配線
- 上方左右の基板から出ている4パラずつの配線
- 上方中央の基板から出ている7パラの配線
になります。
スティックの配線
下側にある2本の配線が、スティックを上に倒した時の信号を伝える線になります。ほかも上側の配線が下入力の情報、右側左側がそれぞれ左入力、右入力と、上下左右が逆に対応するので注意が必要です(が、スティックの倒れ方を考えると自然に理解できるかと思います)。
スティックやボタンの配線ペアは、GNDとPull-upされた線の対になります。ボタンが押されるとGNDにショート、Pull-up側から電圧を読み取ることでON/OFFを判断できます。スティック・ボタン側の役割はショートさせる事なので、基本どっちがどっちでもかまわないのですが、元の配線で言えば、下に向かって伸びている白い線が上入力のPull-up、右から出てる白い対になる線がGNDです。他の3方向も同様の並びになります。
ボタンの配線
見ての通り、各ボタンから2つの端子が出ており、この端子間をショートさせるのがボタンの約目です。CNB_2としてコネクタ番号が振られていますが、1から8までLT/RT/Y/X/Y/RT/LTのボタン順にPull-up/GNDが交互に接続されています。メイン基板に直接繋がっていたA/Bボタンの配線は、オリジナルでは写真でみて左側がPull-up、右側がGNDでした。
1 | LR Pull-up |
2 | LR GND |
3 | RT Pull-up |
4 | RT GND |
5 | Y Pull-up |
6 | Y GND |
7 | X Pull-up |
8 | X GND |
上方左右の基板
左側がSTART/RB、右側がLB/BACKの基板です。
START/RB | LB/BACK | |
---|---|---|
1 | START Pull-up | BACK Pull-up |
2 | START GND | BACK GND |
3 | RB Pull-up | LB Pull-up |
4 | RB GND | LB GND be |
上方中央の基板
XboxロゴボタンとLEDの配線です。基板を外すと裏側にご丁寧にも回路図が書かれていました。
という事で、配線は以下の通り。
1 | LED3(右下) |
2 | LED1(左下) |
3 | LED4(右上) |
4 | LED2(左上) |
5 | V3.3 |
6 | SW3 Pull-up |
7 | SW3 GND |
LEDは共通電源に繋がっており、各LED端子をGNDに落とすと対応するLEDが点灯します。普段はPull-upに設定しておき、点灯時に0出力で良いでしょう。
制作
配線中の様子はこんな感じ。元の線材を使えるところは使いつつ、ピンソケットに一度繋いでます。GNDも途中で全部まとめちゃっても良かったんですが、一応ピン端子として個別に残しつつソケット内で合流させました。このソケットからOrange PiやRaspberry Piのピンヘッダに繋げることでGPIOから制御します。ここから先はブレッドボード感覚ですね。
今回のテストにはOrange Pi Zeroを使いました。ピンヘッダとGPIO名、配線の関係は以下の通り。端子が足りなくてLB/RBは今回省略。いずれPi Oneに繋ぐときには復活させたい。
1 | PA10 B | 2 | GND |
3 | PA13 X | 4 | PA14 BACK |
5 | PA02 Y | 6 | PA16 START |
7 | GND | 8 | PA15 上 |
9 | PA18 Xbox | 10 | 3.3V |
11 | PA19 LED1 | 12 | PA03 下 |
13 | GND | 14 | PA00 左 |
15 | PA07 LED2 | 16 | PA01 右 |
17 | PG07 LED3 | 18 | GND |
19 | PG06 LED4 | 20 | PA06 A |
21 | GND | 22 | PA11 LT |
23 | 5V | 24 | PA12 RT |
25 | 5V | 26 | 3.3V |
入力確認
以前書いたOrange Pi Zero Armbian導入後の設定とmmapなLチカの方法を取ります。ざっくり5m秒間隔で入力を確認するプログラムは次の通り。
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#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];
};
char pad_state[] = "@@@@@@@@@@@@@";
void update(int data) {
char state[13];
// 各ビットが0なら対応するボタンが押されている
state[ 0] = (data & (1 << 14)) ? '0' : '1';
state[ 1] = (data & (1 << 16)) ? '0' : '1';
state[ 2] = (data & (1 << 15)) ? '0' : '1';
state[ 3] = (data & (1 << 3)) ? '0' : '1';
state[ 4] = (data & (1 << 0)) ? '0' : '1';
state[ 5] = (data & (1 << 1)) ? '0' : '1';
state[ 6] = (data & (1 << 6)) ? '0' : '1';
state[ 7] = (data & (1 << 11)) ? '0' : '1';
state[ 8] = (data & (1 << 12)) ? '0' : '1';
state[ 9] = (data & (1 << 10)) ? '0' : '1';
state[10] = (data & (1 << 13)) ? '0' : '1';
state[11] = (data & (1 << 2)) ? '0' : '1';
state[12] = (data & (1 << 18)) ? '0' : '1';
// 入力内容が変わってたらコピーをとって画面に表示
if (memcmp(pad_state, state, 13)) {
memcpy(pad_state, state, 13);
puts(pad_state);
}
}
int main(int argc, char** argv) {
// 例によってメモリマップドI/Oをmmapで直接触りに行く
int mem = open("/dev/mem", O_RDWR | O_SYNC);
if (mem < 0) {
perror("open /dev/mem");
return -1;
}
int prot = PROT_READ | PROT_WRITE;
size_t size = sysconf(_SC_PAGE_SIZE);
char *pa_base = mmap(NULL, size, prot, MAP_SHARED, mem, GPIO_PA_BASE);
struct pio_cfg* pa = (struct pio_cfg*)&pa_base[GPIO_PA_OFFSET];
// ここまでの処理でpaが指す先はI/Oレジスタになる。
// そう言えば前回も忘れてたけど、volatile付けるべきかも。
// データシートを眺めつつ設定を正しく入れる。
// 今回はANDとORを使いGPIOに見えてて入力に使うピンだけを選んで設定。
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]
for (;;) {
update(pa->dat);
usleep(5000); // 5msec
}
return 0;
}
配線が間違えていなければ正しく入力が取れているはず。LEDの制御は出力になりますが、前回の記事も参考にすれば簡単にLチカできます。
まとめ
Xbox One Xが買えなかったので360のコントローラーをばらして遊んでます。