Release 2017.6.26 / Update 2018.4.30

「B2CS」Arduino用自作ライブラリプログラミングでチャタリング回避

“ゲージ判定”を使ったチャタリング回避ライブラリを作ってみました。Arduino用です。

こんな事ができます。

以前、タクトスイッチを使いArduinoでMIDIを鳴らす方法を紹介しましたが、原理は一緒です。プログラミングだけでチャタリングを回避し、複数のスイッチを手軽に扱えるようにします。

見よう見真似で作ったライブラリなので、コードの稚拙さは勿論、まだ使い倒してないため不具合があるかもしれません。それでも興味がある方は試してみてください。

始めに

B2CSライブラリの特徴

このライブラリの目的は、Arduinoでのモーメンタリスイッチ使用時、物理的に起きる“チャタリング”をプログラミングだけで回避することです(完全ではありませんが…)。仕組みは単純で、digitalRead一回の読み取りでなく、何回か読みに行った統計を“ゲージ”として捉え、一定量を超えたときにONとみなす“ゲージ判定”によるスイッチ判別方法です。また、以前の記事で紹介したとおり、“ゲージ判定”によって、ついでに可能になった「長押し」の判別も出来るようになっています。

そして、初期設定では最大8個までのスイッチを個別に判定できる仕様にしています。

未完成

普段スケッチを書くのにClassを使うことも無ければ、勉強もしていませんので、ソースコードがひどい状態です。無駄な関数や変数も多く、容量がかさばっています。

多分、判る人が見れば「なんじゃこりゃ」と思うでしょう。無知な自分でさえ、「Classはこう使うものじゃない」と確信しています。ですが、整理・修正していこうとなると、勉強しつつになるので、だいぶ先の長い話になってしまいます。

なので、このライブラリは「ベータ版」という事でご了承ください。

ただ、そのマイナス点を考慮しても、十分機能はすると思うので、特にArduino初心者の方(自分もまだまだ初心者レベルですが…)には重宝してもらえるのでは、と。

B2CSライブラリの導入

B2CSライブラリでは「タイマー割り込み」を使って、頻繁にスイッチの状態を確認しています。その機能にはMsTimer2というライブラリを使っているので、ArduinoIDEにインストールされている事が前提となります。現行のIDEなら標準で組み込まれているかもしれませんが、もし入っていないなら下記を参考にインストールしてください。

*MsTimer2使用時は3, 11番ピンのPWMを使えなくなるようです

MsTimer2ライブラリのインストール

IDEのプルダウンメニュー「スケッチ」の「ライブラリをインクルード」から「ライブラリを管理」で“ライブラリマネージャー”を開きます。

検索で“MsTimer2”と入力。「INSTALLED」ならOK。そうでなければインストールしてください。

インストール後は念のため、一度IDEを終了して、立ち上げ直してください。

ライブラリZIPファイル

こちらのファイルをダウンロードしてください。

 version  日付  修正内容
B2CS.ZIP – version 1.2.0 2018.4.25
  • 読み取りタスクの負荷軽減
  • PUSH_MID(中間)の実装
B2CS.ZIP – version 1.1.0 2017.11.14 readOnce実装
B2CS.ZIP – version 1.0.2 2017.6.26 bug fix
B2CS.ZIP – version 1.0 2017.6.25  

ver 1.2.0 での変更点 (2018.04.25 追記)

新しく考えた方法に基づいて、ピンの読み取りタスクの負荷を軽減を試み、更に、「押し」「長押し」の間に「中間押し」判定も実装してみました。ただし、それに伴い、それぞれの「押し」判定のしきい値、「define定義文字列の順番」に変更が出たので、ご注意ください。

また、下記の説明、サンプルスケッチもところどころ修正しています。

以前のバージョンを削除 (2017.11.14 追記)

古いバージョンを入れている方は、新しいバージョンをインストールする前に、削除作業をしてください。手間ですが、手作業でフォルダを削除します。

