Release 2016.8.19 / Update 2017.6.26

Arduinoのスケッチだけでスイッチのチャタリングを回避する

Arduinoを使い始めて最初にすることはLEDの点灯ですよね。その次にやるのがスイッチ操作。

“ゲージ判定”を使った、スイッチ読み取りライブラリを作ってみました。
(2017.6.26 追記)

まず簡単なスケッチ例を書きます。4ピンでオンオフの判定をして、その状態を6ピンのLEDの点灯状態に反映するシンプルなスケッチです。

void setup() {
  pinMode(6, OUTPUT);
  pinMode(4, INPUT_PULLUP);
}

void loop() {
  digitalWrite(6 !digitalRead(4));
}

4ピンのスイッチにはArduino内蔵プルアップを使うことで、回路の簡略化が出来ます(3行目INPUT_PULLUP)。その代わり論理の1/0が逆になるので、7行目のdigitalReadで得る読み取り値は「!」で反転しています。それをLEDにそのまま反映っていう感じです。

このスケッチだと、押している間LEDが点灯するだけです。そこで、「button」というグローバル変数を用意して、状態を保持し、押すたびに点灯・消灯を切り替えられるスケッチにします。

boolean button  = 0;
boolean but_old = 0;

void setup() {
  pinMode(6, OUTPUT);
  pinMode(4, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  boolean but_now = !digitalRead(4);

  if (but_now && but_old == 0)
  {
    button = !button;
    Serial.println(button);
  }

  but_old = but_now;

  digitalWrite(6, button);
}

別で変数but_nowとbut_oldを用意し、現在の4ピン読み取り値をまず、but_nowに入れます。13行目では「現在の押し判定(but_now)が1であり、かつ前回の押し判定(but_old)が0」であれば、新規に押されたと判断して、buttonステータスを反転します。そして、次の判定用にbut_oldへ今の状態(but_now)を代入。これで前回、今回が共に1だとbutton反転は行われなくなります。

狙ったようになったでしょうか?ちゃんと動いてはいるものの、たまに変な反応をするかと思います。

これがいわゆる「チャタリング」というやつです。

チャタリングとは

chattering_anim

スイッチは基本的に離れている接点を繋げることで通電させているだけの単純な仕組みです。

そして、その接点が問題です。押したり、離したりする瞬間に中のバネなどで反動が起きて、一瞬通電と無通電が交互に切り替わる状況が起きます。ボールを地面に落とすと、バウンドするのと同じです。これはミリセカンドの世界の話なんですが、上記のスケッチでは、Arduinoの処理速度がそれを上回っているので、短い変化も拾ってしまっている、と。

つまりチャタリングは機械的な仕組みには必ず起こる物理的な問題です。

よくあるチャタリングの回避方法

それでも、一般的な電子機器でこういう話は聞きません。何故ならチャタリング回避の対策を施しているからです。Web上で調べてみると、

 

  • コンデンサの蓄電効果を使った回避
  • シュミットトリガー回路を使った回避

 

こんな類の解説が出てきます。また、プログラム上で回避する方法も出てきます。

 

  • 読み取り間隔を空けて、チャタリングしている時間をスキップする
  • 読み取り値を何回分かを記憶、分析してチャタリングかどうか判断する

 

いろんな方がさまざまな方法で回避策を考えていますが、大体この2パターンに分けられると思います。あえて詳しく説明はしないので、具体的な方法は自分で調べてみてください。

スケッチだけでチャタリング現象を回避する

標準的な回避方法

前項の中で一番簡単な方法はチャタリング時間のスキップです。本題に入る前に一旦、この方法を説明します。

#define LED 6
#define SW  4
boolean button = 0;

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(SW, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {

  if (!digitalRead(SW))
  {
    button = !button;
    Serial.println(button);
  }
  digitalWrite(LED, button);
  delay(100);
}

最後にdelayを置いて一回読み取った後は時間を置きます。これでチャタリングしている間は何もしないので、誤作動は減ります。また、delayだと全体が止まるので、タイマー割り込みや、インターバルで発動するような方法にしている方もいます。

ただ、これらの方法は確実に判定しようとすればするほど、間隔が空いてしまうので、どんどん反応が鈍くなってしまいます。早い反応を求められる場合では向いてません。かといって、「多少の誤作動は良しとする」というのも納得できません。「電子回路的な回避」は計算するのが嫌だしなぁ…

なんて感じで色々悩んで試行錯誤した結果、ひとつの方法を思いつきました。

ゲージ判定方式

“ゲージ判定”を使った、スイッチ読み取りライブラリを作ってみました。
(2017.6.26 追記)

これはコンデンサを使った回避方法からヒントを得ました。

「電気が貯まったら反応するのを、プログラミング上でやったらうまくいくんじゃないか?」

そのアイディアで単純に書いてみるとこうなります。

#define LED 6
#define SW  4
boolean button = 0;

#define PUSH_SHORT 100

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(SW, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  unsigned long gauge = 0;

  while (!digitalRead(SW))
  {
    gauge++;
  }

  if (gauge > PUSH_SHORT)
  {
    button = !button;
    Serial.print(button);
    Serial.print(":");
    Serial.println(gauge);
  }
  digitalWrite(LED, button);
}

ゲージ量をカウントするためにgauge変数を用意し、4ピン(SW)が1ならwhile文で引っ掛けます。押している間はひたすらgaugeを加算していき、スイッチを離せば抜け出します。gaugeの値が一定の量(この場合、PUSH_SHORT = 100)を超えていれば、押したとみなし、命令を下します。

どうでしょうか?シンプルだけど確実に機能していると思います。この方法は「押されていれば、とりあえずカウントするけど、規定の数値に達してなかったら認めないよ」という単純な発想で、チャタリングで発生しているであろう小さいgaugeは無視しているわけです。

gauge_monitor_1

Serial.printで「押したとみなした数値」を出していますが、かなり大きいですよね。結構、早押ししたつもりですけど、押している間はgauge++しかしていないので、人の動作が如何にコンピュータより鈍いかが分かります。

そして、すでにひらめいた人がいるかもしれませんが、この方法だともうひとつメリットがあります。

それは長押しも判定できるということです。

#define LED 6
#define SW  4
boolean button = 0;

#define PUSH_SHORT 100
#define PUSH_LONG  140000

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(SW, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  unsigned long gauge = 0;

  while (!digitalRead(SW))
  {
    gauge++;
  }

  if (gauge > PUSH_SHORT)
  {
    button = !button;
    Serial.print(button);
    Serial.print(":");
    Serial.print(gauge);
    if (gauge > PUSH_LONG)
    {
      Serial.print("  LONG!");
    }else{
      Serial.print("  SHORT!");
    }
    Serial.println();
  }
  digitalWrite(LED, button);
}

新たにPUSH_LONGで既定量を作っておき、それによって判断すれば、簡単に「押してない」「押した」「長く押した」の分岐が操れるようになります。長押しのスケッチって普通に考えると結構な量になりそうですが、これなら一発で終わります。

便利なようですが、注意点が二つあります。

  • gaugeは時間ではなく、加算している数値なので、使うプロセッサによって個々に既定量の調整が必要になる
  • この例ではwhile文で単純なループをしているだけなので、押している間は他の事はできない。同時に何かさせたい場合工夫が必要

と、こんな感じで紹介させてもらいましたが、この方法は既に自分のproject「followfocusを作る」でも活用しています。リアルタイムで確実に反応させるものにはオススメです。

実はこのスケッチを書いた後、whileを使わなくてももっと反応を優先できそうなアイディアも浮かんだんですが、それはまた、このサイトにもっと反応が出てきたらにします。

複数のスイッチを使ったゲージ判定方式の説明(2016/12/1 追記)

最後にこのゲージ方式を使ってArduinoの簡単なゲームを作ってみたので紹介します。

Arduinoで簡易「シュウォッチ」

このゲージ方式で「結構正確なスイッチ判定ができるんじゃないかな」と思ったので、昔なつかし連射測定ゲームのスケッチを書いてみました。配線はこのページ最初にあるイラストと同じそのままです。スイッチとLEDだけ。

遊び方

スイッチを押すと、3秒カウントダウンし、ゲームがスタートします。10秒間の間にスイッチを何回連打できたかを競います。情報、結果はシリアルモニタで表示されます。

本気でカウントしたい人は44行目をコメントアウトしてください。Serial.printのモタつきで反応が悪くなるかもしれませんので。

それでは、お楽しみください。

#define LED 6
#define SW  4
#define PUSH_SHORT 500


void setup() {
  pinMode(LED, OUTPUT);
  pinMode(SW , INPUT_PULLUP);
  Serial.begin(9600);
  delay(100);
  Serial.println("Shooting Watch GAME   ver 1.0");
}

void loop() {
  unsigned long time_to_go = millis();

  //押したらゲーム開始
  if (BUTTON())
  {
    Serial.print("Ready?");

    //3秒カウントダウン
    for (byte i = 0 ; i < 3 ; i++)
    {
      time_to_go = millis();
      Serial.print(".");
      while ((millis() - time_to_go) <= 200) digitalWrite(LED, HIGH);
      while ((millis() - time_to_go) <= 800) digitalWrite(LED, LOW);
    }

    //スタート!
    Serial.println("GO!!!");
    int count = 0;
    unsigned long time_to_led;
    time_to_go = millis();
    digitalWrite(LED, HIGH);

    //押した回数をカウント
    while ((millis() - time_to_go) < (10 * 1000))
    {
      if (BUTTON())
      {
        count++;
        Serial.println(count); //本気の人はここをコメントアウト
        digitalWrite(LED, LOW);
        time_to_led = millis();
      }
      if ((millis() - time_to_led) >= 10) digitalWrite(LED, HIGH);
    }

    //終了。結果発表
    digitalWrite(LED, LOW);

    Serial.println();
    delay(500);
    Serial.print("your record: ");
    Serial.print(count / 10.0);
    Serial.println(" per sec");
    delay(500);
    if (count >= 160) Serial.print("Awesome!!!");
    else if (count >= 110) Serial.print("Good job!");
    else if (count >= 60) Serial.print("Try again...");
    Serial.println();
    delay(1000);
  }

}


boolean BUTTON() {
  long gauge = 0;

  while (!digitalRead(SW)) gauge++;

  if (gauge > PUSH_SHORT) return true;
  else return false;
}
 

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください