遊戯王真デュエルモンスターズ~封印されし記憶~のポケットステーションで強力なカードを入手するためのツール:チートリモコンを製作した。
今回はチートリモコンのソフトウェア設計③:チャタリング対策を行う。
<チートリモコン作ってみたシリーズ>
・封印されし記憶を語る
・概要編
・ハードウェア編
・ソフトウェア編①:動作確認
・ソフトウェア編②:キー操作
・ソフトウェア編③:チャタリング対策 ←いまここ
・ソフトウェア編④:NECフォーマット
・ソースコード一覧
・入手カード一覧
以下の内容は、ある程度PICやC言語を知っている前提となります。
何をしているのか分からない部分があったら、参考書やwebのPIC入門などを参照してください。
そもそもチャタリングとは、物理スイッチを押したり離したりする際に、内部のバネなどによりON/OFF状態が細かく変化する物理現象である。
対策方法としてはハードウェアによる方法とソフトウェアによる方法があるが、チートリモコンではスペースと見た目の関係でハードウェア対策を行っていない。
ちなみに、ハードウェアの対策はRCのローパスフィルタやシュミットトリガなどがあり、ローパスフィルタだけでも十分な効果がある。
今回、チャタリングが実際どのようになっているかを確認するため、アマゾンで激安中華USBロジアナを入手した。
今時USB miniBという怪しいものだったが、実動作は問題なかったので、まずはこれでチャタリングを測定してみる。
これが測定中の写真。
PICerFTもUSB miniBのため、ケーブル箱に封印していたUSB miniBケーブルを引っ張り出した。
ちなみに、この途中急に焦げ臭くなりチェックしたところ、電池ケースのケーブルが断線してショートしてた。
電池ケースのケーブルって何であんな切れやすいんだろう?
RESETスイッチを押下した際のロジアナ波形は以下のような感じ。
以前説明した通り、1(緑線)がOFF状態で、0(赤線)がON状態となっている。
OFF→ON部分を拡大したものはこちら。
ON→OFF部分を拡大したものはこちら。
見比べてみると、OFF→ONよりもON→OFFの方がチャタリング時間がはるかに長い。
これは、OFF→ONは物理的に押すため、押さえつけ効果で振動が発生しにくく、ON→OFFは離すことにより、振動が発生しやすかったと思われる。
では、チャタリング対策をプログラムに組み込んでみよう。
前回のRESETスイッチの関数は以下の通り。
int NecDataRst(unsigned char *custom, unsigned char *data){ // 値の変更 *custom = 0; *data = 0; // スイッチがOFFになるまで待ち while(RST_SW == 0); // 終了処理 return 3; }
まぁ値をリセットするだけなのでチャタリング対策は不要だが、エンコーダーのスイッチも同様の処理をするので、RESETスイッチを例にする。
チャタリング対策で主に行うのは、関数の最初でチャタリングによる誤動作でないか確認することと、関数の最後で確実にOFF状態にすること。
今回はこのようにしてみた。
// チャタリング対策時間の定義 #define chata_delay_ms 10 int NecDataRst(unsigned char *custom, unsigned char *data){ // チャタリング対策 __delay_ms(chata_sw_delay_ms); if(RST_SW == 1) return 0; // 値の変更 *custom = 0; *data = 0; // スイッチがOFFになるまで待ち while(RST_SW == 0); __delay_ms(chata_sw_delay_ms); // 終了処理 return 3; }
最初に10ms待ち、再びスイッチの状態確認を行い、OFF状態であれば誤作動と判断して、関数を即終了させている。
値の変更とスイッチがOFFになるまでの待ちは同じだが、その後10ms追加で待つことにより、確実にOFF状態になるようにした。
数値を直接入力してもいいが、エンコーダーのスイッチと共通で使用するため、値を変える際は6ヶ所変更する必要がある。
そのため、#define chata_delay_msで文字列に置き換えをした。
これをエンコーダーのスイッチにも行ったところ、チャタリング対策前は桁が変わらないことがあったが、確実に変わるようになった。
次が本命である、エンコーダーのチャタリング対策。
エンコーダーの場合、チャタリングだけでなく回転速度も考慮する必要がある。
という訳で、低速、中速、高速回転をそれぞれ測定してみた。
これが低速。
これが中速。
これが高速。
このように、回転速度によってON/OFF期間が大幅に異なる。
ちなみに、チャタリングはこんな感じ。
ディレイ時間は5~10msぐらいにしたいが、高速回転だと取りこぼす可能性がありそう。
こちらはCUSTOMエンコーダーの時計回りの関数で、これを例にしてみる。
int NecCustCw(int cursor, unsigned char *custom){ // 値の変更 switch(cursor){ case 0 : *custom = *custom + 0x01; break; case 1 : *custom = *custom + 0x10; break; defoult : *custom = *custom + 0x01; break; } // スイッチがOFFになるまで待ち while(CUST_A == 0 || CUST_B == 0); // 終了処理 return 1; }
まずはディレイ式のチャタリング対策をしてみる。
// チャタリング対策時間の定義 #define chata_start_delay_ms 3 #define chata_end_delay_ms 3 int NecCustCw(int cursor, unsigned char *custom){ // チャタリング対策 __delay_ms(chata_start_delay_ms); if(CUST_A == 1) return 0; // 値の変更 switch(cursor){ case 0 : *custom = *custom + 0x01; break; case 1 : *custom = *custom + 0x10; break; defoult : *custom = *custom + 0x01; break; } // スイッチがOFFになるまで待ち while(CUST_A == 0 || CUST_B == 0); __delay_ms(chata_end_delay_ms); // 終了処理 return 1; }
回転速度次第では誤検出を誤検出(ダジャレ)してしまうので、チャタリング対策の待ち時間はギリギリまで短くしてみた。
上記の通り、回転速度次第では誤検出や回転を拾えない可能性がある。
より精度を上げるため、プランBであるカウンタ式も設計してみた。
// チャタリング対策カウンタの定義 #define chata_start_cnt_max 10 #define chata_end_cnt_max 30 int NecCustCw(int cursor, unsigned char *custom){ unsigned int chata_start_cnt = 0; unsigned int chata_end_cnt = 0; unsigned int timeout_cnt = 0; // チャタリング対策 while(chata_start_cnt < chata_start_cnt_max){ if(CUST_A == 0 && CUST_B == 0){ chata_start_cnt++; } else{ chata_start_cnt = 0; } if(timeout_cnt < 65535){ timeout_cnt++; } else{ return 0; } } // 値の変更 switch(cursor){ case 0 : *custom = *custom + 0x01; break; case 1 : *custom = *custom + 0x10; break; defoult : *custom = *custom + 0x01; break; } // スイッチがOFFになるまで待ち while(chata_end_cnt < chata_end_cnt_max){ if(CUST_A == 1 && CUST_B == 1){ chata_end_cnt++; } else{ chata_end_cnt = 0; } } // 終了処理 return 1; }
前2つと比べて長くなったが、やっていることはけっこう単純。
カウンタを用意し、それが一定回数以上連続で狙い通りの状態になっていたらwhileループを抜け出し、次に進むという方法となっている。
今回の場合、誤作動の検出は10回連続でONならループを抜け、OFFになるまでの待ちは30回連読OFFになっていればループを抜けるようにした。
また、誤作動の検出時にはすでにOFFになっていた場合、ループを抜け出せなくなる恐れがあるため、タイムアウト検出用のカウンタを別に用意し、強制終了できるようにした。
ちなみに、クロック周波数は40MHz、PICマイコンの8ビット品は4クロックで1サイクルのため0.1us単位で動作するのだが、IF文やカウントアップ等の処理に数サイクル必要なため、思ったより時間を要する。
仕事でFPGA設計している者としては1クロックで全部処理してほしいが、CPUのクセも理解しないとね。
何度かカウンタの最大値を変更し、精度を高めていこう。
3パターンについて精度を確認してみた。
以下の動画では、時計回り→反時計回りを各12クリック分、中速→高速の順に行っている。
まずはチャタリング対策なしの結果。
中速では0x00→0x13(+19)→0x03(-16)、高速では0x00→0x17(+23)→0x07(-16)と変化しており、本来なら+12→-12となるはずなので、大幅にずれている。
次に、ディレイ式チャタリング対策の結果。
中速では0x00→0x0D(+13)→0x01(-12)、高速では0x00→0x0F(+12)→0xFF(-13)と変化している。
±1程度ではあるがズレる頻度が高く、地味に使いづらいのでこれも実用的ではない。
本命のカウンタ式チャタリング対策の結果。
中速、高速共に±12であり、精度はだいぶ上がった。
何度もやると時々±1程度ズレることはあったが、十分実用的になった。
次回はNECフォーマット編で、ようやく完成の予定。
週1程度しか更新できず、予告編から2ヶ月近くも待たせてしまって申し訳ない。
最後に、チャタリング対策をしたkeys.cを置きます。
他は前回の記事を参照してください。
keys.c
#include "xc.h" #define RST_SW PORTAbits.RA0 #define CUST_A PORTAbits.RA1 #define CUST_B PORTAbits.RA2 #define CUST_SW PORTAbits.RA3 #define DATA_A PORTCbits.RC0 #define DATA_B PORTCbits.RC1 #define DATA_SW PORTCbits.RC2 #define SEND_SW PORTCbits.RC3 #define chata_sw_delay_ms 10 #define chata_start_cnt_max 10 #define chata_end_cnt_max 30 #define _XTAL_FREQ 40000000 int NecDataRst(unsigned char *custom, unsigned char *data){ // チャタリング対策 __delay_ms(chata_sw_delay_ms); if(RST_SW == 1) return 0; // 値の変更 *custom = 0; *data = 0; // スイッチがOFFになるまで待ち while(RST_SW == 0); __delay_ms(chata_sw_delay_ms); // 終了処理 return 3; } int NecCustCw(int cursor, unsigned char *custom){ unsigned int chata_start_cnt = 0; unsigned int chata_end_cnt = 0; unsigned int timeout_cnt = 0; // チャタリング対策 while(chata_start_cnt < chata_start_cnt_max){ if(CUST_A == 0 && CUST_B == 0){ chata_start_cnt++; } else{ chata_start_cnt = 0; } if(timeout_cnt < 65535){ timeout_cnt++; } else{ return 0; } } // 値の変更 switch(cursor){ case 0 : *custom = *custom + 0x01; break; case 1 : *custom = *custom + 0x10; break; defoult : *custom = *custom + 0x01; break; } // スイッチがOFFになるまで待ち while(chata_end_cnt < chata_end_cnt_max){ if(CUST_A == 1 && CUST_B == 1){ chata_end_cnt++; } else{ chata_end_cnt = 0; } } // 終了処理 return 1; } int NecCustCcw(int cursor, unsigned char *custom){ unsigned int chata_start_cnt = 0; unsigned int chata_end_cnt = 0; unsigned int timeout_cnt = 0; // チャタリング対策 while(chata_start_cnt < chata_start_cnt_max){ if(CUST_A == 0 && CUST_B == 0){ chata_start_cnt++; } else{ chata_start_cnt = 0; } if(timeout_cnt < 65535){ timeout_cnt++; } else{ return 0; } } // 値の変更 switch(cursor){ case 0 : *custom = *custom - 0x01; break; case 1 : *custom = *custom - 0x10; break; defoult : *custom = *custom - 0x01; break; } // スイッチがOFFになるまで待ち while(chata_end_cnt < chata_end_cnt_max){ if(CUST_A == 1 && CUST_B == 1){ chata_end_cnt++; } else{ chata_end_cnt = 0; } } // 終了処理 return 1; } void NecCustSw(int *cursor){ // チャタリング対策 __delay_ms(chata_sw_delay_ms); if(CUST_SW == 1) return; // 値の変更 *cursor = *cursor + 1; if(*cursor >= 2) *cursor = 0; // スイッチがOFFになるまで待ち while(CUST_SW == 0); __delay_ms(chata_sw_delay_ms); } int NecDataCw(int cursor, unsigned char *data){ unsigned int chata_start_cnt = 0; unsigned int chata_end_cnt = 0; unsigned int timeout_cnt = 0; // チャタリング対策 while(chata_start_cnt < chata_start_cnt_max){ if(DATA_A == 0 && DATA_B == 0){ chata_start_cnt++; } else{ chata_start_cnt = 0; } if(timeout_cnt < 65535){ timeout_cnt++; } else{ return 0; } } // 値の変更 switch(cursor){ case 0 : *data = *data + 0x01; break; case 1 : *data = *data + 0x10; break; defoult : *data = *data + 0x01; break; } // スイッチがOFFになるまで待ち while(chata_end_cnt < chata_end_cnt_max){ if(DATA_A == 1 && DATA_B == 1){ chata_end_cnt++; } else{ chata_end_cnt = 0; } } // 終了処理 return 2; } int NecDataCcw(int cursor, unsigned char *data){ unsigned int chata_start_cnt = 0; unsigned int chata_end_cnt = 0; unsigned int timeout_cnt = 0; // チャタリング対策 while(chata_start_cnt < chata_start_cnt_max){ if(DATA_A == 0 && DATA_B == 0){ chata_start_cnt++; } else{ chata_start_cnt = 0; } if(timeout_cnt < 65535){ timeout_cnt++; } else{ return 0; } } // 値の変更 switch(cursor){ case 0 : *data = *data - 0x01; break; case 1 : *data = *data - 0x10; break; defoult : *data = *data - 0x01; break; } // スイッチがOFFになるまで待ち while(chata_end_cnt < chata_end_cnt_max){ if(DATA_A == 1 && DATA_B == 1){ chata_end_cnt++; } else{ chata_end_cnt = 0; } } // 終了処理 return 2; } void NecDataSw(int *cursor){ // チャタリング対策 __delay_ms(chata_sw_delay_ms); if(DATA_SW == 1) return; // 値の変更 *cursor = *cursor + 1; if(*cursor >= 2) *cursor = 0; // スイッチがOFFになるまで待ち while(DATA_SW == 0); __delay_ms(chata_sw_delay_ms); } void NecDataSend(unsigned char custom, unsigned char data){ while(SEND_SW == 0){ // NecSend(custom, data); __delay_ms(40); } }