Arduino IDE プルダウンメニュー「ファイル」から「環境設定」でライブラリの保存先を確認します。

このフォルダの中に「libraries」というフォルダがあり、そこにインストールされているはずです。この、古い「B2CS」ライブラリをフォルダごと削除してください。

Users\ユーザー名\Documents\Arduino\libraries

インストール

プルダウンメニュー「スケッチ」の「ライブラリをインクルード」から「.ZIP形式のライブラリをインストール」を選択。ダウンロードしたZIPを選択してください。

こちらも念のため、一度IDEを終了して、立ち上げ直しします。

使い方

まずは一番シンプルな使い方で、順を追って説明します。

サンプルスケッチ 1

B2CS使い方の基本です。

回路図

必要なのはモーメンタリスイッチ1個だけです。

1.ライブラリのインクルード

インストールした”B2CS”ライブラリを読み込みます。

2. インスタンス作成

関数外でインスタンスを作成。名称は任意でOKですが、ここでは小文字で“jumbleat_sw”にします。

B2CS jumbleat_sw;

3.ピンのアサイン

スイッチとして使うピンの設定をします。設定には.setSW()という関数を使います。

.setSW( ピン番号、INPUT/INPUT_PULLUP、スイッチON理論値 );

3つの引数が入りますが、最初の2つはpinMode()と一緒です。3つ目には、スイッチがONになる時の理論値を入れます。

この例では、2ピンへ内部INPUT_PULLUP接続したいので、

jumbleat_sw.setSW( 2, INPUT_PULLUP, false );

となります。

例えば、扱うスイッチを増やしたければ、この関数を再度呼び出すだけです。

jumbleat_sw.setSW( 2, INPUT_PULLUP, false );
jumbleat_sw.setSW( 3, INPUT_PULLUP, false );
.
.

単純です。

4.タイマー始動

ピンの設定が終わったら、割り込みをスタートさせます。関数は.GO()です。

jumbleat_sw.GO();

Setup()内の最後の方で呼び出します。

5.状態を読み取り

後は、loop()内等でスイッチの状態を読みに行って、分岐命令を書くだけです。ピンの状態を見るには.read()関数を使います。

.read( ピン番号 );

こちらの関数には下記の返り値があります。

define定義文字列 状態 数値 バイナリ
– – – – – 押されていない 0 b0000 0000
SW_PUSHED スイッチが押されている 1 b0000 0001
SW_PUSHED_MID しばらくスイッチが押されている 3 b0000 0011 
SW_PUSHED_LONG 長いことスイッチが押されている 7 b0000 0111
SW_RELEASED スイッチを離した 9 b0000 1001 
SW_RELEASED_MID しばらく押した後スイッチを離した 11 b0000 1011
SW_RELEASED_LONG 長いこと押した後スイッチを離した 15 b0000 1111

つまり、単純に押した後で何かアクションをつけたければ、

if ( jumbleat_sw.read(2) == SW_RELEASED )
{
//  させたい命令を書く
}

 これで成立します。

また、離した時の返り値は「SW_RELEASED」「SW_RELEASED_MID」「SW_RELEASED_LONG」の3つがありますが、LONGの方が値が大きいので、下記のような書き方でまとめることも出来ます。

if ( jumbleat_sw.read(2) >= SW_RELEASED )

6.まとめ

以上をまとめた「スイッチを押すとカウントする」スケッチです。返しはシリアルモニタに表示させます。

#include <B2CS.h>
B2CS jumbleat_sw;

void setup() {
  jumbleat_sw.setSW(2, INPUT_PULLUP, false);
  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  static int val;
  if (jumbleat_sw.read(2) >= SW_RELEASED)
  {
    val++;
    Serial.println(val);
  }
}

連打してチャタリングの塩梅も確認してみてください。

サンプルスケッチ 2

状態によって違う反応をさせたい場合、一時的な変数に投げ込みます。例えば、ピン2のスイッチであれば、こんな感じです。

