ハードオフに行ったら、スーパーファミコンのコントローラが100円で売ってました。黄ばんでたけど、安かったので、つい買ってしまいました。
今回は、これを使って、Arduinoで制御したいと思います。目標は、前の記事でやったLEDシート。このLEDの点灯をこんな感じで制御したい。
- SFCコントローラの制御の仕組み
- コントローラの操作ボタンを検知するプログラムのつくりかた
SFCコントローラの制御の内容
他のサイトでも内容が説明されてますが、大体のソースは下のサイトからみたいです。
コントローラから出ている線は5本で、うち2本は電源とGNDで、制御の信号に使うのは、ラッチ(オレンジ)とクロック(黄色)、そしてデータ(赤)の線になります。
ラッチとクロックはゲーム機本体からコントローラに送られる信号です。コントローラはこの二つの信号を受信して、ボタンの操作状況に合わせて、然るべきタイミングでデータ線に信号をだします。
そのデータ線の信号を本体が解読して、ボタンの状態を把握するって感じの流れになります。そして、今回はArduinoが本体の代わりにラッチとクロックをコントローラに送信して、データを受信して、ボタン判定をするって訳です。
今回はArduinoとコントローラは下のように繋げました。
実際の信号のルールなんですが、こんな感じです。説明不要かもしれませんが、念のため。不明な点があれば、上のサイトをみてみてください。結構詳しく書いてました。
中身は実際、超シンプルで、クロックがパルス信号をコントローラに出して、上の表に従って、コントローラは押されたボタンに応じて、指定のパルス番号タイミングで出力をLOWにするって、仕組みです。
ここで、少し気になったのが、ボタンの識別につかう16パルスの周期とラッチの周期があまりにも違うなーということです。ラッチの周期がクロックと同様に短すぎるとワンプッシュで連続判定してしまうため、入力を受けつけないようにしてるという訳でしょうか?理由は不明ですが、一応、クロックとラッチの信号を言われた通りにコントローラにだしてみます。
ゲットボタンの関数を下のようにつくりました。信号のパルス幅がやや狭い(6us, 大体10サイクル分ぐらい)ので、遅くて有名なdigitalWriteは使わずにレジスターを直接書き込むやり方で処理を高速化してみました。
#define PIN_LATCH 12U //uno pin number #12 - Latch
#define PIN_LATCH_SHIFT 4U //#12-PB4
#define PIN_CLOCK 13U //uno pin number #13- Clock
#define PIN_CLOCK_SHIFT 5U //#13-PB5
#define PIN_DATA 11U //uno pin number #11- Data
#define PIN_DATA_SHIFT 3U //#11-PB3
//digital の入出力の設定は省略している。別途、Setup()内で定義しとく必要あり
void GetButton(void) //latch信号とクロック信号をデジタル出力し、コントローラから信号を取得する関数
{
//latch
PORTB|=1U<<PIN_LATCH_SHIFT; //latchをHIGHにする。
//digitalWriteは使わないでレジスタを直接操作
delayMicroseconds(12); //12usec待つ
PORTB&=~(1U<<PIN_LATCH_SHIFT); //latchをLOWにする
//clock
for (int i=0;i<16;i++) //for文でクロックのパルスを16回だす
{
delayMicroseconds(6); //パルス幅は6us
PORTB&=~(1U<<PIN_CLOCK_SHIFT); //lowにする
//クロックが立ち下がったら、Data信号を取得する PB3(#11)からデジタル入力
Button[i]=(PINB&(1U<<PIN_DATA_SHIFT))>>PIN_DATA_SHIFT;
delayMicroseconds(6); //50%dutyなので、low側も6us
PORTB|=1U<<PIN_CLOCK_SHIFT; //またhighにする
}
}
このコードを実行した時のオシロの結果がこれ。上がAボタン、下がXボタン押した時のデータです。青い線がデータ線、しっかりローになるタイミングが変化していってます。
この処理を60Hzで繰り返すわけか…
タイマー関数を設定する
arduinoのライブラリでは、タイマ関数が色々用意されてますが、今回は勉強のため、どうせなら、タイマーもレジスターを直接操作して、設定したいとおもいます。
ちなみに、unoで搭載されてるコアATmega328には、8bitタイマが2つと16bitタイマが1つあります。
今回は16bitタイマであるTC1(タイマ・カウンタ1)を使います。今回の用途だと、60Hz周期で上のGetButton関数を呼びたいので、このタイマーを使って、約16.67ms毎に割り込みが発生するようにしたいです。
タイマー設定の3ステップ
- 分周比をきめる
- TOP値を設定する
- 割り込みを許可する
まず、分周比の設定です。
タイマーは通常CPUのクロック毎(unoは16MHz)にカウントアップされますが、クロックスピードがはや過ぎる場合はすぐにオーバフローしてしまうので、役に立ちません。そこで、下のように分周比を設定して、ゆっくりカウントアップするようにします。
今回は、分周比を8に設定しました。
16bitタイマだと値の上限が65535なので、8分周だと、計測できる上限は65535×(0.0625us)×(8分周) で、約32msぐらいになります。今回は16msのタイマをつくりたいので、これで十分。
次にTOP値の設定。
TOP値とは、呼びたい周期(ここでは、16.67ms)に相当するタイマのカウント数になります。下のような感じで、タイマはカウントがTOP値になると0リセットされ、再びカウントアップしていく仕組みです。
16.77ms 8分周で、実際に計算すると、TOP値は
(16.67us×1000) / ( 0.0625us × 8分周) =33340
だと、わかりました。
最後に割り込みの設定です。計算したTOP値で割り込みが発生するようにします。下の図が割り込み許可レジスターですが、比較Bか比較Aの割り込み許可を1にセットすればいいです。今回は比較Aのほうを使います。
他にもオーバーフローした時の割り込みだとか、外部信号からの割り込みだとか色々種類がありましたが、今回は用はないです。なんでBit1だけセット。
比較Aで設定する場合、それに対応するレジスタにさっき計算したTOP値をいれます。16bitタイマだと当たり前ですが、TOP値も16bitの信号がはいります。
いままで、説明をコードにすると下のような感じです。
void setup(){
//タイマ0,CTC,割り込み用、比較A一致で割り込み
TCCR1A = 0b00000000; //16bitタイマ 比較一致タイマモード
TCCR1B = 0b00001010; // N=8
OCR1AH = 0b10000010; // 60Hzごとに割り込み
OCR1AL = 0b00110101; // top value = 33333
TIMSK1 = 0b00000010; //比較A一致割り込み有効
}
ISR(TIMER1_COMPA_vect) //タイマ割り込み
{
GetButton();
}
こんだけです。
ボタンのコマンド
読みにくくて、すいません。列挙型とかつかって、可読性をあげたかったんですが、面倒になりました。Button[..]の右にコメントで何のボタン操作の時の処理かを書いておいたので、なんとなくわかるとおもいます。
setLEDとかの関数は前回記事のとおりですので、説明はコメント文だけにします。
if (Button[0]==LOW) // button B push
{
Set_Color_All(LEDdata,0,0,0); // LED Clear
setLED(LEDdata);
}
if (Button[1]==LOW) // button Y push
{
Set_Color(LEDdata,pos_led,0,255,0); //LED GREEN
setLED(LEDdata);
}
if (Button[8]==LOW) // button A push
{
Set_Color(LEDdata,pos_led,255,0,0); //LED RED
setLED(LEDdata);
}
if (Button[9]==LOW) // button X push
{
Set_Color(LEDdata,pos_led,0,0,255); //LED BLUE
setLED(LEDdata);
}
if (Button[5]==LOW) // button ↓ push
{
pos_y ++;
if (pos_y>NL-1) pos_y=NL-1;
pos_led=GetPos(pos_x,pos_y);
memcpy(temp, LEDdata, sizeof(LEDdata));
Set_Color(temp,pos_led,10,10,10);
setLED(temp);
}
if (Button[4]==LOW)
{
pos_y --;
if (pos_y<0) pos_y=0;
pos_led=GetPos(pos_x,pos_y);
memcpy(temp, LEDdata, sizeof(LEDdata));
Set_Color(temp,pos_led,10,10,10);
setLED(temp);
}
if (Button[6]==LOW)
{
pos_x ++;
if (pos_x>NW-1) pos_x=NW-1;
pos_led=GetPos(pos_x,pos_y);
memcpy(temp, LEDdata, sizeof(LEDdata));
Set_Color(temp,pos_led,10,10,10);
setLED(temp);
}
if (Button[7]==LOW)
{
pos_x --;
if (pos_x<0) pos_x=0;
pos_led=GetPos(pos_x,pos_y);
memcpy(temp, LEDdata, sizeof(LEDdata));
Set_Color(temp,pos_led,10,10,10);
setLED(temp);
}
コントローラとArduinoはこんな感じでユニバーサル基板にピンを立てて、本体と合体させました。ボード上の白いコネクタはXHコネクタです。QIコネクタと違って、つめがあるので、抜けづらいので、こんな時おすすめです。
見た目コンパクトに出来上がり、いい感じに整理されました。
実際の動作をgifにしてみました。コントローラの動きはいたって、スムーズでボタン操作は快調にうごいてます。
今回は8×8ですが、これを16×16まで拡張して、テトリスとかにしたら、面白いかも。ナイスなアイデア!とか思ってネットで探したら、既にやられてました。
ただ少し、自分の構想とは違う点もあるので、時間のある時にトライしたいとおもいます。ただ、この輝度でテトリスなんかやったら、確実に目をやられますね。超まぶしーですよこれ。
それにしても、スーファミのコントローラはかわいい。