0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

teensyでSFCコントローラーの信号を模倣する

0
Last updated at Posted at 2026-02-22

teensyにSFCコントローラー達のフリをしてもらう。
通信規格はとてもシンプル。

↑聖典。SFCのロジックの全てが載っている。

この記事ではどちらから数えるか,反転の有無など混乱を避けるため、Serialはサイクルで数え、信号はHIGH/LOWで表記する。

コントローラーコネクタ

その前に...

コントローラーのオスコネクタの形は扱いにくく、自作は困難と思われる。
ジャンクコントローラーを勇気の切断すれば入手できるが、ここで一つ注意点。
通常のコントローラーでは5番、6番ピンを使用しないため、コネクタにその金具と配線が無い。
金具は円環状で、適当な金属辺でうまくいくかは怪しい。
確実な方法は、コントローラー2つを金具のためだけに犠牲にする or 5,6番ピンを使うコントローラー...つまりスーパースコープ受信器から奪うか。

↑分解し端子部を露出させた。
ここにたどり着く前にコネクタの外装を外す必要があるが、ツメがとても外れづらい。
かなり日焼けした個体だったので、弄っている間に外装が割れて剥がれてしまった。
いっそのこと、この方が分解しやすいかも。
3dpで外装を作り直してもいいかもしれないな。

ピンアウト

コントローラー端子は上図のような配線。1,2,3,4,7番ピンは図に示した色で配線されている。5,6番ピンは通常のコントローラーでは配線されていない。

各通信プロトコル

コントローラー

SFCが送信するLatch,Clockに合わせてデータをSerialで送信する。
実装としては、Latch上りで送信データ作成、データの1bit目を送信、以降Clockの上り毎に次のデータをSerial出力すればよい。資料にはSerialはnormallyLOWであるとあるが、HIGHのままでも動作した。Clock時点さえ合っていればなんでもよいようだ。
コントローラーでの配列は以下。