byte sw_cur = jumbleat_sw.read(2);

.read()は一度呼び出すと、この「読み取り」は(特に「離した」事に対して)リセットされるため、消される前にキープしておくというわけです。

そして、返り値の大きい設定からif文で分岐命令します。

    byte sw_stat = jumbleat_sw.read(2);

    if (sw_stat == SW_RELEASED_LONG) {

        // action when switch has Released after "long push"

    } else if (sw_stat == SW_RELEASED_MID) {

        // action when switch has Released after "middle push"

    } else if (sw_stat == SW_RELEASED) {

        // action when switch has Released after "push"

    } else if (sw_stat == SW_PUSHED_LONG) {

        // action when switch is being pushed for "long time"

    } else if (sw_stat == SW_PUSHED_MID) {

        // action when switch is being pushed for "middle time"

    } else if (sw_stat == SW_PUSHED) {

        // action switch is pushed

    } else {

        // action when switch is not pushed

    }

「離した」時の返り値は「押し」判定より大きいビットに配置しているので、別変数でマスキングしたものを作っておくと、更に見易くなると思います。

byte sw_stat = jumbleat_sw.read(2);
byte sw_stat_push = sw_stat & SW_PUSHED_LONG;

という事を踏まえ、以下、「スイッチの押し・離し具合で反応が変わる」サンプルスケッチです。

シリアルモニタにvalueのカウントが表示されます。

#include <B2CS.h>
B2CS jumbleat_sw;

void setup() {
  jumbleat_sw.setSW(2, INPUT_PULLUP, false);
  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  static bool one_shot_push = false;
  static int value;

  byte sw_stat = jumbleat_sw.read(2);
  byte sw_stat_push = sw_stat & SW_PUSHED_LONG;

  if (sw_stat)
  {
    int value_ref = value;

    // --- after switch release
    if (sw_stat >= SW_RELEASED)
    {
      if (sw_stat_push == SW_PUSHED_LONG) {
        value = 0;
        Serial.print("Reset! ");
      } else if (sw_stat_push == SW_PUSHED_MID) {
        // non
      } else {
        // non
      }
      one_shot_push = false;


      // --- during switch being pushed
    } else {
      if (sw_stat_push == SW_PUSHED_LONG) {
        Serial.print("Double! ");
        value += 10;
      } else if (sw_stat_push == SW_PUSHED_MID) {
        value++;
        Serial.print("Continuous! ");
        delay(50);
      } else {
        if (!one_shot_push)
        {
          value++;
          one_shot_push = true;
          Serial.print("Just add! ");
        }
      }
    }

    if (value != value_ref) Serial.println(value);
  }
}

短い「押し」の時は、繰り返し加算しないよう“one_shot_push”で制限をかけ、「離した」時に解除するようにしています。

サンプルスケッチ 3

次は複数のスイッチを使ったサンプルを書いてみます。スイッチの「押し」「長押し」「離した」等の状態変化によって振る舞いを変えてみます。

回路図

モーメンタリスイッチとLEDを4個づつ用意。LEDにつける抵抗も必要です。

離した時「点滅、押した回数のカウントのシリアル表示」、長押し時「ゆっくり点滅」、単に押した時「点灯」という振り分けをしたスケッチを書いてみます。諸々のピン設定や変数は“配列”としてまとめて繰り返し、なるべく簡潔にしています。

#include <B2CS.h>
B2CS jumbleat_sw;

#define SIZE 4
byte sw_pins[SIZE]  = {2, 3, 4, 5};
byte led_pins[SIZE] = {A0, A1, A2, A3};

