ESP32でプルアップとGPIO割り込み(Interrupt)をテストする

Arduino/ESP32ESP32,IRAM_ATTR,volatile,チャタリング,プルアップ,割り込み

ちょこっとプルアップされた回路を操作する要件が出てきましたので、実際にどんな動きをするのか確認しておこうと思います。

まずはプルアップとはどんなものかを大雑把に。

プルアップとは

何も接続されていない状態の時に電位をHIGHレベルにしておく事

だそうですが、、

つまりGPIOに接続済で電圧をかけた状態で、何か別のものが接続されると電流が減ってLOWになる動きをするという、、?

回路図ではこんな感じでしょうか。

プルアップしたいGPIOに既定の電圧をかけて入力させておく事で、HIGHを保ちます。

一定以上の電圧がかかっていればHIGH、一定以下の電圧しかかかっていなければLOWという感じですが、マイコンによってこの規定は異なる様です。

ESP32の場合には内部動作電圧が3.3Vですので、最大3.3Vまでの入力で上位75%(2.475V)以上がHIGH、下位25%(0.825V)以下がLOWとなっている様です。

この間の0.825V~2.476Vの電圧の場合はどちらになるか「わからない」のだそうで、、、

というわけでESP32の場合、2.475V以上をGPIOにかけておけば、プルアップ状態という事に。

そして、マイコンの内部抵抗よりも十分に低い接続をすると、マイコンにかかる電圧が非常に小さくなるためLOWになるという仕組みです。

プルアップされた回路をLOWにする場合には、余計な負荷を挟まずガツンとGNDに落としてやるのがセオリーっぽいです。

この動作により、マイコンにお知らせできるという事らしい。

尚、何も接続しない状態の電線は色々な電波や摩擦等で帯電してしまいますので、プルアップやプルダウン(ここでは省略)を行わないでマイコンに待ち受けさせると、ほぼ誤動作すると思います。

電流を流しておくと、静電気や電波等の外部からの干渉に強くなるらしいです。

どのくらい強くなるのか知りませぬが・・・

実際にプルアップをLOWにしてみる

さて、実際にESP32にかかる電圧をLOWにして知らせてみます。

回路のスイッチにはタクトスイッチ(押しボタン)を配置してLOWにしてみます。

こんな感じの4つ足のボタンですが、足が向いている方向は導通しているらしいのでご注意ください。

これらをチャチャっと配置して、、

ブレッドボードはこんな感じに。

なんだかゴチャゴチャしてますね、、、

回路図ではスッキリしたものなんですけれども

尚、タクトスイッチをブレッドボードに挿す時、あまり足を真っ直ぐにし過ぎるとブレッドボードからビョン!って弾かれますので、、、

ある程度ウネウネしていた方がいいです。

簡単な回路ですのでスケッチもこんな感じで簡単に。

#include <Arduino.h>

uint8_t gpio_pin1 = 32;
uint8_t gpio_pin2 = 26;

void setup()
{
    // シリアル通信を開始
    Serial.begin(115200);

    // GPIOピンモードを設定
    pinMode(gpio_pin1, INPUT);

    // LEDを光らせるピン
    pinMode(gpio_pin2, OUTPUT);
    digitalWrite(gpio_pin2, LOW);
}

void loop()
{
    if(digitalRead(gpio_pin1) == LOW) { // HIGH = 0x0
        digitalWrite(gpio_pin2, HIGH);
        delay(3000);
        digitalWrite(gpio_pin2, LOW);
    }
    // 0.1秒毎にポーリング
    delay(100);
}

ボタンを押すと、LEDが3秒光ります。

今回はdigitalReadを使ったポーリングでGPIOの状態を取得する様にしました。

さて、、、

予想通りというかなんというか。

一応ちゃんとLOWである事を検出してくれた様です。

しかしボタンだけ待ち受けるのも何だかアレですし、次は割り込み(Interrupt)処理を試してみます。

ESP32のGPIO割り込み(Interrupt)処理で検知する

メインループでGPIOの状態を直接ポーリングすると、タイミングによってはボタンが押されたかどうかを検知できません。

GPIO割り込み処理を使えば、目的のGPIOピンの状態が変わった時にあらかじめ登録しておいた関数を実行してくれますので、確実にボタンが押されたかどうかを検知できます。

たぶん。

メソッドはattachInterruptを使います。

