Release 2016.12.1 / Update 2017.6.26

続・プログラミングでチャタリング回避

ArduinoのSoftwareSerialでMIDI機器を鳴らす記事を書きましたが、その中で「複数のスイッチでもチャタリング回避する方法」を扱っています。以前書いた、While文で回避するゲージ判定の進化版なんですが、MIDIに興味ない人は見ないかなと思ったので、ここでもう一度まとめておきます。

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

こちらがゲージ判定方式で動いているスイッチ。

ちゃんと個別にスイッチのオンオフを認識しているのがわかると思います。

ゲージ判定方式

Arduino上でスイッチを使う場合、物理的な接点のハネ返りで正確なオンオフが判別できない場合があり、それはチャタリングと呼ばれています。Web上にはたくさん回避方法がありますが、電気回路的に回避する方法は別途パーツが必要だったり、一般的なプログラミング回避方法は精度が犠牲になったりします。そこで、簡単にプログラミングだけで、うまくチャタリングだけを無視する方法はないものかと模索してみたのがこの方法です。

メリット

  • 単純
  • 連打でも反応
  • 押し時間を反映できる

デメリット

  • スケッチの長さによって調整が必要
  • 判定用関数の配置に工夫が必要

以前の記事では1コのスイッチのみで、押している間は他の命令を挟みづらい方法でしたが、今回は複数でも対応できるようなスケッチの書き方を紹介します。

回路図

no_chatter_sw2_breadboard

  • Arduino
  • LEDx3
  • タクト(モーメンタリ)スイッチx3

no_chatter_sw2_insert2

スケッチ

1.個別スイッチでLEDのオンオフ

まず、3つのスイッチを使って、押すたびにLEDが点灯・消灯が反転するスケッチです。スイッチ&LEDは個別に動くので「1つを押しながら別を押す」なんて動作でも反応します。

#define SW_NUM 3
#define PUSH_SHORT  700
#define PUSH_LIMIT  PUSH_SHORT + 10

const byte s_pin[SW_NUM] = {5, 6, 7};
const byte leds [SW_NUM] = {8, 9, 10};


void setup() {
  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    pinMode(s_pin[i], INPUT_PULLUP);
    pinMode(leds[i], OUTPUT);
  }
}


void loop() {
  static bool led_stat[SW_NUM] = {0, 0, 0};

  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    byte sw = BUTTON(i);
    if (sw == 255) led_stat [i] = !led_stat [i];

    digitalWrite(leds[i], led_stat[i]);
  }
}


byte BUTTON(byte pin_num) {
  static unsigned short gauge[SW_NUM];

  byte sw_status = 0;

  //check switch status on or off
  if (!digitalRead(s_pin[pin_num]))
  {
    gauge[pin_num]++;
    gauge[pin_num] = min(gauge[pin_num], PUSH_LIMIT);
  } else {
    if (gauge[pin_num] >= PUSH_SHORT) sw_status = 255;
    gauge[pin_num] = 0;
  }

  //return value of switch status
  if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

  return sw_status;
}

解説

最初に扱うピンの数をSW_NUMで定義し、その数に合わせてスイッチとLEDのデジタルピン番号を配列に配置します。ちなみに、ここらを変更すれば扱えるスイッチの数が増やせます。

#define SW_NUM 3
#define PUSH_SHORT  700
#define PUSH_LIMIT  PUSH_SHORT + 10

const byte s_pin[SW_NUM] = {5, 6, 7};
const byte leds [SW_NUM] = {8, 9, 10};

メインのスイッチ判定関数「BUTTON」はWhile文ではなく、ifで処理しています。これなら、一つの命令で立ち止まらないので、複数のスイッチ判定も可能です。

gauge変数をカウンターとしてスイッチごとに用意し、digitalReadでHIGHならカウンター増加、LOWならリセットします。また、gaugeカウンターが増えすぎないようmin関数でPUSH_LIMIT内に制限しています。

byte BUTTON(byte pin_num) {
  static unsigned short gauge[SW_NUM];

  byte sw_status = 0;

  //check switch status on or off
  if (!digitalRead(s_pin[pin_num]))
  {
    gauge[pin_num]++;
    gauge[pin_num] = min(gauge[pin_num], PUSH_LIMIT);
  } else {
    if (gauge[pin_num] >= PUSH_SHORT) sw_status = 255;
    gauge[pin_num] = 0;
  }

  //return value of switch status
  if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

  return sw_status;
}