void setup() {
  for (byte i = 0 ; i < SIZE ; i++)
  {
    jumbleat_sw.setSW(sw_pins[i], INPUT_PULLUP, false);
    pinMode(led_pins[i], OUTPUT);
  }

  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  static int  val[SIZE];
  static bool led_status[SIZE];
  static unsigned long time_led[SIZE];

  for (byte i = 0 ; i < SIZE ; i++)
  {
    // get switch status
    byte sw_stat = jumbleat_sw.read(sw_pins[i]);

    // when switch has Released
    if (sw_stat >= SW_RELEASED)
    {

      for (byte ii = 0 ; ii < 10; ii++)
      {
        digitalWrite(led_pins[i], ii % 2);
        delay(25);
      }
      val[i]++;
      Serial.print((char)('A' + i));
      Serial.print(":");
      Serial.println(val[i]);

      // when switch has been pushed for long time
    } else if (sw_stat == SW_PUSHED_LONG) {

      if ((millis() - time_led[i]) > 500)
      {
        led_status[i] = !led_status[i];
        digitalWrite(led_pins[i], led_status[i]);
        time_led[i] = millis();
      }

      // when switch has turned into ON
    } else if (sw_stat == SW_PUSHED) {
      digitalWrite(led_pins[i], HIGH);
    } else {
      // if switch is not pushed
      digitalWrite(led_pins[i], LOW);
    }
  }

}

各々のスイッチが単独で反応してくれると思います。ここら辺は以前の記事でも書いたやり口と一緒ですが、ライブラリで扱うとスッキリします。

ただ、「離した時」の早い点滅をdelayを使ったfor文でやっていて、このやり方はあまりオススメできません。点滅中は他の事が出来ないので、例えば、同時にスイッチを離しても一個ずつ処理されていくと思います。早い反応を求めないならいいんですけど、並列で処理したい場合は、なるべくforやdelayを使わない「流れる」書き方を工夫する必要があります。

それでも、そうは言ってられない時もあるかもしれません。そんな時は以下の方法もあります。

.count()関数

.read()と違い、こちらは前回呼び出してから現在までの間に、スイッチを押した回数を返す関数です。

例えば、forとdelayを使って、10秒間LEDを点滅をさせるスケッチを書いたとします。.read()だと、その間の状態は1回分しか認識しませんが、.count()であれば、255回までその押した数を教えてくれます。

タクトスイッチ Arduino D2ピンへ
LED&抵抗  Arduino D13ピンへ

#include "B2CS.h"
#define SW1 2
#define LED 13

B2CS jumbleat_sw;

void setup() {
  pinMode(LED, OUTPUT);
  jumbleat_sw.setSW(SW1, INPUT_PULLUP, false);
  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  static byte count;

  // 10 seconds task
  for (byte i = 0 ; i < 10 ; i++) {
    digitalWrite(LED, i % 2);
    delay(1000);
  }

  //counting
  byte temp = jumbleat_sw.count(SW1);
  if (temp > 0)
  {
    count += temp;
    Serial.print(temp);
    Serial.print("  TOTAL:");
    Serial.println(count);
  }
}

ただし、これを使うと、「長押し」も「長押し後の離し」も分かりません。あくまで、更新できない間に押された回数のみです。悪しからず。

.readOnce()関数 (2017.11.14 追記)

上述までのやり方だと、状態を見ることが主体なので、単純に、「押したら即反応」っていうのは書きづらいかと思います(まあ、工夫すれば出来るんですが…)。

この関数は、単純にチャタリングを排除した「押し」判定を使いたい場合に便利です。

jumbleat_sw.readOnce( ピン番号 );

スイッチを押してから閾値を越えたら、1度だけtrueを返します。その後、スイッチを離してリセットされるまで、falseを返し続けます。

こういう使い方をしたい方が多いのではないかと思い実装してみました。

#include <B2CS.h>
#define SW1 2

B2CS jumbleat_sw;

void setup() {
  jumbleat_sw.setSW(SW1, INPUT_PULLUP, false);
  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  static int count;
  if (jumbleat_sw.readOnce(SW1))
  {
    count++;
    Serial.println(count);
  }
}

