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用コード
#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);
}
}
マウス
teensy用コード
#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用コード
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 サンプルコード追記