GCコン信号をteensy4.0で読む

ゲームキューブコントローラーと通信し、入力ステータスを読み取る。
GCコンは独自の端子・通信様式のためそのままではほかのデバイスで使えない。
GC接続タップは純正でも3000円程度で売られているので穏便に事を済ませたいならそれで充分かもしれない。
無線化や組み込みにして使いたい時には役立つだろう。

# GCコン仕様
## コントローラー機能
![IMG_7076.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3123751/031f8562-3e81-4e3f-b16d-e6c85f210f02.jpeg)
ジョイスティック(メイン)
ABXYボタン
RLトリガースイッチ ポジションあり
Zボタン
Cスティック
十字ボタン
スタートボタン
ランブル機能あり ジャイロなし

## 電気的仕様
ロジック3.3v、ランブルのみ5v要求。
以下ピンアウト、配線ひん剥いたケーブルの色を併記する。

![gc-socket1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3123751/24c09bd2-10e5-492f-8c2c-8cb629c738c1.gif)
(引用サイトより)

1 黄 ランブル電源(5v)
2 赤 シグナル
3 緑 GND
4 白 GND
5 ー NC
6 青 ロジック電源(3.3v)

シグナルはプルアップされているようで、平時HIGH。

## 通信プロトコル
双方向デジタルシングルライン通信。
3usのLOW,1usのHIGHで「0」、1usのLOW,3usのHIGHで「1」。

![GCコン図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3123751/821961bd-0232-452e-a9b0-f2ed53c3a338.png)


エンドコードに「1」を送信する。すなわち「00000000」を送信する時、実際は「00000000 1」が送信される。

### 接続認識
コンソールから「00000000」で接続認識応答を要求する。
応答は3Byteで、先の2Byteがコントローラーの種類、後の1Byteがコントローラーの設定ステータス。
必ずしも行わなければならないわけではない。正味今回は不要。詳細省略。

### 入力送信要求
入力状況要求は3Byte分あり、ByteA ByteB ByteCとして、
ByteA: 入力送信要求コード/アナログ値のセットも指定,
ByteB: アナログ値のbit数を指定,
ByteC: ランブルの有効/無効化指定。

ByteA:
「01000000」コントローラーは8Byteで応答。
「01000001」アナログのポジションセット？(動作は確認されなかった)/コントローラーは10Byteで応答?/増えた2Byteは0埋めされていた。(そうでない場合もあった)
「01000010」アナログのポジションセット？(動作は確認されなかった)↑と同様。

ByteB:
「00000abc」アナログ値のデータ長さを指定できる。「00000011」(両方8bit)でいい。
| a | b | c | Cスティック | LRボタンポジション |
|:-:|:-:|:-:|:-:|:-:|
|0|0|0|8bit|4bit|
|0|0|1|4bit|8bit|
|0|1|0|4bit|4bit|
|0|1|1|8bit|8bit|
|1|0|0|8bit|0bit|
|他|||8bit|4bit|

ByteC:
「00000000」: ランブル無効化
「00000001」: ランブル有効化

よって普通に値を取得する要求コードは「01000000 00000011 00000000」で、ランブル有効化は最後のbitを1にしたもの。

### 入力応答
コントローラーからの応答は(基本)8Byteで返される。以下にデコード表を示す。
![でコード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3123751/ff54b56b-9f78-4f33-a2b8-ada4aee2dcd5.png)
Asetはアナログのポジションセットを行ったかどうか。
1bit当たり4us、一度の応答に264us、一連の通信全体で352us+少しかかる。
送信要求から応答までは3~5usほどで、微妙に一定ではない。LOWをトリガーとして読むのがよいだろう。

![pulse.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3123751/165ab15b-b450-4070-88b1-39b0a96b4ddb.png)


↑信号をオシロで観測した図 要求が終了してから応答までのタイミングが若干振れる。


# サンプルコード
Arduinoベースで動くコードを以下に示す。
動作確認はTeensy4.0のみ。
(適当に書いてるので汚い。)

:::note warn
使うマイコンが5vロジックの場合は別途レベルシフタが必要。クロックが低い場合はレジスタ書き込みでdigitalWrite/Readを置き換える必要があるかも。要求/応答でpinMode切り替えもしているのでそこも調整要素か。(未検証)
:::