あくまで、押したことを早く認知するために作ったので、通常の.read関数とは併用が難しいかもしれません。悪しからず。

また、併せてリセットする関数も用意しました。

jumbleat_sw.ResetreadOnce( ピン番号 );

この二つを組み合わせれば、押している間「一定期間で繰り返す」という機能も可能です。

void loop() {
  static unsigned long time_intval = millis();
  static int count;
  if (jumbleat_sw.readOnce(SW1))
  {
    count++;
    Serial.println(count);
    time_intval = millis();
  }

  if ((millis() - time_intval) > 1000) jumbleat_sw.ResetreadOnce(SW1);
}

millis()で時間をキープする変数を作り、それに沿ってリセットするようにします。スイッチを押し続ければ、一定間隔でカウントが増えていきます。

チューニング

この“ゲージ判定”はdigitalReadの統計から判別しているので、読みに行く回数が重要になっています。そして、その読み取り回数は、使うArduinoの速さ(クロック周波数)、スケッチの長さなどによって変わってきます。つまり、設定している初期値は、状況によっては弊害にもなるので、自分の状況に合わせて諸々の数値をカスタマイズする必要が出てきます。

ゲージ判定値の調整

「押し」判定しきい値は

.setPushShort( 数値 );

「中間押し」判定しきい値は

.setPushMid( 数値 );

「長押し」判定しきい値は

.setPushLong( 数値 );

で変更できます。

この数値は時間ではなく、読み取り統計のしきい値なので、Arduinoの処理速度(クロック周波数)に依存します。ただし、version 1.2.0で「PushShortまでは頻繁に、その後は緩やかに」というような読み取り方に変更したので、PushShort調整は不要になったかな、と思います。

一応、初期値ではPushShort「300」、PushMid「350」、PushLongを「850」としています。

これはArduinoUno(16MHz)での使用を想定しているので、自分の環境に合わせて調整する必要が出てくると思います。しきい値の設定をシリアルモニタで確認しながら、と言う場合は下記を使用してください。

.GetGauge()関数

この関数は実際にカウントされたゲージ量を返します。そこで「サンプルスケッチ1」の回路図で、下記のようなスケッチを実行します。

#include <B2CS.h>
B2CS jumbleat_sw;

void setup() {
  jumbleat_sw.setSW(2, INPUT_PULLUP, false);
  jumbleat_sw.setPushLong(30000);
  jumbleat_sw.GO();
  Serial.begin(38400);
}

void loop() {
  if (jumbleat_sw.read(2) >= SW_RELEASED)
  {
    Serial.println(jumbleat_sw.GetGauge());
  }
}

スイッチを離した後、押していた間のゲージ量がシリアルモニタ上に表示されます。この値を参考に自分に合ったPushMid、PushLongのしきい値を探してください。

PushShortをあまり低くするとチャタリングも拾うようになってしまい、逆に高すぎると、スイッチ自体の反応が鈍くなってしまうので注意してください。

割り込み頻度の調整

B2CSのタイマー割り込みは初期値で5msに設定しています。でも、これでもやりたい事によっては邪魔になるかもしれません。

.GO()関数は、引数を入れる事で割り込み頻度を変更出来るようにしています。

.GO( ミリ秒 – 整数 );

ただし、間隔を縮めたり伸ばしたりすれば、その分、取りこぼしが起き、PushMid、PushLongのしきい値も変わってくるので、反応全体が変わってきます。タイマーとしきい値、双方の兼ね合いで調整する必要が出てきます。

最大スイッチ数の変更

.setSW()は最大8個までスイッチを設定できますが、メモリやタスク時間を削減するために減らしたり、或いはもっとたくさんスイッチを扱いたい場合が出てくるかもしれません。

「インスタンスを増やせば」と思うかもしれませんが、技術的に難しかったのでそこの対応は出来ていません。が、ライブラリのファイル内の定数を直接書き換える事で最大数を変更できます。インストールされた「B2CS.h」ファイル内に記述されています。