( https://www.repairfaq.org/REPAIR/F_SNES.html より)
SFCはActive:LOWであり、ボタンが押されている時にLOW,いないときにHIGHである。
13~16サイクル目はコントローラーの識別子で、通常コントローラーでは全てHIGHである。

マウス

16サイクル目がLOWであるとき、SFCはコントローラー端子に接続しているものをマウスとして識別する。
マウスの場合、通常の16サイクルの2.5ms後にSFCから追加のクロックが送られ、ここでマウスの移動量を送信することになる。追加サイクルは通常サイクルと異なるパルス幅で、8us周期/0.5usのLOW時間となっている。
また、9サイクル目が左クリック、10サイクル目が右クリックとなる。
実装としては通常コントローラーと同様、送信量を32サイクル分用意するだけ。

追加サイクルの配列は以下。

(同じく https://www.repairfaq.org/REPAIR/F_SNES.html より)

スーパースコープ

これはこれ単体で話が長くなるので以下記事へ分離。

スーパースコープでは6番ピンを使用する。各ボタンは通常のコントローラーと同様に送信する。マウスのような追加bitはない。

マルチタップ5 (未確認,ドキュメントより)

・マルチタップは2Pモードの時はただのパススルーとなり、ロジック的に接続は検知されない。
・マウス、スーパースコープのような識別子はない
・識別方法:latchを立てる、pin5を読み0xFFであることを確認、latchをおろす、pin5が0xFFでなければマルチタップと判定。
・↑latch下りで1サイクル目を送り始めるので、ctr3のserialがつながったpin5は0xFFでなくなる。=ctr3がつながってないと判定されないのでは? 違う
・コントローラー側のpin5,6はNC。なのでスコープは使えない。追加クロックも使うのでマウスも使えない。
・latch,clockはctr2&3,ctr4&5と共有されており、ctr2のserialはpin4,ctr3はpin5と接続。4&5も同様。
・pin5、pin6を使用。pin6はマルチプレクサに接続され、ctr2&3,ctr4&5とそれぞれ切り替える。
・"17サイクル目"を読むことで次にコントローラーがいるかどうかを検知(?)

つまり...
pin4/INPUT:D0(ctr2,4のserial)
pin5/INPUT:D1(ctr3,5のserial)
pin6/OUTPUT:select(ctr2&3 or ctr4&5)
クロックは32回送るはず?
これだけ?

サンプルコード

全体コピペは非推奨(やや妙なつくりになったため)。部分つぎはぎして使うとよいのではないでしょうか。

通常コントローラー

teensy用コード
sfcController.ino
#include <Arduino.h>

const int PIN_LATCH = 3;
const int PIN_CLOCK = 2;
const int PIN_DATA  = 4;


#define BTN_B      0
#define BTN_Y      1
#define BTN_SELECT 2
#define BTN_START  3
#define BTN_UP     4
#define BTN_DOWN   5
#define BTN_LEFT   6
#define BTN_RIGHT  7
#define BTN_A      8
#define BTN_X      9
#define BTN_L      10
#define BTN_R      11

volatile uint16_t buttonStates = 0xFFFF;
volatile int bitCounter = 0;

void setup() {
  pinMode(PIN_LATCH, INPUT);
  pinMode(PIN_CLOCK, INPUT);
  pinMode(PIN_DATA, OUTPUT);
  digitalWrite(PIN_DATA, HIGH);

  attachInterrupt(digitalPinToInterrupt(PIN_LATCH), latchISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PIN_CLOCK), clockISR, RISING);

  Serial.begin(115200);

}

void latchISR() {
  bitCounter = 0;
  //1サイクル目だけlatch時点で出力しておく
  digitalWriteFast(PIN_DATA, (buttonStates & (1 << bitCounter)) ? HIGH : LOW);
  bitCounter++;
}

void clockISR() {
  if (bitCounter < 16) {//クロック上りごとにサイクルを回して出力
    digitalWriteFast(PIN_DATA, (buttonStates & (1 << bitCounter)) ? HIGH : LOW);
    bitCounter++;
  } else {
    digitalWriteFast(PIN_DATA, HIGH);
  }
}


#define RECEIVELEN 12
String input;
int values[RECEIVELEN];

void loop() {
  //serialを受け取る
  input = Serial.readStringUntil('\n');
  int lastIndex = 0;
  int commaIndex = 0;
  int i = 0;
  while ((commaIndex = input.indexOf(',', lastIndex)) != -1 && i < RECEIVELEN-1) {
    values[i] = input.substring(lastIndex, commaIndex).toInt();
    lastIndex = commaIndex + 1;
    i++;
  }
  if (i == RECEIVELEN-1) {
    values[RECEIVELEN-1] = input.substring(lastIndex).toInt();
  }

  // デバッグ表示
  Serial.print("receive: ");
  for (int j = 0; j < RECEIVELEN; j++) {
    Serial.print(values[j]);
    Serial.print(j < RECEIVELEN-1 ? ", " : "");
  }
  Serial.print("\n");
  
  //データ反映
  for(int i=0;i<12;i++){ 
    if(bool(values[i])) buttonStates &= ~(1 << i);
    else buttonStates |= ~(1 << i);
  }
  
}
PC等とのシリアル通信はマウスとほぼ共通にて略。

マウス

teensy用コード
sfcMouse.ino
#include <Arduino.h>

const int PIN_LATCH = 3;
const int PIN_CLOCK = 2;
const int PIN_DATA  = 4;

volatile uint8_t rawPacket[4];
volatile int bitCounter = 0;

void setup() {
  pinMode(PIN_LATCH, INPUT);
  pinMode(PIN_CLOCK, INPUT);
  pinMode(PIN_DATA, OUTPUT);
  digitalWriteFast(PIN_DATA, HIGH);

  attachInterrupt(digitalPinToInterrupt(PIN_LATCH), latchISR, RISING);
  attachInterrupt(digitalPinToInterrupt(PIN_CLOCK), clockISR, RISING);

  updateMouseData(0, 0, false, false);
  Serial.begin(115200);
}

void updateMouseData(int8_t dx, int8_t dy, bool btnL, bool btnR) {
  // 4byte分
  for(int i=0; i<4; i++) rawPacket[i] = 0xFF;

  // Byte 1: なし
  // Byte 2: クリック、識別子
  if (btnL) rawPacket[1] &= ~(1 << 0); // サイクル8、左クリック
  if (btnR) rawPacket[1] &= ~(1 << 1); // サイクル9、右クリック
  rawPacket[1] &= ~(1 << 7); // サイクル13~16、識別子HHHL
  // Byte 3: Y軸
  if (dy > 0) rawPacket[2] &= ~(1 << 0); // サイクル17、Y方向
  else        rawPacket[2] |=  (1 << 0); 
  uint8_t absY = abs(dy) & 0x7F;
  for(int i=0; i<7; i++) {
    if (absY & (1 << (6-i))) rawPacket[2] &= ~(1 << (i+1)); //サイクル18~15、Y値
  }
  // Byte 4: X軸
  if (dx > 0) rawPacket[3] &= ~(1 << 0); // サイクル25、X方向
  else        rawPacket[3] |=  (1 << 0);
  uint8_t absX = abs(dx) & 0x7F;
  for(int i=0; i<7; i++) {
    if (absX & (1 << (6-i))) rawPacket[3] &= ~(1 << (i+1)); //サイクル26~32、X値
  }
}

void latchISR() {
  bitCounter = 0;
  //1サイクル目だけlatch時点で出力しておく
  digitalWriteFast(PIN_DATA, (rawPacket[0] & (1 << 0)) ? HIGH : LOW);
  bitCounter = 1;
}

void clockISR() {
  // クロック上りごとにサイクルを回して出力
  // ↓rawPacket形式用の処理
  int byteIdx = bitCounter >> 3;   // bitCounter / 8
  int bitIdx  = bitCounter & 0x07; // bitCounter % 8
  if (bitCounter < 32) {
    digitalWriteFast(PIN_DATA, (rawPacket[byteIdx] & (1 << bitIdx)) ? HIGH : LOW);
    bitCounter++;
  } else {
    digitalWriteFast(PIN_DATA, HIGH);
  }
}

#define RECEIVELEN 4
String input;
int values[RECEIVELEN];

void loop() {
  //serialを受け取る
  input = Serial.readStringUntil('\n'); 
  int lastIndex = 0;
  int commaIndex = 0;
  int i = 0;
  while ((commaIndex = input.indexOf(',', lastIndex)) != -1 && i < RECEIVELEN-1) {
    values[i] = input.substring(lastIndex, commaIndex).toInt();
    lastIndex = commaIndex + 1;
    i++;
  }
  if (i == RECEIVELEN-1) {
    values[RECEIVELEN-1] = input.substring(lastIndex).toInt();
  }

  // デバッグ表示
  Serial.print("receive: ");
  for (int j = 0; j < RECEIVELEN; j++) {
    Serial.print(values[j]);
    Serial.print(j < RECEIVELEN-1 ? ", " : "");
  }
  Serial.print("\n");
  
  //データ 反映
  updateMouseData(values[0], values[1], bool(values[2]), bool(values[3]));
}

PC用コード
sfcMouseSender.py
import pygame
import serial
import sys

COM_PORT = "COM5"
BAUD_RATE = 115200

try:
    ser = serial.Serial(COM_PORT, BAUD_RATE, timeout=1)
    print(f"[o] serial com {COM_PORT} begin({BAUD_RATE}bps)")
except Exception as e:
    print(f"[x]Serial error: {e}")
    sys.exit(1)

pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
dx,dy,btnR,btnL=0,0,0,0
while True:
    dx,dy=0,0
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.MOUSEMOTION:
            x, y = event.pos       
            dx, dy = event.rel     
        if event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                btnL=1
            if event.button == 3:
                btnR=1
        if event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                btnL=0
            if event.button == 3:
                btnR=0
    
    mx,my,rc,lc=-dx,-dy,btnR,btnL

    send_data = f"{mx},{my},{rc},{lc}\n"
    ser.write(send_data.encode('utf-8'))

    if ser.in_waiting:
        response = ser.readline().decode('utf-8').strip()
        print(f"[Arduino] {response}")

    clock.tick(60)

参考

↑SFCの開発マニュアルらしきもの。たいていはここに書いてあると思われる。(再掲)
これらの資料では0/1=HIGH/LOWと対応する。SFCはActive:LOWである。
ここに書かなかったマルチタップやその他についても記載がある。

↑コネクタのピンアウトなども含め抜粋したもの。(再掲)

↑各所の文献からとりまとめているサイト。

ログ

2026/01/17 コネクタの為犠牲になってもらうコントローラーを入手
2026/02/20 コネクタに配線,圧着 通常コントローラー,マウスの模倣成功
2026/02/22 記事制作
2026/02/25 サンプルコード追記

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?