スーパーファミコン コントローラーの無線化プロジェクト:動作バージョン1


動作のバグが残っていた( https://funasover.blogspot.com/2025/04/blog-post.html)スーパーファミコンのコントローラー無線化プロジェクト。
多少力技だが一旦の問題解決、完全動作。
本来は、BLE通信で低消費電力に作るべきだと思うが、まずまずはWiFiを利用している点はあしからず。BLE化は後ほど。また現時点ではブレッドボード品。BLE完了時点で基板を起こそうかと。

【以前の課題】
SFC本体が発するP/Sのパルスを検出して、ボタン状態を本体に送り込む際、パルスの検出時間にバラつき(ジッター)があり、どうする事もできず。
おそらく、ESP-IDFなどの開発環境で開発すれば回避できるかもしれないが、お手軽Arduino環境下ではお手上げ。

【解決手法】
ESPのArduino環境下でのGPIOインターラプトは使えないのであれば、別のマイコンという事でCH32V003を使う。ESPとの間はSPIで通信。肝心なCH32V003のGPIOインターラプトのバラつきは非常に小さい事を動作を確認済み(https://funasover.blogspot.com/2025/05/ch32v003gpio-external-interrupt.html#more

ICが一つ増えるが、50円のIC、上の図ではP/Sをレベルコンバーターを通しているが、CH32V003は5VトレラントのGPIOピンをもっているので、トランジスタを1つ割愛はできるので、トータルではフットプリントや部品点数もあまり変わらない。

【作製ステップ1】
コントローラー回路

現時点では、電源をバッテリーから取るという回路ではなく、USBから給電しているのであしからず。D1,D2,D5を用いているが、これは任意。スケッチの方も用いるピンに合わせて変更する。


ArduinoIDEスケッチ(コントローラー側)
#include <ESP8266WiFi.h>  // WiFih>
#include <WiFiUdp.h>
#include "ESP8266TimerInterrupt.h"

const char ssid[] = "esp8266";   // SSID
const char pass[] = "12345678";  // password

static WiFiUDP wifiUdp;  //create UDP instance

// IP address to send UDP data to.
// it can be ip address of the server or
static const char *kRemoteIpadr = "192.168.4.1";
static const int kRmoteUdpPort = 5000;

#define PS_PIN 4    //D1
#define DAT_PIN 14  //D5
#define CLK_PIN 5   //D2
ESP8266Timer ITimer;

int status = 0b0;

void timertask() {
  String D_DATA;

  digitalWrite(PS_PIN, HIGH);
  delayMicroseconds(12);
  digitalWrite(PS_PIN, LOW);
  delayMicroseconds(6);
  for (int i = 0; i < 16; i++) {
    digitalWrite(CLK_PIN, LOW);
    if (digitalRead(DAT_PIN) == LOW) {
      bitWrite(status, 15 - i, 0);
    } else {
      bitWrite(status, 15 - i, 1);
    };
    delayMicroseconds(6);
    digitalWrite(CLK_PIN, HIGH);
    delayMicroseconds(6);
  }
  D_DATA = String(status);
  wifiUdp.beginPacket(kRemoteIpadr, kRmoteUdpPort);
  //wifiUdp.write(String(val,BIN), 16);
  wifiUdp.print(D_DATA);
  wifiUdp.endPacket();
  Serial.println(status, BIN);
}

void setup() {
  Serial.begin(115200);
  Serial.println("");                  // to separate line
  static const int kLocalPort = 5000;  //local port
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);  //Connect to the WiFi network
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  Serial.print("IP address:");
  Serial.println(WiFi.localIP());
  wifiUdp.begin(kLocalPort);

  pinMode(PS_PIN, OUTPUT);
  pinMode(CLK_PIN, OUTPUT);
  pinMode(DAT_PIN, INPUT);
  digitalWrite(PS_PIN, LOW);    //normal state of PS pin
  digitalWrite(CLK_PIN, HIGH);  //nomal state of CLK pin

  ITimer.attachInterruptInterval(16666, timertask);
}

void loop() {

}


タイマーを使って定期的に、純正のコントローラーに対して、PS(処理の開始の合図)とクロックを送信、純正コントローラー内のICから返ってくるボタンの押されている状態を16ビットstatusに格納。その16ビットのstatusの変数をWiFi-UDPで送信する。


【作製ステップ2】
受信回路

5Vの電源はSFC本体から取得。ESPに搭載されているレギュレーター3.3Vを得てCH32に供給。
(この点、使っているSFC自体、電源回りを改造しているので本当の純正の場合電力が足りるかは未検証です)
ESPとCH32はSPIで接続。SFCからのPSをPC1、DATの出力をPC2から。上の図はPC1(11番ピン)にレベルコンバーターを介して接続しているが、11番ピンは5Vトレラントなので、直接入力してもOK。(5Vトレラントは、入力は5Vでも問題ないが、5Vを出力してくれるものではない)
P/Sにパスコンを付けているが、これなくても大丈夫かもしれないが、テスト時はない場合CH32でパルス検出する際、ノイズでも立ち上がり検出してしまって不要な所でDATを吐き出す事があったので接続。このパスコン搭載後はそのような挙動はなくなっている。

MountRiver
#include "debug.h"
#include "string.h"

/* SPI Mode Definition */
#define HOST_MODE   0
#define SLAVE_MODE   1

/* SPI Communication Mode Selection */
//#define SPI_MODE   HOST_MODE
#define SPI_MODE   HOST_MODE

/* Global define */
#define Size 32


/* Global Variable */

uint8_t rxBuf[34];
uint16_t rxvalue;


uint8_t readBits(uint16_t value, uint8_t bit_position) {
  return (value >> bit_position) & 1; // Returns 1 if the specified bit is 1, otherwise returns 0.
}

void Delay_Ns(uint32_t ns) {
    for(int i=0;i<ns;i++) {
        asm("NOP"); // Insert an empty command and wait
    }
}

void GPIO_INIT(void)
{
    GPIO_InitTypeDef  GPIO_InitStructure={0};

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
}
void EXTI0_INT_INIT(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};
    EXTI_InitTypeDef EXTI_InitStructure = {0};
    NVIC_InitTypeDef NVIC_InitStructure = {0};

    /* GPIO PC1 setting */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOD, &GPIO_InitStructure);

    /*EXTernal Interrupt(EXTI)
    /* GPIOD ----> EXTI_Line1 */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource1);
    EXTI_InitStructure.EXTI_Line = EXTI_Line1;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//EXTI_Trigger_Falling EXTI_Trigger_Rising
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    /* NVIC*/
    NVIC_InitStructure.NVIC_IRQChannel = EXTI7_0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}