sw_status変数には状況に応じた値を代入しています。

ゲージカウンターがPUSH_SHORT値に達していれば「押した」と判定して1を代入。

if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

押してない場合、リセットする前にゲージがある程度貯まっていれば、「押して離した」と判定して255を代入。

if (gauge[pin_num] >= PUSH_SHORT) sw_status = 255;
gauge[pin_num] = 0;

何もなければ0のまま。それらの結果を返り値として渡します。つまりこの「BUTTON」関数はスイッチの状態を、

押してる→1
離した→255
押してない→0

で返す関数になります。あとは、その返り値で振る舞いを分ければ、機能として完成します。

void loop() {
  static bool led_stat[SW_NUM] = {0, 0, 0};

  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    byte sw = BUTTON(i);
    if (sw == 255) led_stat [i] = !led_stat [i];

    digitalWrite(leds[i], led_stat[i]);
  }
}

ここまでだと、スイッチを離した時だけのアクションだけです。なので、次はスイッチの押し時間の違いによって命令が変わるスケッチを紹介します。

no_chatter_sw2_insert1

2.押し時間の識別付き

押すたびにLEDの点灯・消灯が切替わるのは同じですが、しばらく押すと点滅、更に長く押すと早く点滅します。勿論、各スイッチは個別に動作します。

#define SW_NUM 3
#define PUSH_SHORT  700
#define PUSH_MID    20000
#define PUSH_LONG   100000
#define PUSH_LIMIT  PUSH_LONG + 10

const byte s_pin[SW_NUM] = {5, 6, 7};
const byte leds [SW_NUM] = {8, 9, 10};



void setup() {
  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    pinMode(s_pin[i], INPUT_PULLUP);
    pinMode(leds[i], OUTPUT);
  }
  Serial.begin(38400);
}



void loop() {
  static bool led_stat[SW_NUM] = {0, 0, 0};

  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    byte sw = BUTTON(i);

    bool  led_on = led_stat[i];
    short intval = 0;

    if (sw > 0) {
      if (sw == 255) led_stat [i] = !led_stat [i];
      else if (sw == 2) intval = 1000;
      else if (sw == 3) intval = 200;
      led_on = !led_on;
    }

    LEDS(i, led_on, intval);
  }
}



byte BUTTON(byte pin_num) {
  static unsigned long gauge[SW_NUM];

  byte sw_status = 0;

  //check switch status on or off
  if (!digitalRead(s_pin[pin_num]))
  {
    gauge[pin_num]++;
    gauge[pin_num] = min(gauge[pin_num], PUSH_LIMIT);
  } else {
    if (gauge[pin_num] >= PUSH_SHORT)
    {
      sw_status = 255;

      // head of checking push counter
      Serial.print((char)('A' + pin_num));
      Serial.print(":");
      Serial.println(gauge[pin_num]);
      // tail of checking push counter
    }
    gauge[pin_num] = 0;
  }

  //return value of switch status
  if (gauge[pin_num] >= PUSH_LONG) sw_status = 3;
  else if (gauge[pin_num] >= PUSH_MID) sw_status = 2;
  else if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

  return sw_status;
}



void LEDS(byte pin_val, bool stat, unsigned short delay_time) {
  static unsigned long led_intval[SW_NUM];
  unsigned long led_dur = millis() - led_intval[pin_val];

  if (delay_time > 0)
  {
    if (led_dur >= delay_time) led_intval[pin_val] = millis();
    else if (led_dur > (delay_time / 2)) stat = !stat;
  }

  digitalWrite(leds[pin_val], stat);
}

解説

基本的に1.での方法へ希望に合わせた定数を付け足すだけです。

#defineでスイッチ押しの長さ違いを定義。

#define SW_NUM 3
#define PUSH_SHORT  700
#define PUSH_MID    20000
#define PUSH_LONG   100000
#define PUSH_LIMIT  PUSH_LONG + 10

BUTTON関数内で振り分けを書き足していけば、自分の好きなように「押し時間のパターン」を増やすことが出来ます。そして、押しカウントによる違いをsw_statusに割り当てて値を返します。

