ESP32-WROOM-32EのタイマでDIフィルタを実装する

組み込みエンジニア

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

Arduino環境でDIの信号レベルを判定しているとスイッチの切り替えなどで発生するチャタリングのため誤動作の原因になることがあります。タイマライブラリを使用してDIのチャタリングの防止を行う方法をまとめました。

ESP32-WROOM-32Eの開発環境はArduino IDEを使用し、ESP32-WROOM-32E開発ボード(秋月電子)を使用しています。Arduino IDEで開発環境の作り方については下記記事を参考にしてください。

ESP32-WROOM-32Eの開発環境(Arduino IDE)を作り方

タイマを実装する

ESP32-WROOM-32Eの開発環境ではタイマに関するライブラリが標準で搭載されています。スケッチ例である「RepeatTimer」を引用しながらタイマの設定を行いDIフィルタを実装していきます。

タイマの設定

ESP32にはハードウェアタイマが4つあります。タイマ設定で任意の値をセットするとタイマカウントが更新されていき時間経過によって割り込みが発生します。

hw_timer_t *timer = NULL;

void setup() {

    //timerSemaphore = xSemaphoreCreateBinary(); //セマフォを作ってタイマ起動時に通知
    timer = timerBegin(0,80,true); //クロック80MHzを80で分周するので1usが最小分解能
    timerAttachInterrupt(timer, &onTimer,true);
    timerAlarmWrite(timer,10000,true);  // 10msごとにonTimer()がコール
    timerAlarmEnable(timer); //timer通知を開始
}

timerBegin()でタイマの初期化を行います。第1引数に使用するタイマ(0~3)を選択し、第2引数に動作クロックである80MHzに対する分周比を指定します。80を指定すると1usの分解能を持つタイマになります。第3引数はタイマカウントを増加(true)させるか減少(false)させるかを選択します。戻り値は他の設定で使用するためhw_timerの型ポインタの変数に戻り値をセットしています。

timerAttachInterrupt ()で割り込み関数を設定します。第1引数にtimerBegin()の戻り値をセットします。第2引数に割り込み処理を行う関数をセットします。第3引数に割り込みエッジをトリガ(true)にするかレベル(false)にするかを指定します。

timerAlarmWrite ()はタイマに値をセットします。第1引数にtimerBegin()の戻り値をセットします。第2引数にtimerBegin()の第2引数で設定した分周に対するカウント数を指定します。1usが分解能であった時10000を引数に指定すると10ms毎にタイマ割り込みが発生します。第3引数にtrueを指定すると割り込み発生時に第2引数の値が再設定されます。

timerAlarmEnable ()でタイマをスタートします。引数にtimerBegin()の戻り値を指定します。

割り込み関数を実装

void IRAM_ATTR onTimer(void){ //IRAMセクションに割り当てるとRAM領域となり高速となる

#ifdef SEMAPHORE_USE
    portENTER_CRITICAL_ISR(&timerMux);
    //メインと共用の変数がある場合はこの中で修正ーメイン側も同様にしてクリティカル内で更新する。
    portEXIT_CRITICAL_ISR(&timerMux);
    xSemaphoreGiveFromISR(timerSemaphore,NULL); //ISRにセマフォ(排他処理)を与えてタスクの割込みを防ぐ
#endif
    mainTimer(); //タイマ管理
}

タイマ割り込みでコールする関数としてonTimer()を実装します。voidの後ろに記述しているIRAM_ATTRはRAM領域に配置したい場合に指定します。指定しない場合はROM領域(フラッシュメモリ)に配置されます。

RAM領域に配置することでフラッシュメモリよりも高速に処理することができるため割り込み関数を少しでも高速にしたい場合に指定するとよいでしょう。

セマフォ使用時の例も記述していますがタイマ割り込みを一つしか使用しないのであれば指定する必要はありません。セマフォを利用するとタスク処理中に他のタスクがコールされることを防ぐ(排他処理)ことができます。

タイマ管理

mainTimer()の処理を行い各種タイマを管理します。

void mainTimer(void){

    if( timLedOnOff > TIME_UP ){
        --timLedOnOff;
    }
    if( timDifilter > TIME_UP ){
        --timDifilter;
    }
}

mainTimer()は10ms毎にコールされるためtimLedOnOff及びtimDifilterは10ms毎に更新されるタイマとなります。timLedOnOffに20をセットした場合は200msのタイマとなります。このようにタイマ変数に値をセットすることで任意のタイマを作ることができます。

チャタリング防止のDIフィルタを実装

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

DI信号のチャタリング

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

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

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

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

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

DIフィルタを実装する

タイマを利用することで10ms毎にDIの情報を確認し複数回一致したとき値を採用する方法でDIフィルタを実装します。

void DiFilter(void){
    bool boo = true;
    uint8_t i;

    if( timDifilter == TIME_UP ){
        timDifilter = TIM_DIFILT;

        diData.buf[diData.wp] = digitalRead(PIN_DI_SW); //DI情報を取得

        for( i=1; i < sizeof(diData.buf);i++){
            if( diData.buf[i - 1] != diData.buf[i]){
                boo = false; //一度でも不一致があればfalseとなる
            }
        }

        if(boo){ //データがすべて一致なので採用する
            diData.di1 = diData.buf[0];
        }
        if( ++diData.wp >= sizeof(diData.buf)){
            diData.wp = 0;
        }
    }
}

timDifilterがタイムアップするとDI情報を取得しDI情報を格納するバッファを更新します。全てのバッファの値が一致したときDI情報として採用するためチャタリングによる不安定な部分を取り除く事ができます。

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

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

動作確認

動作確認用の回路図
動作確認用の回路図

回路図の番号はESP32-WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

SWに接続しているDIはプルアップを使用する設定にしているためSWを押したときGNDと直接接続しても問題ありません。

電源を入れるとタイマがスタートしLED2を点灯/消灯します。SWを押すと変数を+1してシリアルモニタにSWが押された回数を表示してLED1を点灯します。LED1はSWが押された場合に点灯/消灯するようにしています。

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

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

SW押したときにシリアルモニタに押した回数が表示されていることを確認しました。また連動してLEDが点灯/消灯していることを確認しました。

ソースコード全体

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

#define PIN_DO_LED 13
#define PIN_DO_LDE2 12
#define PIN_DI_SW 5
#define TIME_UP 0
#define TIME_OFF -1
#define TIM_LED_ONOFF 50 //50*10 ms = 500ms
#define TIM_DIFILT 1
#define DIFILT_MAX 4
//#define SEMAPHORE_USE //セマフォを使い場合コメントを外す

typedef struct DIFILT{
    uint8_t wp;
    uint8_t buf[DIFILT_MAX];
    uint8_t di1;
};

/* 変数宣言 */
hw_timer_t *timer = NULL;
#ifdef SEMAPHORE_USE
volatile SemaphoreHandle_t timerSemaphore; //コンパイラに最適化されないようにする
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
#endif
uint32_t testcnt;
int16_t timLedOnOff = TIME_OFF;
int16_t timDifilter = TIME_OFF;
bool flg;
DIFILT diData;

/*  プロトタイプ宣言 */
void IRAM_ATTR onTimer(void);
void mainApp(void);
void mainTimer(void);
void DiFilter(void);

void setup() {
    uint8_t i;
  
    pinMode(PIN_DO_LED, OUTPUT);
    pinMode(PIN_DO_LDE2,OUTPUT);
    pinMode(PIN_DI_SW,INPUT_PULLUP);

    Serial.begin(115200);
    
    #ifdef SEMAPHORE_USE
    timerSemaphore = xSemaphoreCreateBinary(); //セマフォを作ってタイマ起動時に通知
    #endif
    
    timer = timerBegin(0,80,true); //クロック80MHzを80で分周するので1usが最小分解能
    timerAttachInterrupt(timer, &onTimer,true);
    timerAlarmWrite(timer,10000,true);  //コールバックするタイミングを指定 10msごとにonTimer()がコール
    timerAlarmEnable(timer); //timer通知を開始

    timLedOnOff = TIM_LED_ONOFF;
    timDifilter = TIM_DIFILT;
    while( i < 10){
        DiFilter();
        delay(10);
        i++;
    }
}

void loop() {
    mainApp();
    DiFilter();
}
/* タイマ割り込みによってコールする関数 */
void IRAM_ATTR onTimer(void){ //IRAMセクションに割り当てるとRAM領域となり高速となる

#ifdef SEMAPHORE_USE
    portENTER_CRITICAL_ISR(&timerMux);
    //メインと共用の変数がある場合はこの中で修正ーメイン側も同様にしてクリティカル内で更新する。
    portEXIT_CRITICAL_ISR(&timerMux);
    xSemaphoreGiveFromISR(timerSemaphore,NULL); //ISRにセマフォ(排他処理)を与えてタスクの割込みを防ぐ
#endif
    mainTimer();
}
/* メイン関数 */
void mainApp(void){

    if( timLedOnOff == TIME_UP){
        timLedOnOff = TIM_LED_ONOFF;
        digitalWrite(PIN_DO_LED,!(digitalRead(PIN_DO_LED)));  //Toggle LED13 Pin
    }

    if( diData.di1 == 0 ){
        if( flg == false ){
            flg = true;
            ++testcnt;
            Serial.println(testcnt);
            digitalWrite(PIN_DO_LDE2,!(digitalRead(PIN_DO_LDE2)));  
        }
    }
    else{
        flg = false;
    }
}
/* タイマ管理 */
void mainTimer(void){

    if( timLedOnOff > TIME_UP ){
        --timLedOnOff;
    }
    if( timDifilter > TIME_UP ){
        --timDifilter;
    }
}
/* DIフィルタ */
void DiFilter(void){
    bool boo = true;
    uint8_t i;

    if( timDifilter == TIME_UP ){
        timDifilter = TIM_DIFILT;

        diData.buf[diData.wp] = digitalRead(PIN_DI_SW);

        for( i=1; i < sizeof(diData.buf);i++){
            if( diData.buf[i - 1] != diData.buf[i]){
                boo = false;
            }
        }

        if(boo){ //データがすべて一致なので採用する
            diData.di1 = diData.buf[0];
        }
        if( ++diData.wp >= sizeof(diData.buf)){
            diData.wp = 0;
        }
    }
}

関連リンク

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

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

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

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

RUNTEQ-プログラミングで自由を手に入れる

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

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