ソースはこんな感じで。

#include <Arduino.h>

uint8_t gpio_pin1 = 32; //  GPIO 32 プルアップピン
uint8_t gpio_pin2 = 26; //  GPIO 26 LED点灯用出力ピン

volatile bool flag_button_pushed = false; //    排他用処理中フラグ
volatile ulong time_button_pushed = 0;    //    操作時刻保存用

// 割り込み処理 ボタンを検知
void IRAM_ATTR button_pushed()
{
    // ボタンが押されていない場合にしか処理しない
    if (!flag_button_pushed)
    {
        // 押されたフラグを保存
        flag_button_pushed = true;
        // 押された時刻を保存
        time_button_pushed = millis();
    }
}

void setup()
{
    // シリアル通信を開始
    Serial.begin(115200);
    delay(50);

    // GPIOピンモードを設定
    pinMode(gpio_pin1, INPUT);

    // LEDを光らせる為の出力ピン
    pinMode(gpio_pin2, OUTPUT);
    digitalWrite(gpio_pin2, LOW);

    // 押されたフラグを初期化
    flag_button_pushed = false;
    // 押された時刻を初期化
    time_button_pushed = millis();

    // 割り込みを登録 トリガはLOWになった時
    attachInterrupt(gpio_pin1, button_pushed, FALLING);
}

void loop()
{
    // 状態取得
    if (flag_button_pushed)
    {
        //  LEDをトグルさせる
        if ((digitalRead(gpio_pin2) == LOW))
        {
            digitalWrite(gpio_pin2, HIGH);
        }
        else
        {
            digitalWrite(gpio_pin2, LOW);
        }

        // 押しっぱなしの時は無限ループで待つ
        while (digitalRead(gpio_pin1) == LOW)
        {
            delay(10);
        }
        // フラグリセット
        flag_button_pushed = false;
    }

    // 一定時間以上経過していたらフラグをリセット
    //  フラグ状態がおかしくなった時用のトラップ
    if((millis() - time_button_pushed) > 100000) {
        // フラグリセット
        flag_button_pushed = false;
        // 押された時刻をリセット
        time_button_pushed = millis();
    }

    // 1ms間隔
    delay(1);
}

なにやら見慣れない接頭語と属性が出てきました。

volatile

volatile bool flag_button_pushed = false; //   排他用処理中フラグ
volatile ulong time_button_pushed = 0;    //    操作時刻保存用

これは、コンパイラに確実にRAMに乗っけておけよ!っていう指示だそうで。

これを指定しておかないと、コンパイラがメモリ節約のために変数を不揮発性メモリに置いて随時読み書きする様な変換を行う恐れがあるんだとか。

リアルタイム性の求められる割り込み処理で操作する変数は、オンメモリーで処理しないと間に合わない事もあるらしいので、volatileを付けておくんだそうです。

IRAM_ATTR

// 割り込み処理 ボタンを検知
void IRAM_ATTR button_pushed()
{
    // ボタンが押されていない場合にしか処理しない
    if (!flag_button_pushed)
    {
        // 押されたフラグを保存
        flag_button_pushed = true;  // volatile指定の変数だけを操作
        // 押された時刻を保存
        time_button_pushed = millis();  // volatile指定の変数だけを操作
    }
}

割り込み処理にフックする関数ですよと指定する属性だそうです。

これを指定すると関数がIRAMに配置されるという事らしい。

IRAMに配置しておかないと、割込み発生時に割り込み処理関数(ISR)が見つからないという事らしい。

ロジックについて

割り込み中の処理は、押されたかどうかを拾ってフラグを操作するだけにします。

物理ボタンスイッチを使っているので、チャタリングはどうするの?という疑問については、、

ここでは、割り込み処理でフラグをONにするだけという一方通行の動作をさせるので、いくらチャタリングが起きようが無視出来てしまいます。

■割り込み処理側
・ボタンが押されたら、押されたフラグを立てる
・ボタンフラグが既に立っていたら、割り込み処理では何もしない

■メインループ側
・ボタンフラグ監視処理(ここではメインループを使ってしまっていますが)でフラグに応じた処理を行う
・処理が終わったらフラグをフリーにする

こんな感じで・・・

どうやら割り込み中にはディレイ等の操作が効かないらしいので、チャタリングの対策を割り込み処理に入れ込むのは難しそうでした。

