スーパーファミコン コントローラーの無線化プロジェクト:動作バージョン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】
コントローラー回路
#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】
受信回路
(この点、使っている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コンへの対応
一つのICからの出力を分けているだけと思われる。(中身の回路を追った訳ではない)
DATだけを2系統用意すればよくて、その他は1コンの信号を基にすれば十分という事はわかった。
コメント
コメントを投稿