Arduino環境でのタイマ管理とDIのチャタリング防止の方法

組み込みエンジニア

こんにちは、ENGかぴです。

Arduino環境でソフト開発しているとDIに信号を入力して処理を判定させることがよくあります。DI信号がスイッチなどであった場合チャタリングによってDI信号が安定しないことがあります。対策としてフィルタを作る方法を記事にしました。

Arduino UNOを対象としますが、ArduinoのライブラリであるMsTimer2を使用してタイマを起動しメイン関数内でベースタイマを作ってタイマの管理を行います。

Arduino UNOを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Arduinoでタイマを動作させる

ArduinoのIDEの初期ではタイマ動作をさせるライブラリがありません。(あるのかもしれませんが分かりませんでした。)Arduino環境でタイマ2を動作させるライブラリとしてよく使用されているMsTimer2のライブラリをインストールして使用します。

MsTimer2のインストール

Arduino-ライブラリマネージャ
Arduino-ライブラリマネージャ

Arduino IDEのツール内のライブラリを管理を選択するとライブラリマネージャ画面が表示されます。検索をフィルタに「mstimer2」(大文字小文字はどちらでもよい)を入力すると表示される「MsTimer2」中の右下のインストールボタンを押すとインストールされます。

上記の例は既にインストール済みなのでインストールボタンが表示されていません。

タイマの管理の考え方

タイマ管理の考え方のイメージ

タイマ2を初期設定する際に設定した割り込み周期がベースタイマとなります。

割り込みが入る毎にカウントを+1して更新していき、規定回数(ソフトウェアタイマの分周比を決める値)以上になったときソフトウェアタイマを更新します。

例)ベースタイマを1msとし、規定回数を10とした場合
1ms間隔でタイマ2の割り込みが発生しベースタイマ値を更新します。規定回数以上になったとき1msが10回経過したことになるので10msのタイミングをつくることができます。

欠点はメイン関数の1周の時間に左右されることですが、通常メイン関数のループは1msを超えることが少ないため問題にならないことが多いです。メイン関数の1周に時間を要している場合は誤差が大きくなる可能性もありますので誤差が問題になるような場合は注意が必要です。

意図してメインループ1周が遅れている場合は問題ありませんが、割り込みを含めて遅くなりすぎる場合はプログラムの構成を考え直した方が良いかもしれません。

タイマ管理を実装する

MsTimer2の初期化とタイマ管理について一例を示します。

#include <MsTimer2.h>

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define LED_ONOFF 50

// application use
int8_t timled; //管理したいタイマ
int8_t cnt1ms; //1ms毎にカウントアップさせる
/*** Local function prototypes */
void TimerCnt();

MsTimer2を使用するためにインクルードします。手打ちしてもよいですがスケッチ内のライブラリのインクルードからMsTimer2を選択することで表示を追加することができます。

管理したいタイマを符号付の変数として宣言します。-1をTIME_OFFと定義しタイマを使用しない時にセットします。MsTimer2のコールバック用の関数としてTimerCnt()を宣言しています。

void setup() {

    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移 TimerCnt関数がコールバックされる
    MsTimer2::start();
    timled = LED_ONOFF; //ソフトウェアタイマを起動(値をセット)
}
/* Timer2 callback function add */
void TimerCnt(){
    ++cnt1ms; //ベースカウントを更新
}

MsTimer2割り込みが発生するとTimerCnt()がコールバックとして呼び出されます。TimerCnt()でベースカウントをカウントして割り込みが発生した回数をカウントしています。

void loop() {
 
    if( cnt1ms >= BASE_CNT ){//10msごとにここに遷移する
        cnt1ms -=BASE_CNT;
    
        if( timled > TIME_UP ){ //タイマ値を更新
            timled--;
        }
    }

    if( timled == TIME_UP ){
        //タイムアップた時に処理したい内容を入れる
        //timled = TIME_OFF; タイマを使用しない時はOFFにすると更新停止となる
    }
}

ベースカウントが規定値(10)になったときif文の条件を満たすためタイマ値の更新処理となります。

MsTimer2の割り込みタイミングとBASE_CNTを組み合わせることで任意のタイマを作ることができます。

MsTimer2を使用しない場合(参考)

Arduino環境では32ビットでmsec管理できる関数としてmillis()がライブラリとして搭載されています。

#define BASE_CNT 10
uint32_t basemillis;    

if( millis() - basemillis > BASE_CNT ){
    basemillis = millis(); //条件を満たしたときのmsecを保管
    
    if( timled > TIME_UP ){ //タイマ値を更新
        timled--;
    }
}

millis()では経過したmsecをカウントし続けるためカウント値がBASE_CNT以上の差になったときが10ms経過した時間になるためMsTImer2を使用したときと同じ動きになります。

欠点としては値がオーバーフローして0xFFFFFFFFから0に戻ったときにカウント値の判定が一度だけずれてしまうことです。オーバーフローするのは電源ONから1193時間後なので常にONしておくことがなれば問題になりません。