また、割り込み処理をシステムでも多用しているっぽくて、割込み処理に長い時間をかけるとシステムの割り込み処理全体に影響を及ぼす可能性があるので推奨されません。

例えば、ボタンを押しっぱなしにした時のwhileループを割り込み処理中に挟んだりしたら、恐らくwatchdogタイマーにやっつけられてしまうと思います。

今回は、メインループでボタンが押された時の動作に関する処理をやってしまっていますが、ボタン等の操作をポーリングするループを別タスク等に分けて、フラグ状態のポーリングを専門に高速に動作させておけば、かなりのレベルまで即応性も担保されると思います。

チャタリングの対策も履修しておくべき所だとは思いますが、ブレッドボードの品質が悪く膨大なチャタリングが発生しそうですので、ここではあえてチャタリングを無視する設計になっています。

ボタンが押された事だけ拾えればいいので、、、

こんなで多分大丈夫だと思いますが、、、

実際にちゃんと動作するか実食確認します。

まぁまぁですかね。

想定通りの動作で安心しました。

ただ回路があまり見栄えも良くないですぬ

ESP32 内臓プルアップを使う

さて、ゴチャっとしたこの回路。

ESP32の内臓プルアップを使えばだいぶスッキリいたします。

5Vから3.3Vを作ってプルアップする部分を、ゴッソリとESP32マイコンにお任せです。

こうしてしまえばブレッドボードもほらこんなにスッキリ

・・・・

あんまり変わって無いって・・・?

さてスケッチは、先ほどのソースのGPIO設定をINPUT_PULLUPにするだけです。

#include <Arduino.h>

uint8_t gpio_pin1 = 32; //  GPIO 32 プルアップピン
uint8_t gpio_pin2 = 26; //  GPIO 26 LED点灯用出力ピン

volatile bool flag_button_pushed = false; //    排他用処理中フラグ
volatile ulong time_button_pushed = 0;    //    操作時刻保存用

// 割り込み処理 ボタンを検知
void IRAM_ATTR button_pushed()
{
    // ボタンが押されていない場合にしか処理しない
    if (!flag_button_pushed)
    {
        // 押されたフラグを保存
        flag_button_pushed = true;
        // 押された時刻を保存
        time_button_pushed = millis();
    }
}

void setup()
{
    // シリアル通信を開始
    Serial.begin(115200);
    delay(50);

    // GPIOピンモードを設定 内臓プルアップ
    pinMode(gpio_pin1, INPUT_PULLUP);

    // LEDを光らせる為の出力ピン
    pinMode(gpio_pin2, OUTPUT);
    digitalWrite(gpio_pin2, LOW);

    // 押されたフラグを初期化
    flag_button_pushed = false;
    // 押された時刻を初期化
    time_button_pushed = millis();

    // 割り込みを登録 トリガはLOWになった時
    attachInterrupt(gpio_pin1, button_pushed, FALLING);
}

void loop()
{
    // 状態取得
    if (flag_button_pushed)
    {
        //      LEDをトグルさせる
        if ((digitalRead(gpio_pin2) == LOW))
        {
            digitalWrite(gpio_pin2, HIGH);
        }
        else
        {
            digitalWrite(gpio_pin2, LOW);
        }

        // 押しっぱなしの時は無限ループで待つ
        while (digitalRead(gpio_pin1) == LOW)
        {
            delay(10);
        }
        // フラグリセット
        flag_button_pushed = false;
    }

    // 一定時間以上経過していたらフラグをリセット
    //  フラグ状態がおかしくなった時用のトラップ
    if((millis() - time_button_pushed) > 100000) {
        // フラグリセット
        flag_button_pushed = false;
        // 押された時刻をリセット
        time_button_pushed = millis();
    }

    // 1ms間隔
    delay(1);
}

これで動かしてみると、こころなしかボタンの反応が良くなった様な、、、

ブレッドボード上の分圧で作った3.3Vに比べて正確さが増したという事なのでしょうか。

さて、なんとなく割り込み処理とプルアップの使い方をなぞってみました。

割り込み処理はもう少し何とかしたいところですね。

なんだかFreeRTOSにはキューやら優先度やらの機能もあるらしいので、クリティカルな動作を作るならそっちを掘り下げて使って行くべきなのでしょう。

時間があれば公式リファレンスを掘ってみます。