Edited at

Xbox360のコントローラをばらして*Piに繋げる

More than 1 year has passed since last update.


はじめに


モチベーション

周囲に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のゲームコントローラとして認識できるようにしたいと思います。(記事を書き続ける気力が続くことに)乞うご期待。


コントローラの調査


解体

media-20171106.jpg

底板を外して裏返した写真になります。中央のメイン基板に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の配線です。基板を外すと裏側にご丁寧にも回路図が書かれていました。

001.jpg

という事で、配線は以下の通り。

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出力で良いでしょう。


制作

IMG_0931.JPG

配線中の様子はこんな感じ。元の線材を使えるところは使いつつ、ピンソケットに一度繋いでます。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のコントローラーをばらして遊んでます。