void EXTI7_0_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void EXTI7_0_IRQHandler(void)
{
    EXTI_ClearFlag(EXTI_Line1); // rest exti
    for(int i=0; i<16; i++){
       // uint16_t test=0b0101010101010000;

        GPIO_WriteBit(GPIOC, GPIO_Pin_2, readBits(rxvalue, 15-i));

        if (i==0){
            Delay_Us(18);
            Delay_Ns(4);
        }else{
            Delay_Us(8);
            Delay_Ns(1);
        }
    }

       // GPIO_WriteBit(GPIOC, GPIO_Pin_0, readBits(rxvalue, 14));

        GPIO_WriteBit(GPIOC, GPIO_Pin_2, 0);


}
/*********************************************************************
 * @fn      SPI_FullDuplex_Init
 *
 * @brief   Configuring the SPI for full-duplex communication.
 *
 * @return  none
 */
void SPI_FullDuplex_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure={0};
    SPI_InitTypeDef SPI_InitStructure={0};

    RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC | RCC_APB2Periph_SPI1, ENABLE );

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

#if (SPI_MODE == HOST_MODE)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

#elif (SPI_MODE == SLAVE_MODE)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_30MHz;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init( GPIOC, &GPIO_InitStructure );

#endif

    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//(SPI_DIRECTION_2LINES_RXONLY //SPI_Direction_2Lines_FullDuplex

#if (SPI_MODE == HOST_MODE)
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;

#elif (SPI_MODE == SLAVE_MODE)
    SPI_InitStructure.SPI_Mode = SPI_Mode_Slave;

#endif

    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;// SPI_CPOL_Low SPI_CPOL_High
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//SPI_FirstBit_MSB
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init( SPI1, &SPI_InitStructure );

    SPI_Cmd( SPI1, ENABLE );
}

void SPISendReceiveBytes(uint8_t *sendData, uint8_t *getData, uint32_t length)
{
    for(int i = 0; i  < length; i ++)
    {
        //Send SPI Byte
        while( SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_TXE ) == RESET ); // wait while flag is zero or TX buffer not empty
        SPI_I2S_SendData( SPI1, sendData[i] );

        //Receive SPI Byte
        while(SPI_I2S_GetFlagStatus( SPI1, SPI_I2S_FLAG_RXNE ) == RESET ); // wait while flag is zero or RX buffer is empty
        getData[i] = SPI_I2S_ReceiveData( SPI1 );

    }
}
void printBinary(int num) {
    for (int i = 15; i >= 0; i--) {
        printf("%d", (num >> i) & 1);
    }
}



