【遊戯王真DM】チートリモコンを作ってみた~ソフトウェア編③:チャタリング対策~【封印されし記憶】

遊戯王真デュエルモンスターズ~封印されし記憶~のポケットステーションで強力なカードを入手するためのツール:チートリモコンを製作した。
今回はチートリモコンのソフトウェア設計③:チャタリング対策を行う。



<チートリモコン作ってみたシリーズ>
封印されし記憶を語る
概要編
ハードウェア編
ソフトウェア編①:動作確認
ソフトウェア編②:キー操作 
ソフトウェア編③:チャタリング対策 ←いまここ
ソフトウェア編④: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);
  }
}