保存先は環境によって変わるかもしれませんが、Windowsの場合、

C:\Users\ユーザー名\Documents\Arduino\libraries\B2CS

B2CS_PINS_MAXが最大ピン数の初期設定値です。

ちなみにB2CS_PUSH_SHORT、B2CS_PUSH_MID、B2CS_PUSH_LONGはゲージ判定しきい値の初期設定なので、もしスケッチの度に.setPushLong()等で変更するのが面倒であれば、ここを書き換えてもらったほうがいいかもしれません。

関数一覧

.setSW(pin_number, pin_setting, pin_logic)

B2CSで使うスイッチ用のピンを設定します。

引数 説明
pin_number byte スイッチを接続したArduinoピン
ピン番号 整数
pin_setting byte スイッチの接続回路
INPUT、INPUT_PULLUP
pin_logic boolean スイッチON時の理論値設定
0 / false / LOW、1 / true / HIGH

.read(pin_number)

ピンの状態を確認します。Byte型での返り値があります。

引数 説明
pin_number byte 確認したいスイッチのピン番号
整数
define定義文字列 状態 数値 バイナリ
– – – – – 押されていない 0 b0000 0000
SW_PUSHED スイッチが押されている 1 b0000 0001
SW_PUSHED_MID しばらくスイッチが押されている 3 b0000 0011 
SW_PUSHED_LONG 長いことスイッチが押されている 7 b0000 0111
SW_RELEASED スイッチを離した 9 b0000 1001 
SW_RELEASED_MID しばらく押した後スイッチを離した 11 b0000 1011
SW_RELEASED_LONG 長いこと押した後スイッチを離した 15 b0000 1111

.count(pin_number)

前回呼び出してから、現在までに押された回数をByte型で返します。

引数 説明
pin_number byte 確認したいスイッチのピン番号
整数

.setPushShort(value)

「押し」判定になるしきい値を変更します。値を大きくするほど、チャタリングしなくなりますが、その分反応が鈍くなり、逆に小さくすると、反応が良くなりますが、チャタリングも拾うようになります。

PushMid、PushLongより大きい値は入れないでください。

引数 説明
value unsigned int チャタリングと「押し」を判別するしきい値
整数

.setPushMid(value)

「押し」以上「長押し」以下の、「中間押し」判定になるしきい値を変更します。

PushShortより小さい値、PushLongより大きい値は入れないでください。

引数 説明
value unsigned int チャタリングと「中間押し」を判別するしきい値
整数

.setPushLong(value)

「長押し」判定になるしきい値を変更します。PushShortより小さい値は入れないでください。

引数 説明
value unsigned int 「長押し」を判別するしきい値
整数

.GO() / .GO(ms)

B2CSのタイマー読み取りを開始します。引数を入れない場合は5msで作動します。

引数 説明
ms byte タイマー割り込みの間隔
ミリ秒(整数)

.B2CS_STOP()

B2CSのタイマー読み取りを停止します。

.GetGauge()

実際に計測しているゲージ量を返します。PushShort以下の場合は前回の値のままになるので、全部の値を確認したければ、PushShortを1に設定するといいかもしれません(チャタリングも出るようになりますが)。

.readOnce(pin_number)

スイッチの判定が閾値が越えた時、1度だけtrueを返します。後は、.ResetreadOnce()を呼ぶか、スイッチが離されないとfalseしか返しません。

引数 説明
pin_number byte 確認したいスイッチのピン番号
整数

.ResetreadOnce(pin_number)

.readOnce()の返り値をリセットします。

引数 説明
pin_number byte 確認したいスイッチのピン番号
整数

 

まだ試作段階に近いので、どこでどういった不具合が出るのかも分かりません。そういった症状を見つけた方は、コメント欄に書き込んでいただけるとありがたいです。また、使ってみた感想などでも嬉しいです。

参考リンク

コメントを残す

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

CAPTCHA


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