byte BUTTON(byte pin_num) {
  static unsigned long gauge[SW_NUM];

  byte sw_status = 0;

  //check switch status on or off
  if (!digitalRead(s_pin[pin_num]))
  {
    gauge[pin_num]++;
    gauge[pin_num] = min(gauge[pin_num], PUSH_LIMIT);
  } else {
    if (gauge[pin_num] >= PUSH_SHORT)
    {
      sw_status = 255;

      // head of checking push counter
      Serial.print((char)('A' + pin_num));
      Serial.print(":");
      Serial.println(gauge[pin_num]);
      // tail of checking push counter
    }
    gauge[pin_num] = 0;
  }

  //return value of switch status
  if (gauge[pin_num] >= PUSH_LONG) sw_status = 3;
  else if (gauge[pin_num] >= PUSH_MID) sw_status = 2;
  else if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

  return sw_status;
}

「BUTTON」関数からの返り値で命令を振り分ければ完了です。ここでは最終的に「LEDS」関数へ送る変数への代入を振り分けています。

void loop() {
  static bool led_stat[SW_NUM] = {0, 0, 0};

  for (byte i = 0 ; i < SW_NUM ; i++)
  {
    byte sw = BUTTON(i);

    bool  led_on = led_stat[i];
    short intval = 0;

    if (sw > 0) {
      if (sw == 255) led_stat [i] = !led_stat [i];
      else if (sw == 2) intval = 1000;
      else if (sw == 3) intval = 200;
      led_on = !led_on;
    }

    LEDS(i, led_on, intval);
  }
}

カウンター値の調整

この方式ではloopの中で「BUTTON」を実行するインターバル時間によって、カウンターの増加量が変わってきます。つまり、#defineで定義するカウンターの値はスケッチの長さによって変更しないといけません(delayなど停滞する関数を使わなければ劇的に変わらないとは思いますが)。

#define PUSH_SHORT  700
#define PUSH_MID    20000
#define PUSH_LONG   100000
#define PUSH_LIMIT  PUSH_LONG + 10

サンプルスケッチの中では押した長さのカウンター値をシリアルモニタに返すようしています。

// head of checking push counter
 Serial.print((char)('A' + pin_num));
 Serial.print(":");
 Serial.println(gauge[pin_num]);
// tail of checking push counter

no_chatter_sw2_serial_mon

自分の感覚で「これが長押し」「中押し」「押し」になるような値を探ってみてください。ただし、自分の製作しているプロジェクトが出来上がってきたとき、再度最終調整するのをオススメします。また、Serial通信はloopの処理速度に影響を与えるので、調整ではないのであればコメントアウトして外しておくほうが無難です。

delay関数を使うようなスケッチだと、カウンター値が大幅に減ってしまうので、そこら辺が面倒な方はタイマー割り込みで動かすほうがいいかもしれません…。

まとめ

最後に「BUTTON」関数に必要な要素をまとめておきます。

#define SW_NUM 3
#define PUSH_SHORT  700
#define PUSH_MID    20000
#define PUSH_LONG   100000
#define PUSH_LIMIT  PUSH_LONG + 10

const byte s_pin[SW_NUM] = {5, 6, 7};  // digital pins for Switches

/* use in setup()
 for (byte i = 0 ; i < SW_NUM ; i++) pinMode(s_pin[i], INPUT_PULLUP);
*/

byte BUTTON(byte pin_num) {
  static unsigned long gauge[SW_NUM];

  byte sw_status = 0;

  //check switch status on or off
  if (!digitalRead(s_pin[pin_num]))
  {
    gauge[pin_num]++;
    gauge[pin_num] = min(gauge[pin_num], PUSH_LIMIT);
  } else {
    if (gauge[pin_num] >= PUSH_SHORT)
    {
      sw_status = 255;

      // head of checking push counter - comment out on normal running
      Serial.print((char)('A' + pin_num));
      Serial.print(":");
      Serial.println(gauge[pin_num]);
      // tail of checking push counter
    }
    gauge[pin_num] = 0;
  }

  //return value of switch status
  if (gauge[pin_num] >= PUSH_LONG) sw_status = 3;
  else if (gauge[pin_num] >= PUSH_MID) sw_status = 2;
  else if (gauge[pin_num] >= PUSH_SHORT) sw_status = 1;

  return sw_status;
}
 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


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