チャタリング防止を考える

マイコンのDIはデジタル入力で入力された電圧値からHighレベル(1)かLowレベル(0)かの判定を行います。スイッチのチャタリングなどが原因でマイコンが誤動作する可能性があるため注意が必要です。

DI信号のチャタリング

チャタリング防止の考え方
チャタリング防止の考え方

スイッチがONからOFFになったときスイッチの接触抵抗などによって電圧レベルが安定しない瞬間があります。数ms経過するとDI信号が安定するため、安定後にDI情報を取り込む対策が必要です。

僅か数ms間ではありますが1と0が繰り返されること0からフィルタを入れていなかった場合誤動作の可能性があります。リレーの接点などは容量が大きくチャタリングが大きく出ることがあります。

周辺回路でコンデンサを入れたりすることでチャタリングの高周波成分を減衰させることができますが、完全に除去できるわけではありません。

ソフトでDI信号が安定したことを確認して入力として採用することでチャタリングの影響を抑えることができます。

DIフィルタを実装する

DIフィルタはDIのチャタリング対策のために実装するものです。DIフィルタの方法は様々ですが以下の2通りが主になります。

  1. タイマを使って複数回一致した値を採用する方法
  2. メインループ周回ごとにDI情報を比較して規定回数一致を確認する方法

今回は1のタイマを使った方法を実装します。

#define DI_FILT_MAX 4

enum DI_NO{
    DI1 = 0,
    DI2,
    DI_MAX
};

struct DIFILT_TYP{
    uint8_t wp;
    uint8_t buf[DI_MAX][DI_FILT_MAX];
    uint8_t di1;
    uint8_t di2;
};

DIFILT_TYP difilt;
void DiFilter();

上記のタイマ管理の方法を使って10ms毎にDIの情報を確認し4回一致したときDI値として採用する方法をDiFilter()関数ないに実装していきます。

void DiFilter(){

    if( timdifilt == TIME_UP ){
        difilt.buf[DI1][difilt.wp] = digitalRead(PIN_DI1);
        difilt.buf[DI2][difilt.wp] = digitalRead(PIN_DI2);
    
        if( difilt.buf[DI1][0] == difilt.buf[DI1][1] &&
            difilt.buf[DI1][1] == difilt.buf[DI1][2] &&
            difilt.buf[DI1][2] == difilt.buf[DI1][3] ){
            difilt.di1 = difilt.buf[DI1][0];
        }
    
       if( difilt.buf[DI2][0] == difilt.buf[DI2][1] &&
           difilt.buf[DI2][1] == difilt.buf[DI2][2] &&
           difilt.buf[DI2][2] == difilt.buf[DI2][3] ){
           difilt.di2 = difilt.buf[DI2][0];
        }
        
       if( ++difilt.wp >= DI_FILT_MAX ){
           difilt.wp = 0;
       }

       timdifilt = FILT_MIN;
    }
}

timdifiltタイマがTIME_UPすると内部の処理を行います。DI情報を格納するバッファを更新しながら入れていき4つのバッファの値が一致したときDI情報として採用しています。

4回一致でDI情報として採用するためチャタリングによる不安定な部分を除いて値が採用できるようになります。

timdifiltにセットする値を大きくするとDI情報の安定を判定する時間は長くなりますが、チャタリング防止による誤動作は防ぎやすくなります。

例では4つのバッファで構成していますが、タイマの値を短くしたりバッファの値を増やす方法を選択したりすることで任意のフィルタを実装することができます。

void setup() {
   
    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移
    MsTimer2::start();
    timdifilt = FILT_MIN;

    for( uint8_t i=0; i < 10; i++ ){
       //メインに行く前にDI情報を確定しておく
        mainTimer();
        DiFilter();
        delay(10);
    }
}

メインループに遷移する前にDI情報を確定しておきたい場合はsetup()関数から抜ける前にDIフィルタ処理を入れておくとよいです。

製品の動作トリガーになっている場合やDI情報で処理を分岐させる場合は、誤動作防止のためDIフィルタの初期化はしておいたほうが良いでしょう。

タイマ管理とDIフィルタの動作を確認する

タイマ管理とDIフィルタの動作確認
タイマ管理とDIフィルタの動作確認

11番をDOにしてタイマ管理の方法でLEDを点灯/消灯しています。同じタイミングだとタイマ管理ができていることの確認がしにくいため点灯の時間を短くして消灯の時間を短くするようにします。

6番をDIピンのマイコンプルアップに設定しSW1を押したときLOWレベルになりシリアルモニタに「di1–ok」を表示するようにします。同様に7番もDIピンのマイコンプルアップにしSW2を押したときLOWレベルにしシリアルモニタに「di2–ok」を表示するようにします。

10msのタイマ管理で4回分の値が一致してから出力が変更されることから応答時間が最小で約40msとなります。