/*********************************************************************
 * @fn      main
 *
 * @brief   Main program.
 *
 * @return  none
 */
int main(void)
{
    SystemCoreClockUpdate();
    Delay_Init();

    USART_Printf_Init(115200);//460800
    printf("SystemClk:%d\r\n",SystemCoreClock);
    printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );

    GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_RESET);
    GPIO_ResetBits(GPIOC, GPIO_Pin_4); // CS LOW
    GPIO_ResetBits(GPIOC, GPIO_Pin_2); //  LOW
#if (SPI_MODE == SLAVE_MODE)
    printf("Slave Mode\r\n");
    Delay_Ms(1000);

#endif

    SPI_FullDuplex_Init();

#if (SPI_MODE == HOST_MODE)
    printf("Host Mode\r\n");
    Delay_Ms(2000);

#endif
    GPIO_INIT();
    EXTI0_INT_INIT();

    uint8_t txBuf[34];


    for(int i = 0; i < 34; i++){
        txBuf[i] = 0x0;
        rxBuf[i] = 0x0;
    }
    txBuf[0] = 0x03;

    while(1)
    {
        GPIO_SetBits(GPIOC, GPIO_Pin_4); // CS HIGH
        Delay_Us(5);
        GPIO_ResetBits(GPIOC, GPIO_Pin_4); // CS LOW
        SPISendReceiveBytes(txBuf,rxBuf,4);
        GPIO_SetBits(GPIOC, GPIO_Pin_4); // CS HIGH
        Delay_Us(5);
        GPIO_ResetBits(GPIOC, GPIO_Pin_4); // CS LOW

        rxvalue=(rxBuf[2] << 8) | rxBuf[3];

        printBinary(rxvalue);
        printf("\r\n");
        //}
        Delay_Ms(10);

    }
}

【ArduinoIDEスケッチ(SFC側)】
#include <ESP8266WiFi.h>  // WiFi
#include <WiFiUDP.h>      // UDP
#include "SPISlave.h"

#define DAT_OUTPIN_BIT (1 << 4)

uint16_t button_status;

// SSIDとパスワード
const char *ssid = "esp8266";
const char *password = "12345678";
//create UDP instance
static WiFiUDP udp;

#define LOCAL_PORT 5000   // port number for local
#define REMOTE_PORT 5000  // port number for remote
// IPアドレス
IPAddress localIP;   //local IP address
IPAddress remoteIP;  // remote address

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  pinMode(LED_BUILTIN, OUTPUT);


  button_status = 0b1010101010101111;

  // AP setting
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);
  delay(100);
  localIP = WiFi.softAPIP();
  Serial.println();
  Serial.print("AP IP address: ");
  Serial.println(localIP);

  // start udp
  udp.begin(LOCAL_PORT);
  delay(100);


  SPISlave.onDataSent([]() {
    uint8_t data_to_send[2] ;//= { Counter, 10 };
    data_to_send[0]=button_status>>8;
    data_to_send[1]=button_status & 0x00FF;
    SPISlave.setData(data_to_send, 2);
  });

  SPISlave.begin();
}
void loop() {
  char packetBuffer[16];
  int packetSize = udp.parsePacket();
  if (packetSize) {
    int len = udp.read(packetBuffer, packetSize);
    if (len > 0) packetBuffer[len] = '\0';
    button_status = atoi(packetBuffer);
  }
  Serial.println(button_status,BIN);
}
【動作】
以前( https://funasover.blogspot.com/2025/04/blog-post.html)のように、ボタンを押してもないのにドンキーがジャンプするってこともなくなっていて、普通には遊べる。
特に遅延は感じる事はない。

コントローラーを無線化しようと始めたプロジェクトであるが、いったんマイルストーンは達成と言ってよいかな。
次は
2コンへの対応
コントローラー内への2次電池と充電回路
USBCから充電しながらも遊べるとかの回路
Li電池からだとSFCコントローラー内のICが5V動作なので昇圧
BLE化(低消費電力化)←ESPのWifiの電力は大きいのはわかっているが、電池側の容量をあげてまずは対応。

【追記】
・2コンへの対応


2コンへの対応に向けて、SFC本体から送られてくるP/Sのタイミングを見てみたが、1コン、2コンともに、同じタイミング。
同じようにクロックも確認したが同じタイミングで入ってくる。

一つのICからの出力を分けているだけと思われる。(中身の回路を追った訳ではない)
DATだけを2系統用意すればよくて、その他は1コンの信号を基にすれば十分という事はわかった。


コメント

このブログの人気の投稿

Attiny85とAQM0802A(LCD)のI2C接続

CH9329で日本語キーボード109で正しく表示する方法