```c:GCpad.ino
#define GCpin 5
bool rumble=false;
int gcpad_buttons[18];

void sendSignal(char data[],int lengh){
  for(int i=0;i<lengh;i++){
    if(data[i]=='1'){//highLevel
      digitalWrite(GCpin,LOW);
      delayMicroseconds(1);
      digitalWrite(GCpin,HIGH);
      delayMicroseconds(3);
    }else{//lowLevel
      digitalWrite(GCpin,LOW);
      delayMicroseconds(3);
      digitalWrite(GCpin,HIGH);
      delayMicroseconds(1);
    }
  }
  digitalWrite(GCpin,LOW);
  delayMicroseconds(1);
  digitalWrite(GCpin,HIGH);
  delayMicroseconds(3);
  digitalWrite(GCpin,HIGH);
  
}

void setup() {
  pinMode(GCpin,OUTPUT);
  pinMode(4,OUTPUT);
  Serial.begin(115200);
  sendSignal("00000000",8);
  delay(100);
}

void loop() {
  if(gcpad_buttons[7])rumble=true;
  else rumble=false;
  
  pinMode(GCpin,OUTPUT);
  if(rumble)sendSignal("010000000000001100000001",24);
  else sendSignal("010000000000001100000000",24);
  
  pinMode(GCpin,INPUT);
  int values[64];
  values[0]=0;
  int texCnt=0;
  //1bit目捨て　1bit目は0で固定
  while(digitalRead(GCpin)==0);
  //ここ1bit 3us目　以降4us毎にデータ取得
  unsigned long stime,etime;
  stime=micros();
  for(int t=1;t<64;t++){
    while(micros()-stime<4);
    values[t]=digitalRead(GCpin);
    stime=micros();
  }
  //取得値表示
  /*for(int t=0;t<64;t++){
    Serial.print(values[t]);
    if((t+1)%8==0)Serial.print(" ");
  }*/
  
  //デコード
  Serial.print(" start:");Serial.print(values[3]);gcpad_buttons[0]=values[3];
  Serial.print(" Y:");Serial.print(values[4]);gcpad_buttons[1]=values[4];
  Serial.print(" X:");+Serial.print(values[5]);gcpad_buttons[2]=values[5];
  Serial.print(" B:");Serial.print(values[6]);gcpad_buttons[3]=values[6];
  Serial.print(" A:");Serial.print(values[7]);gcpad_buttons[4]=values[7];
  Serial.print(" L:");Serial.print(values[9]);gcpad_buttons[5]=values[9];
  Serial.print(" R:");Serial.print(values[10]);gcpad_buttons[6]=values[10];
  Serial.print(" Z:");Serial.print(values[11]);gcpad_buttons[7]=values[11];
  Serial.print(" up:");Serial.print(values[12]);gcpad_buttons[8]=values[12];
  Serial.print(" down:");Serial.print(values[13]);gcpad_buttons[9]=values[13];
  Serial.print(" right:");Serial.print(values[14]);gcpad_buttons[10]=values[14];
  Serial.print(" left:");Serial.print(values[15]);gcpad_buttons[11]=values[15];
  for(int c=0;c<6;c++){ //2進数変換
    int v=0;
    for(int i=0;i<8;i++){
        v+=values[i+c*8+16]*int(pow(2,(7-i)));
    }
    gcpad_buttons[11+c]=v;
    Serial.print(" stick");
    Serial.print(c);
    Serial.print(":");
    Serial.print(v);
  }
  Serial.println();
  delay(10);
  
}
```
別にイニシャライズではないので冒頭の「00000000」は不要。
ステータス要求を送り、応答をデコードしてシリアルモニタに表示する。間隔は適当に10msだが、あまり近づけるとコントローラーが応答できなくなる。
適宜改変して対応してください。

# おわり
ゆくゆくは無線化してもいいかも。現状使いどころはないが。

# 参考
https://www.int03.co.uk/crema/hardware/gamecube/gc-control.html

↑画像引用。


https://hitmen.c02.at/files/yagcd/yagcd/chap9.html#sec9

↑GCコンに限らず、GCの内部仕様について解析してある。

https://sandas.hatenablog.com/entry/2020/02/07/202523

↑「00000000」の応答3Byte目について解析してある。

# おまけ
teensyをPCで認識できるコントローラーにするコード。
<details>
<summary>先述のものに少し書き加えただけ</summary>

```GCconUSB.ino
#define GCpin 5
bool rumble=false;
int gcpad_buttons[18];

void sendSignal(char data[],int lengh){
  for(int i=0;i<lengh;i++){
    if(data[i]=='1'){//highLevel
      digitalWrite(GCpin,LOW);
      delayMicroseconds(1);
      digitalWrite(GCpin,HIGH);
      delayMicroseconds(3);
    }else{//lowLevel
      digitalWrite(GCpin,LOW);
      delayMicroseconds(3);
      digitalWrite(GCpin,HIGH);
      delayMicroseconds(1);
    }
  }
  digitalWrite(GCpin,LOW);
  delayMicroseconds(1);
  digitalWrite(GCpin,HIGH);
  delayMicroseconds(3);
  digitalWrite(GCpin,HIGH);
  
}

void setup() {
  pinMode(GCpin,OUTPUT);
  pinMode(4,OUTPUT);
  Serial.begin(115200);
  sendSignal("00000000",8);
  Joystick.hat(-1);
  delay(100);
  
}

void loop() {
  if(gcpad_buttons[7])rumble=true;
  else rumble=false;
  
  pinMode(GCpin,OUTPUT);
  if(rumble)sendSignal("010000000000001100000001",24);
  else sendSignal("010000000000001100000000",24);
  
  pinMode(GCpin,INPUT);
  int values[64];
  values[0]=0;
  int texCnt=0;
  //1bit目捨て　1bit目は0で固定
  while(digitalRead(GCpin)==0);
  //ここ1bit 3us目　以降4us毎にデータ取得
  unsigned long stime,etime;
  stime=micros();
  for(int t=1;t<64;t++){
    while(micros()-stime<4);
    values[t]=digitalRead(GCpin);
    stime=micros();
  }
  //取得値表示
  /*for(int t=0;t<64;t++){
    Serial.print(values[t]);
    if((t+1)%8==0)Serial.print(" ");
  }*/
  
  //デコード
  Serial.print(" start:");Serial.print(values[3]);gcpad_buttons[0]=values[3];Joystick.button(10,gcpad_buttons[0]);
  Serial.print(" Y:");Serial.print(values[4]);gcpad_buttons[1]=values[4];Joystick.button(1,gcpad_buttons[1]);
  Serial.print(" X:");Serial.print(values[5]);gcpad_buttons[2]=values[5];Joystick.button(4,gcpad_buttons[2]);
  Serial.print(" B:");Serial.print(values[6]);gcpad_buttons[3]=values[6];Joystick.button(2,gcpad_buttons[3]);
  Serial.print(" A:");Serial.print(values[7]);gcpad_buttons[4]=values[7];Joystick.button(3,gcpad_buttons[4]);
  Serial.print(" L:");Serial.print(values[9]);gcpad_buttons[5]=values[9];Joystick.button(5,gcpad_buttons[5]);
  Serial.print(" R:");Serial.print(values[10]);gcpad_buttons[6]=values[10];Joystick.button(6,gcpad_buttons[6]);
  Serial.print(" Z:");Serial.print(values[11]);gcpad_buttons[7]=values[11];Joystick.button(7,gcpad_buttons[7]); //78
  Serial.print(" up:");Serial.print(values[12]);gcpad_buttons[8]=values[12];Joystick.button(11,gcpad_buttons[8]);
  Serial.print(" down:");Serial.print(values[13]);gcpad_buttons[9]=values[13];Joystick.button(12,gcpad_buttons[9]);
  Serial.print(" right:");Serial.print(values[14]);gcpad_buttons[10]=values[14];Joystick.button(14,gcpad_buttons[10]);
  Serial.print(" left:");Serial.print(values[15]);gcpad_buttons[11]=values[15];Joystick.button(13,gcpad_buttons[11]);
  for(int c=0;c<6;c++){ //2進数変換
    int v=0;
    for(int i=0;i<8;i++){
        v+=values[i+c*8+16]*int(pow(2,(7-i)));
    }
    gcpad_buttons[11+c]=v;
    Serial.print(" stick");
    Serial.print(c);
    Serial.print(":");
    Serial.print(v);
  }
  Serial.println();
  Joystick.X(map(gcpad_buttons[11],20,225,0,1023));
  Joystick.Y(map(gcpad_buttons[12],20,225,1023,0));
  Joystick.Z(map(gcpad_buttons[13],20,225,0,1023));
  Joystick.Zrotate(map(gcpad_buttons[14],20,225,1023,0));
  Joystick.sliderLeft(map(gcpad_buttons[15],30,225,0,1023));
  Joystick.sliderRight(map(gcpad_buttons[16],30,225,0,1023));
  delay(10);
}
```
</details>

# 私的ログ
'25/05/18 GCコン入手
'25/05/21 文献漁り サンプルコードはうまく動かず自作へ切り替え
'25/05/22 通信成功 
'25/05/23 記事作成 デコードプログラム一旦完成
'25/05/24 配線を綺麗にする ランブル成功 要求コードを全部理解
'25/05/25 記事完成