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

# はじめに
## モチベーション
周囲に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](https://qiita-image-store.s3.amazonaws.com/0/12843/301cadb4-35bc-5ae7-c137-b2e70791577d.jpeg)
底板を外して裏返した写真になります。中央のメイン基板に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](https://qiita-image-store.s3.amazonaws.com/0/12843/7917d137-8fe7-1c18-1040-b0900bb9a986.jpeg)
という事で、配線は以下の通り。

|   |             |
|:-:|:-----------:|
| 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](https://qiita-image-store.s3.amazonaws.com/0/12843/6219d16b-c5d2-5d18-811d-f0e65e5e4e82.jpeg)
配線中の様子はこんな感じ。元の線材を使えるところは使いつつ、ピンソケットに一度繋いでます。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チカ](https://qiita.com/toyoshim/items/f097579a032af6047243)の方法を取ります。ざっくり5m秒間隔で入力を確認するプログラムは次の通り。

```c
#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のコントローラーをばらして遊んでます。