ORANGE pico のゲームコントローラ連携を試す

[ORANGE pico](http://www.picosoft.co.jp/orange/) のゲームコントローラ連携を試してみた。

# ゲームコントローラの入手

ORANGE pico 対応のゲームコントローラは[aitendoにページがある](https://www.aitendo.com/product/16046)が、残念ながら在庫切れとなっている。

似たコントローラがAliExpressで売られていたので、以下を購入した。

[Controller For NES Classic Edition Mini For Nintendo Entertainment System Controller Gamepad Joystick with 1.8m built-in Cable](https://www.aliexpress.com/item/33034011002.html)

コントローラとORANGE pico を接続するには、公式にはWiiChunkアダプターを使用することになっている。
これも[aitendoでは在庫切れ](https://www.aitendo.com/product/16045)だが、[オレンジピコショップで入手](https://store.shopping.yahoo.co.jp/orangepicoshop/pico-m-013.html)できた。

# ゲームコントローラの状態を読み取るプログラム その1

以下のプログラムは、ゲームコントローラを初期化した後、ボタンの状態を読み取って画面に表示する。
さらに、ゲームコントローラの初期化および読み取りを行う関数の実行中はPORT1にHIGHを出力し、処理中であることがわかるようにする。

```
10 ioctrl 1,0:out 1,0
20 cpeek &H81AA:cpoke asc("^"),-1:cpeek &H81AB:cpoke asc("v"),-1
30 cpeek &H81A9:cpoke asc("<"),-1:cpeek &H81A8:cpoke asc(">"),-1
40 cls:ix=5:iy=2:lx=5:rx=25:by=5
50 for i=0 to 15
60 locate lx,by+i:print format$("%2d",i);
70 locate lx+3+7,by+i:print 0;
80 locate rx,by+i:print format$("%2d",16+i)
90 locate rx+3+7,by+i:print 0;
100 next
110 locate lx+3,by:print "^";
120 locate lx+3,by+1:print "v";
130 locate lx+3,by+2:print "<";
140 locate lx+3,by+3:print ">";
150 locate lx+3,by+4:print "A";
160 locate lx+3,by+5:print "B";
170 locate lx+3,by+6:print "X";
180 locate lx+3,by+7:print "Y";
190 locate lx+3,by+8:print "L";
200 locate lx+3,by+9:print "R";
210 locate lx+3,by+10:print "START";
220 locate lx+3,by+11:print "SELECT";
230 pstatus=0
240 out 1,1
250 ires=button(-1)
260 out 1,0
270 locate ix,iy:print "INIT ";ires;
280 out 1,1
290 status=button(0)
300 out 1,0
310 diff=status^pstatus
320 for i=0 to 15
330 if (diff>>i)&1 then locate lx+3+7,by+i:print (status>>i)&1;
340 if (diff>>(i+16))&1 then locate rx+3+7,by+i:print (status>>(i+16))&1;
350 next
360 pstatus=status
370 locate 0,by+16
380 goto 280
```

このプログラムは、[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.ja) でライセンスする。

まずはゲームコントローラを接続せずに実行してみると、初期化のために呼び出した関数は `0` を返し、全ボタンが押されている判定となった。

![ゲームコントローラを接続しない状態での実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/f7c7328c-13fd-fa4b-f597-691c9bcc4a18.png)

WiiChunkアダプタを経由して ORANGE pico とゲームコントローラを接続し、さらに[ロジックアナライザ](https://www.switch-science.com/products/7642)も接続した。

![接続の様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ead7fbf5-cb4f-902a-cc16-ae5d322eaccd.jpeg)

すると、初期化関数は変わらず `0` を返し、ボタンの状態は0と1が点滅していた。
コントローラーのボタンを押すと、状態は1で安定した。

ロジックアナライザのキャプチャ結果を見ると、ボタンの取得処理がすぐに終わっているときと時間がかかっているときがあることがわかる。

![通信の様子 (全体)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/f7305a81-0776-d49b-04ce-5adceda1bf29.png)

詳しく見てみると、取得処理がすぐに終わっているときはNACKが返っており、取得に失敗しているようである。

![通信の様子 (失敗)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/cd9500df-0962-e4b7-5842-4631f3eb57d5.png)

取得に成功しているときは、1回の取得に約10msかかっている。

![通信の様子 (成功)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/9272594e-93e7-6c3a-be80-073cfee33285.png)

取得処理は、以下のように行われているようである。

1. 7ビットアドレス `0x52` に `0x00` の1バイトを送信し、ストップコンディションを送信する
   ![取得の様子 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/fc211d68-b352-a290-ce5a-9d646af4c601.png)
2. 10ms待機する
   ![取得の様子 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/679f438b-9223-027f-b214-332bfe930ae2.png)
3. 7ビットアドレス `0x52` から8バイトを受信し、ストップコンディションを送信する
   ![取得の様子 3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ba080c69-f8ea-8c66-a98d-4d8a7c643901.png)

受信した内容とボタンの判定の関係は、後で解析を行う。

取得に成功した際はストップコンディションを送信してSCLをHIGHにしているのに対し、取得に失敗した際はストップコンディションを送信せずSCLがLOWのままになっている、という点にも注目したい。

初期化処理は、間隔をあけた数回の通信からなる。

![初期化の様子 (全体)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/c2a1ab0e-35f7-4dca-9443-b2489d3f2a50.png)

初期化処理は、以下のように行われた。

1. 7ビットアドレス `0x52` に `0x40 0x00` の2バイトを送信し、ストップコンディションを送信する
   ![初期化の様子 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/12c8435b-7bd8-896c-226c-7e0172a6609d.png)
2. 15ms待機する
   ![初期化の様子 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/dda7459f-ab30-aa6c-afba-85ca0a99f0c8.png)
3. 7ビットアドレス `0x52` への送信を試み、NACKが返ってきた
   ![初期化の様子 3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ff8b5f20-1930-92e1-92e2-72e872c561dd.png)
4. 15ms待機する
   ![初期化の様子 4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/fabfc30e-23f6-a44b-a8c7-92bf7d1e1ece.png)
5. 7ビットアドレス `0x52` に `0xFB 0x00` の2バイトを送信し、ストップコンディションを送信する。続いて7ビットアドレス `0x52` への送信を試み、NACKが返ってきた
   ![初期化の様子 5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/b0bec88b-f70c-b79e-1a1c-9b7ab70fa72f.png)
6. 10ms待機する
   ![初期化の様子 5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/47a68f05-2f2b-869c-7e7e-4dc9111d0e18.png)
7. 7 ビットアドレス `0x52` からの受信を試み、NACKが返ってきた
   ![初期化の様子 6](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/c932afa4-7f4e-4c5e-022f-54b11cfc1654.png)

ゲームコントローラを接続していない状態での初期化処理も、それぞれ送信する場面でNACKが返って送信が行われない以外は同様であった。

# ゲームコントローラの状態を読み取るプログラム その2

先ほどのプログラムではボタンの状態の取得に成功するときと失敗するときがあった。
これは高速でポーリングしているからかもしれないと考え、1秒間隔で状態を取得するようにしてみた。
初期化と状態の読み取り、およびそれらの結果の出力だけを行うシンプルなプログラムである。

```
10 print button(-1)
20 pause 1000
30 print button(0)
40 goto 20
```

このプログラムは、[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.ja) でライセンスする。

さらに、コマンド

```
i2c 2,1
```

を実行し、トレース出力を有効化した。
この状態でプログラムを実行すると、以下の出力が得られた。

```
[S][a4][A][40][A][00][A][P]
[S][a4][S][a4][A][fb][A][00][A][P]
[S][a4][A][5c][A][78][S][a5]0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5]-1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5]-1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4]-1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5]-1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5]-1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
```

最初のプログラムでは失敗していた初期化処理最後の読み込みに成功し、ボタンの状態の取得同様の8バイトを読み込んでいる。
ボタンの状態の取得に成功した場合は (ボタンを押していないので) `0` が返っており、失敗した場合は `-1` が返っているようである。
状態の取得に失敗する場合は、送信に失敗する場合と、送信は成功して受信に失敗する場合があるようである。
1秒間隔というゲームにはあまり役立たなそうな低速でのポーリングでも取得失敗が発生するため、ポーリング間隔の問題ではなさそうであることがわかった。

# Wiiコネクタ経由での接続

ボタンの状態の取得に失敗することがある原因としてWiiChunkアダプタとコントローラの接触の問題を疑い、かわりにaitendoの[Wiiコネクタwith基板](https://www.aitendo.com/product/16639)経由で接続してみることにした。

そのため、まずは端子の関係を調べた。
まず、Wiiコネクタ(オス)にWiiChunkアダプタを接続し、接続関係を調べた。
その結果、Wiiコネクタの端子とWiiChunkアダプタの端子の関係は以下のようになっていることがわかった。

![Wiiコネクタの端子とWiiChunkアダプタの端子の関係](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/57cb9e08-5549-02fb-5915-1aeeec87aa36.jpeg)

次に、Wiiコネクタ(オス)を変換基板に接続したWiiコネクタ(メス)に接続し、端子の対応関係を調べた。
その結果、変換基板に書かれている番号とWiiChunkアダプタの端子の関係は以下のようになっていることがわかった。

|WiiChunkアダプタ|変換基板|
|:-:|:-:|
|-|6|
|+|1|
|D|3|
|C|4|

これに沿って ORANGE pico に接続し、実験を行った。

![Wiiコネクタ経由でのコントローラの接続](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/9add2d3c-a279-f753-17ac-76f5f5e115b0.jpeg)

しかし、取得の失敗は無くならなかった。
原因としては、たとえば以下が考えられるが、現時点ではわからない。

* もともと取得失敗が発生しうるものである
* 今回入手したコントローラが低品質である
* 製造・輸送・保管など何らかの問題で、ケーブルなどが断線しかけている

# Arduinoを用いたプロトコルの解析

これまでの調査により、以下のことがわかっている。

* 初期化時に `0x40` が送信される
* ボタンの取得時に8バイトが読み込まれ、その値は `0x00 0x00 0x00 0x00 0xff 0xff 0x00 0x00` が多い

これらの情報をもとに、Arduino UNO 用の以下のプログラムを作成した。

```c++
#include <Wire.h>

const int peripheral7bitAddress = 0x52;

unsigned char receiveBuffer[32];
unsigned char sendBuffer[8];

int phase = -3;

void setup() {
  Wire.begin(peripheral7bitAddress);
  // disable internal pull-up
  pinMode(SDA, INPUT);
  pinMode(SCL, INPUT);
  Wire.onReceive(receiveHandler);
  Wire.onRequest(requestHandler);
}

void loop() {
}

void receiveHandler(int numBytesRead) {
  (void)numBytesRead;
  int receivedSize = 0;
  while (Wire.available()) {
    int c = Wire.read();
    if (receivedSize < 32) receiveBuffer[receivedSize++] = c;
  }
  if (receivedSize > 0 && receiveBuffer[0] == 0x40) {
    // assuming initialization
    phase = -3;
  }
}

void requestHandler() {
  switch (phase) {
    case -3:
      // all 0x00
      for (int i = 0; i < 8; i++) sendBuffer[i] = 0x00;
      break;
    case -2:
      // all 0xff
      for (int i = 0; i < 8; i++) sendBuffer[i] = 0xff;
      break;
    case -1:
      // reversed
      for (int i = 0; i < 8; i++) sendBuffer[i] = 0xff;
      sendBuffer[4] = sendBuffer[5] = 0x00;
      break;
    default:
      // normal
      for (int i = 0; i < 8; i++) sendBuffer[i] = 0x00;
      sendBuffer[4] = sendBuffer[5] = 0xff;
      if (1 <= phase && phase <= 8) sendBuffer[4] &= ~(1 << (phase - 1));
      if (9 <= phase && phase <= 16) sendBuffer[5] &= ~(1 << (phase - 9));
  }
  for (int i = 0; i < 8; i++) {
    Wire.write(sendBuffer[i]);
  }
  phase++;
  if (phase > 16) phase = -3;
}
```

このプログラムは、[CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.ja) でライセンスする。

このプログラムを書き込んだArduino (互換機) を以下のように ORANGE pico に接続した。

|Arduino (互換機)|ORANGE pico|
|---|---|
|GND|GND|
|A4|SDA|
|A5|SCL|

ORANGE pico で2番目のプログラムを実行してトレース結果を得ると、以下のようになった。

```
[S][a4][A][40][A][00][A][P]
[S][a4][A][f0][A][55][A][P]
[S][a4][A][fb][A][00][A][P]
[S][a4][A][5c][A][78][A][66][A][61][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][ff][A][ff][A][ff][A][ff][A][ff][A][ff][A][ff][A][ff][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][ff][A][ff][A][ff][A][ff][A][00][A][00][A][ff][A][ff][N][P]
3135
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][fe][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][fd][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][fb][A][ff][A][00][A][00][N][P]
1024
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][f7][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ef][A][ff][A][00][A][00][N][P]
2048
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][df][A][ff][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][bf][A][ff][A][00][A][00][N][P]
2
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][7f][A][ff][A][00][A][00][N][P]
8
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][fe][A][00][A][00][N][P]
1
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][fd][A][00][A][00][N][P]
4
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][fb][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][f7][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][ef][A][00][A][00][N][P]
16
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][df][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][bf][A][00][A][00][N][P]
32
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][ff][A][7f][A][00][A][00][N][P]
0
[S][a4][A][00][A][P]
[S][a5][A][00][A][00][A][00][A][00][A][00][A][00][A][00][A][00][N][P]
3135
```

初期化処理において送信失敗が発生せず、きれいなデータが得られた。
初期化処理の最後の読み込みが8バイトではなく6バイトになっている。

「3135」を十六進数で表すと「0xc3f」であり、以下のことが推測できる。

* 以下のビットが「0」の場合はそれぞれ対応する数値を取得結果に加算する。「1」の場合は何もしない。
* それ以外のビットは取得結果に影響を与えない。

|バイト (0-origin)|ビット|加算する値|対応するボタン|
|:-:|:-:|--:|---|
|5|0b00000001|1|方向キー上|
|4|0b01000000|2|方向キー下|
|5|0b00000010|4|方向キー左|
|4|0b10000000|8|方向キー右|
|5|0b00010000|16|Aボタン|
|5|0b00000100|32|Bボタン|
|4|0b00000100|1024|STARTボタン|
|4|0b00010000|2048|SELECTボタン|

公式のコマンド一覧で「未使用」となっているXボタン、Yボタン、Lボタン、Rボタンについては、周辺機器デバイスを独自に開発しても、押されていると認識させるのは不可能であるか、少なくとも容易ではないようである。

ここで、初期化処理が返す `0` は初期化時のボタンの状態 (押されていない) であるという仮説を立てた。
そこで、上記の Arduino プログラムの

```c++
    // assuming initialization
    phase = -3;
```

を

```c++
    // assuming initialization
    phase = 3;
```

に変更し、最初の読み込みでボタンが押された状態を返すようにしてみた。
結果は、変わらず初期化では `0` が返され、ボタンの状態が反映されるわけではなさそうであった。

# まとめ

* ゲームコントローラのボタン情報の取得には、成功した場合1回につき10ms強かかる。
* ゲームコントローラの初期化処理は、成功しても失敗しても `0` を返す。
* ゲームコントローラのボタン情報の取得処理は、失敗すると `-1` を返す。
* ゲームコントローラのボタン情報の取得時には、I2Cの7bitアドレス `0x52` から8バイトを読み込み、そのうち一部のビットがボタン情報に変換される。

ボタン情報の取得失敗時に返される `-1` をそのままボタン情報として扱ってしまうと誤動作の原因となるので、`-1` が返ってきた場合はかわりに前回取得に成功したボタン情報を用いるなどの工夫をするとよさそうである。