タイマーの時限を10msとしているのでボタンを押すタイミングによっては最大で10msの誤差が出ます。これらを考慮すると応答時間は40ms~50msになります。

シリアルモニタでの確認
シリアルモニタでの確認

SW1とSW2を押したときにシリアルモニタに対象の文字が表示されます。10msのタイマで確認しているため効果が分かりにくいのですが、DIフィルタをタイマ管理を長くとることで変化が分かりやすくなります。timdifiltにセットする値を増やすとSWに対する反応が遅くなるため効果が分かりやくなります。

例えばtimdifiltに50をセットすると50×10msで1回DIの状態を確認し、4回一致してDI情報とするので2秒経過しないと応答しないフィルタになります。応答が遅すぎるのは問題なのでほどほどがよさそうです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include <MsTimer2.h>

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define LED_ONOFF 50
#define LED_ONOFF2 100
#define FILT_MIN 1

#define PIN_DO1 11
#define PIN_DI1 6
#define PIN_DI2 7
#define DI_FILT_MAX 4

enum DI_NO{
    DI1 = 0,
    DI2,
    DI_MAX
};

struct DIFILT_TYP{
    uint8_t wp;
    uint8_t buf[DI_MAX][DI_FILT_MAX];
    uint8_t di1;
    uint8_t di2;
};

// application use
int8_t timled = LED_ONOFF;
int8_t timled2 = TIME_OFF;
int8_t timdifilt = TIME_OFF;
int8_t cnt10ms;
bool btnflg1;
bool btnflg2;
DIFILT_TYP difilt;

/*** Local function prototypes */
void TimerCnt();
void mainTimer();
void DiFilter();

void setup() {
  
    pinMode( PIN_DO1, OUTPUT);
    pinMode( PIN_DI1, INPUT_PULLUP );
    pinMode( PIN_DI2, INPUT_PULLUP );
  
    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移
    MsTimer2::start();
    Serial.begin(115200);
    timdifilt = FILT_MIN;

    for( uint8_t i=0; i < 10; i++ ){
        mainTimer();
        DiFilter();
        delay(10);
    }
}

void loop() {
 
    mainTimer();
    DiFilter();

    if( timled == TIME_UP ){ //timled2と交互に使用してLEDをONOFFしている
        timled = TIME_OFF;
        timled2 = LED_ONOFF2; //LED_ONOFF2を長くするとON期間が長くなる
        digitalWrite(PIN_DO1, HIGH);
    }

    if( timled2 == TIME_UP ){ 
        timled2 = TIME_OFF;
        timled = LED_ONOFF; //LED_ONOFFを長くするとOFF期間が長くなる
        digitalWrite(PIN_DO1, LOW);
    }

    if(difilt.di1 == 0){
        if(btnflg1){
            btnflg1 = false;
            Serial.println("di1--ok");
        }
    }else{
        btnflg1 = true;
    }
  
    if(difilt.di2 == 0){
        if(btnflg2){
            btnflg2 = false;
            Serial.println("di2--ok");
        }
    }else{
        btnflg2 = true;
    }
}
/* callback function add */
void TimerCnt(){
    ++cnt10ms;
}
/* Timer Management function add */
void mainTimer(){

    if( cnt10ms >= BASE_CNT ){
        cnt10ms -=BASE_CNT;
        //10msごとにここに遷移する
        if( timled > TIME_UP ){
            timled--;
        }

        if( timled2 > TIME_UP ){
            timled2--;
        }

        if( timdifilt > TIME_UP ){
            timdifilt--;
        }
    }
}
/* DiFilter function add */
void DiFilter(){

    if( timdifilt == TIME_UP ){
        difilt.buf[DI1][difilt.wp] = digitalRead(PIN_DI1);
        difilt.buf[DI2][difilt.wp] = digitalRead(PIN_DI2);
    
        if( difilt.buf[DI1][0] == difilt.buf[DI1][1] &&
            difilt.buf[DI1][1] == difilt.buf[DI1][2] &&
            difilt.buf[DI1][2] == difilt.buf[DI1][3] ){ //4回一致を確認
            difilt.di1 = difilt.buf[DI1][0];
        }
    
        if( difilt.buf[DI2][0] == difilt.buf[DI2][1] &&
            difilt.buf[DI2][1] == difilt.buf[DI2][2] &&
            difilt.buf[DI2][2] == difilt.buf[DI2][3] ){ //4回一致を確認
            difilt.di2 = difilt.buf[DI2][0];
        }
        
        if( ++difilt.wp >= DI_FILT_MAX ){
            difilt.wp = 0;
        }

        timdifilt = FILT_MIN;
    }
}

メイン関数であるloop()に遷移する前にsetup()関数内でDIフィルタの値を確定するためにfor文でdelay(10)で遅延させながらDI値を確定しています。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

TECH::CAMPプログラミング教養【無料体験会】

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました