はじめに
@H0meMadeGarbage さんの「マリオカートライブを自作」というサイトを見て、自分も作ってみたくて始めました。Pythonを学び始めたばかりだったので、過去のブログ等を読みながら作成しました。
スーパーファミコン(以下「SNES」という。)のコントローラーの改造まで完成したので記事にまとめました。良かったら読んでください。
(記述内容に誤りがありましたらご指摘いただけると幸いです。)
改造に当たり考えたこと
当初は、スマホでマリオカートを操作することを考えましたが、私自身がスーパーファミコン世代なので、SNESコントローラーで作ることを決めました。
ラズパイPicoしか使ったことがなかったので、同系のPicoWで作成しようと思いましたが、Bluetoothが使えて、コンパクトで、電源管理チップが組み込まれているXIAOのEsp32c3を見つけ、これで作ることを決めました。
付加機能としては、Bluetooth接続をすることから、「電源切」、「Bluetooth接続中」、「Bluetooth接続完了」が分かる機能と、「バッテリー充電が必要になったこと」が分かる機能を付けることにしました。
SNESコントローラーの仕組み
SNESコントローラーの仕組みを理解するため、@odannyさんの記事を主に参考にさせてもらいました。
配線
SNESは本体とコントローラーを、1本のケーブルで接続しています。ケーブルの中身は7本の配線で構成されており、その内容は次の表の通りです。
Pin | Descprition | Cable color |
---|---|---|
1 | +5V | White |
2 | Data Clock | Yellow |
3 | Data Latch | Orange |
4 | Serial Data | Red |
5 | no wire | |
6 | no wire | |
7 | Ground | Brown |
電源関連の2本の配線の他、Data Latch、Data Clock、Serial Data の3本の配線でコントローラーの信号を制御しています。
通信プロトコル
通信プロトコルとはネットワークにつながる機器同士がデータをやりとりするための約束事です。
SNESのコントローラーと本体はどのような約束事で通信をしているのでしょうか。
概要図は下の図のとおりです。
ファミコン、スーパーファミコンのゲーム コントローラの信号線の通信仕様のまとめ
Latch、Clock 及び Serial Data について説明します。
Latch
Latch とは今からデータ送信を始めるよという本体からの合図の信号です。
60分の1秒ごとに電圧を 12us だけ Low から High にします。
Clock
Clockは、通常 High の状態です。Latch が High から Low に下がった 6us 後、Low へ下がり、その 6us 後に Low から High に上がるように、6us ごとに16回繰り返されます。この Clock の周期に合わせて各ボタンの情報を Serial Data に反映します。
具体的には Clock が High になったらコントローラー側がボタンの状態をSerial Dataに反映し、Low になった時、本体側で判定します。
16回繰り返したあとは 次のフレームまで High のままとなります。
Serial Data
コントローラの各ボタンには、そのボタンの状態が報告されるClock Cycle に対応する特定の ID が割り当てられます。
下の表には、すべてのボタンの ID がリストされています。ボタンが押された場合、Serial Data が Low になります。複数のボタンが同時に押されていても、それぞれの状態を把握できます。
Serial Data が High の場合、ボタンは押されていないことを意味し、Clock Cycle が13から16の間は High のままになります。
Clock Cycle(ID) | Button Reported |
---|---|
1 | B |
2 | Y |
3 | Select |
4 | Start |
5 | Up on joypad |
6 | Down on joypad |
7 | Left on joypad |
8 | Right on joypad |
9 | A |
10 | X |
11 | L |
12 | R |
13 | none |
14 | none |
15 | none |
16 | none |
以上のことから、SNESコントローラーから Bluetooth で信号を送信するためには、コントローラーに取り付けた Esp32c3 で Data Latch と Data Clock の信号をコントローラーのICに入力し、出力される Serial Data を Bluetooth で送信できればいいことが分かりました。
配線図
電源
電源は、アリエクで購入したリチウムポリマー電池802030 V,3.7 を使用しています。
トグルスイッチを経由させEsp32c3の裏側にあるBAT端子に接続しています。また、トグルスイッチから、DC/DC昇圧コンバータを使用して5Vに増幅し、SNSEコントローラーの電源端子(white)に接続しています。
ケーブルの配線
Clock(Yellow)、Latch(Orange)及びData(Red)ケーブルは、Esp32c3のGPIO3,4,5に接続し、電源用青LEDケーブルをGPIO8、バッテリーの電圧状況を示す赤LEDはGPIO9に接続しています。
リチウムポリマー電池とEsp32c3の接続はこちらの記事を参考にしています。
プログラム構成
.
├── ble_advertising.py # Bluetoothの接続用
│
├── ble_simple_peripheral.py # Bluetoothの接続用
│
├── main.py
Bluetoothの接続
Bluetoothの接続は、micropythonリポジトリのBluetoothサンプルからダウンロードして使用しています。
ファイル名 | 動作 |
---|---|
ble_advertising.py | アドバタイズなどのユーティリティ・モジュール、デモとして動作する場合には、アドバタイズ情報を表示する。 |
ble_simple_peripheral.py | UARTサービスを実行するペリフェラルとして動作する。セントラルに接続後、0.1秒ごとにデータを送信し続ける一方、セントラルからの情報を受信するごとにローカル・コンソールに表示する。 |
main.py
Bluetooth接続状況の表示
def control_LED_main()
で、Bluetooth接続中は青色LEDが点滅するようにしています。
Serial Dataの出力
read_controller( )
で、SNESコントローラーの仕組みを踏まえてLatch と Clock の電圧をコントローラーのICに入力し、出力されたSerial Data の値を button_states
に代入します。
バッテリー低下時の表示
check_battery()
で、電圧をADC(Pin(2)) から読み取って換算して3.4V 未満になっていないかを確認しています。
Adafruitのサイトの説明によると、
たとえば、これは「クラシック」な3.7V/4.2Vバッテリーの電圧のプロファイルです。電圧は最大4.2から始まり、バッテリ寿命の大部分で約3.7Vまで急速に低下します。3.4Vに達するとバッテリーは切れ、3.0Vではカットオフ回路がバッテリーを切断します。
とあることから、確認する電圧の基準を3.4Vにしました。そして電圧が3.4V未満になったら赤LED を点滅するようにしました。
その他
当初、ボタン名を送信しようとしましたが、文字列のまま送信するとすべてのボタンの状態を送れなかったため、途中から各ボタンのオン・オフを表す16桁の2進数の文字列を送るように修正したので、その名残が残っています。
from machine import Pin, ADC, reset
from ble_simple_peripheral import BLESimplePeripheral
import time
import bluetooth
# Create a Bluetooth Low Energy (BLE) object
ble = bluetooth.BLE()
# Create an instance of the BLESimplePeripheral class with the BLE object
sp = None # spを初期化しますが、まだBLESimplePeripheralのインスタンスは作成しません
# Set the debounce time to 0. Used for switch debouncing
debounce_time = 0
# スーパーファミコンのコントローラーのピン設定
CLOCK = Pin(3, Pin.OUT)
LATCH = Pin(4, Pin.OUT)
DATA = Pin(5, Pin.IN)
# LEDのピン設定
LED_main = Pin(9, Pin.OUT) # 電源等表示
LED_battery = Pin(8, Pin.OUT) # 電源等表示
# 電池の電圧を読み取るためのピンを設定します。
battery = ADC(Pin(2))
battery.atten(ADC.ATTN_11DB) # 電圧範囲を最大3.6Vに設定します。
# ボタンの名前
button_names = ["B", "Y", "SELECT", "START", "UP", "DOWN",
"LEFT", "RIGHT", "A", "X", "L", "R"]
# コントローラーからのデータを読み取る関数
def read_controller():
# ボタンの状態を保存するためのリスト
button_states = []
# LATCHとCLOCKを上げてデータをリセット
LATCH.value(1)
CLOCK.value(1)
time.sleep_us(12)
LATCH.value(0)
# 各ボタンの状態を読み取る
for _ in range(16):
CLOCK.value(0)
time.sleep_us(6)
button_states.append(DATA.value())
CLOCK.value(1)
time.sleep_us(6)
return button_states
def check_battery():
global battery_timer
# リチウムイオンポリマー電池の電圧を読み取ります。
Vbatt = 0
for i in range(16):
# 校正された入力電圧(減衰前)をマイクロボルト単位で返す。
# ただし返される値はミリボルトの分解
Vbatt = Vbatt + battery.read_uv()
# attenuation ratio 1/2, uV --> V
voltage = 2 * Vbatt / 16.0 / 1000000.0
# 電圧に基づいてLEDの状態を制御します。
# 電圧が3.4V未満の場合、LEDを点滅させます。
# (3.4V未満頃から接続が弱くなるため)
if voltage < 3.4:
LED_battery.value(not LED_battery.value())
# 電圧が3.3V以上の場合、LEDを消灯させます。
else:
LED_battery.off()
# voltageを文字列に変換 /nは改行
msg = "battery_v: " + str(voltage) + "\n"
# 30秒ごとにバッテリー電圧をBluetoothで送信
if time.ticks_ms() - battery_timer > 30000:
# voltageを文字列に変換 /nは改行
msg = "battery_v: " + str(voltage) + "\n"
battery_timer = time.ticks_ms()
print(str(voltage) + "V") # エラーチェック用
# LED_mainの状態を制御する関数
def control_LED_main():
# Bluetoothに接続中の時はLED_main点滅
if ble.active() and not sp.is_connected():
LED_main.value(not LED_main.value())
time.sleep(0.5)
else:
# Bluetoothに接続出来たら再びLED_mainを点灯
LED_main.on()
# 電源が入ったらLED_mainを点灯
LED_main.on()
# データを定期的に送信するためのタイマー
data_timer = 0
# バッテリー電圧をチェックし、Bluetoothで送信するためのタイマー
battery_timer = 0
while True:
# コントローラーからデータを読み取る
data = read_controller()
# データに基づいて各ボタンの状態を表示する
for i in range(len(button_names)):
# デバウンスの遅延時間の設定 各ボタンの最後に押された時間との差が300ms以上
# ある場合のみ判定
if (data[i] == 0 and (time.ticks_ms() - debounce_time) > 100):
print(button_names[i])
# スタートボタンが押されたらBluetooth接続を開始
if button_names[i] == "START":
# まだBLESimplePeripheralのインスタンスが作成されていない場合
if sp is None:
# BLESimplePeripheralのインスタンスを作成してBluetooth接続を開始
sp = BLESimplePeripheral(ble)
# すでにBLESimplePeripheralのインスタンスが作成されている場合
else:
# Bluetooth接続がアクティブでない場合
if not ble.active():
ble.active(True) # Bluetooth接続をアクティブにする
sp._advertise() # アドバタイジングを開始
# セレクトボタンが押されたらリセット
if button_names[i] == "SELECT":
reset()
# Check if the BLE connection is established
# BLESimplePeripheralのインスタンスが作成されていて、かつ接続が確立している場合
if sp is not None and sp.is_connected():
print(button_names[i])
# Update the debounce time
debounce_time=time.ticks_ms()
# LED_mainの状態を制御
control_LED_main()
# バッテリー電圧をチェックし、Bluetoothで送信
check_battery()
# 定期的(0.1秒ごと)にデータをBluetoothで送信
if time.ticks_ms() - data_timer > 100:
if ble.active() and sp is not None and sp.is_connected():
# dataを文字列に変換して送信
send_data = "".join(str(i) for i in data)
sp.send(send_data)
data_timer = time.ticks_ms()
結びに
改善の余地はいっぱいありますが、一応作動するものとして完成したので自分でも満足しています。
車体側は、現在Esp32S3Senseで作成中です。