ESP32 ソフトウェア 回路設計

ESP32 GPIO割り込み完全ガイド|GPIO割り込みの基本と使い方を現役エンジニアが解説

投稿日:

ESP32は、Wi-FiやBluetoothだけでなく、多数のGPIO(General Purpose Input/Output)を備えた高機能マイコンです。

GPIOはLEDの点灯やスイッチ入力など、組み込み開発で最も基本となる機能ですが、単純な入力の読み取りだけではありません。

GPIO割り込みを利用することで、ボタンが押された瞬間やセンサーの状態が変化した瞬間に、CPUへ通知して処理を開始できます。

ESP32で開発を始めると、

  • GPIO割り込みとは何?
  • ポーリングと何が違うの?
  • 割り込みでは何をしてはいけないの?
  • ESP-IDFではどのように実装するの?

といった疑問を持つ方も多いでしょう。

この記事では、GPIO割り込みの基本からESP32で利用するメリット、実務で意識している設計ポイントまで、現役エンジニアの視点で分かりやすく解説します。

GPIO割り込みとは?

GPIO割り込みとは、GPIOの状態が変化したことをきっかけに、CPUへ通知を行う仕組みです。

例えば、タクトスイッチを押した瞬間や離した瞬間に割り込みを発生させることで、CPUは必要なタイミングだけ処理を実行できます。

これに対し、一定周期でGPIOの状態を読み取り続ける方法をポーリングと呼びます。

ポーリングとの違い

GPIO入力を監視する方法には、大きく分けて次の2つがあります。

ポーリング

一定周期でGPIOの状態を読み取ります。

例えば、

10msごとにスイッチ状態を確認

というような方法です。

実装は簡単ですが、

  • CPUを定期的に使用する
  • 応答速度は監視周期に依存する

という特徴があります。

GPIO割り込み

GPIOの状態が変化した瞬間だけCPUへ通知します。

例えば、

  • ボタンが押された
  • ボタンが離された
  • センサーが反応した

というイベントが発生した瞬間だけ処理が開始されます。

CPUは待機しているだけなので、

  • CPU負荷が少ない
  • 応答が速い

というメリットがあります。

GPIO割り込みが使われる場面

ESP32では、次のような用途でGPIO割り込みが利用されています。

  • タクトスイッチ
  • ロータリーエンコーダ
  • 人感センサー
  • ドアセンサー
  • リミットスイッチ
  • パルス入力
  • 外部ICからの割り込み通知

IoT機器では、「状態が変わったときだけ処理を実行したい」というケースが多く、GPIO割り込みは非常によく利用されます。

エッジ検出とは?

GPIO割り込みでは、「どのような変化で割り込みを発生させるか」を設定できます。

代表的なものは次の4種類です。

立ち上がりエッジ(Positive Edge)

LowからHighへ変化した瞬間に割り込みが発生します。

立ち下がりエッジ(Negative Edge)

HighからLowへ変化した瞬間に割り込みが発生します。

タクトスイッチをプルアップ接続した場合は、この設定がよく利用されます。

両エッジ(Any Edge)

High→Low、Low→Highの両方で割り込みが発生します。

ロータリーエンコーダなどで利用されることがあります。

Lowレベル/Highレベル

GPIOが一定レベルになっている間、割り込み条件として扱います。

イベント検出ではなく状態監視向けです。

ESP32でGPIO割り込みを使用するメリット

ESP32では、ESP-IDFがGPIO割り込み用のドライバを提供しています。

そのため、複雑なレジスタ操作を行うことなく、比較的簡単に割り込み処理を実装できます。

主なメリットは次のとおりです。

  • CPU負荷を低減できる
  • イベント発生時だけ処理を実行できる
  • 応答性が高い
  • ESP-IDF APIで簡単に実装できる
  • FreeRTOSとの組み合わせが容易

GPIO割り込みとFreeRTOS

実務では、GPIO割り込みの中で長時間の処理を実行することはほとんどありません。

一般的には、

  1. GPIO割り込みが発生する
  2. FreeRTOSタスクへ通知する
  3. タスク側で処理を実行する

という構成を採用します。

この方法なら、

  • 割り込み処理を短時間で終了できる
  • 他の割り込みへの影響を抑えられる
  • システム全体の応答性を維持できる

というメリットがあります。

今回使用する開発環境

この記事では、次の環境でGPIO割り込みの動作を確認します。

  • ESP-IDF:v5.5
  • ターゲット:ESP32-C5
  • 開発環境:Visual Studio Code
  • 入力:タクトスイッチ
  • 出力:LED

今回は、タクトスイッチを押すたびにGPIO割り込みを発生させ、FreeRTOSタスクへ通知します。

通知を受け取ったタスクがLEDをON/OFFすることで、安全な割り込み処理の実装方法を学びます。

接続回路

今回は、GPIO2へタクトスイッチ、GPIO8へLEDを接続します。

              +3.3V
                │
        │
              10kΩ
                │
        │ 
GPIO2──────────┼─────SW──────GND

GPIO8────330Ω────LED────GND

通常時はGPIO2がHighになり、スイッチを押すとLowになります。

そのため今回は、

立ち下がりエッジ(High→Low)

で割り込みを発生させます。

CMakeLists.txt

idf_component_register(
    SRCS "main.c"
    INCLUDE_DIRS "."
    REQUIRES esp_driver_gpio
)

driver/gpio.hを使用するため、esp_driver_gpioへの依存を指定します。

サンプルコード

今回は、ESP-IDF v5.5でビルド可能な完全版を掲載します。

#include <stdbool.h>
#include <stdint.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "driver/gpio.h"

#include "esp_err.h"
#include "esp_log.h"


/*
 * 使用するGPIO番号です。
 *
 * 使用するESP32-C5開発ボードの回路に合わせて
 * 適宜変更してください。
 */
#define SWITCH_GPIO              GPIO_NUM_2
#define LED_GPIO                 GPIO_NUM_8

/*
 * スイッチのチャタリングを除去するため、
 * 割り込み通知後に一定時間待ってから
 * GPIOレベルを確認します。
 */
#define DEBOUNCE_TIME_MS         30

/*
 * 短時間に複数回入力が確定することを防ぐための
 * 最小受付間隔です。
 */
#define MIN_PRESS_INTERVAL_MS    200


static const char *TAG = "GPIO_INTERRUPT";

/*
 * GPIO割り込みから通知を受け取る
 * タスクのハンドルです。
 */
static TaskHandle_t s_gpio_task_handle = NULL;


/**
 * GPIO割り込みハンドラです。
 *
 * 割り込みコンテキストでは重い処理を行わず、
 * FreeRTOSタスクへ通知するだけにします。
 */
static void IRAM_ATTR switch_gpio_isr_handler(
    void *argument)
{
    TaskHandle_t task_handle =
        (TaskHandle_t)argument;

    BaseType_t higher_priority_task_woken =
        pdFALSE;

    if (task_handle != NULL) {
        vTaskNotifyGiveFromISR(
            task_handle,
            &higher_priority_task_woken);
    }

    /*
     * 割り込みによって、より優先度の高いタスクが
     * 実行可能になった場合はコンテキストを切り替えます。
     */
    if (higher_priority_task_woken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
}


/**
 * LEDを接続したGPIOを初期化します。
 */
static void led_gpio_initialize(void)
{
    const gpio_config_t led_config = {
        .pin_bit_mask = (1ULL << LED_GPIO),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };

    ESP_ERROR_CHECK(
        gpio_config(&led_config));

    /*
     * 起動時はLEDを消灯します。
     */
    ESP_ERROR_CHECK(
        gpio_set_level(LED_GPIO, 0));
}


/**
 * スイッチを接続したGPIOを初期化します。
 */
static void switch_gpio_initialize(
    TaskHandle_t notification_task)
{
    /*
     * 回路図では外付け10kΩのプルアップ抵抗を
     * 使用するため、内部プルアップは無効にします。
     *
     * スイッチを押すとHighからLowへ変化するため、
     * 立ち下がりエッジ割り込みを設定します。
     */
    const gpio_config_t switch_config = {
        .pin_bit_mask = (1ULL << SWITCH_GPIO),
        .mode = GPIO_MODE_INPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_NEGEDGE,
    };

    ESP_ERROR_CHECK(
        gpio_config(&switch_config));

    /*
     * GPIOドライバのISRサービスをインストールします。
     */
    ESP_ERROR_CHECK(
        gpio_install_isr_service(0));

    /*
     * GPIO2専用の割り込みハンドラを登録します。
     *
     * 第3引数には、割り込みから通知する
     * タスクのハンドルを渡します。
     */
    ESP_ERROR_CHECK(
        gpio_isr_handler_add(
            SWITCH_GPIO,
            switch_gpio_isr_handler,
            notification_task));
}


/**
 * GPIO割り込み通知を処理するタスクです。
 */
static void gpio_interrupt_task(
    void *argument)
{
    (void)argument;

    bool led_state = false;

    TickType_t last_accepted_press_tick = 0;

    ESP_LOGI(
        TAG,
        "GPIO interrupt task started");

    while (1) {
        /*
         * GPIO割り込みハンドラから通知されるまで
         * タスクをブロックします。
         *
         * pdTRUEを指定すると、蓄積している通知数を
         * 受信時に0へクリアします。
         */
        ulTaskNotifyTake(
            pdTRUE,
            portMAX_DELAY);

        /*
         * 機械式スイッチのチャタリングが収まるまで
         * 一定時間待機します。
         */
        vTaskDelay(
            pdMS_TO_TICKS(DEBOUNCE_TIME_MS));

        /*
         * 待機後もGPIOがLowであれば、
         * スイッチが押されたと判断します。
         */
        int switch_level =
            gpio_get_level(SWITCH_GPIO);

        if (switch_level != 0) {
            ESP_LOGD(
                TAG,
                "Ignored transient switch input");

            continue;
        }

        TickType_t current_tick =
            xTaskGetTickCount();

        /*
         * 前回の有効入力から一定時間が経過していない場合は、
         * チャタリングや連続入力として無視します。
         */
        if ((last_accepted_press_tick != 0) &&
            ((current_tick - last_accepted_press_tick) <
             pdMS_TO_TICKS(MIN_PRESS_INTERVAL_MS))) {

            ESP_LOGD(
                TAG,
                "Ignored input inside debounce interval");

            continue;
        }

        last_accepted_press_tick = current_tick;

        /*
         * LEDの状態を反転します。
         */
        led_state = !led_state;

        ESP_ERROR_CHECK(
            gpio_set_level(
                LED_GPIO,
                led_state ? 1 : 0));

        ESP_LOGI(
            TAG,
            "Switch pressed: LED %s",
            led_state ? "ON" : "OFF");
    }
}


void app_main(void)
{
    /*
     * LED出力を初期化します。
     */
    led_gpio_initialize();

    /*
     * GPIO割り込みから通知を受け取る
     * 専用タスクを生成します。
     */
    BaseType_t task_result = xTaskCreate(
        gpio_interrupt_task,
        "gpio_interrupt_task",
        3072,
        NULL,
        10,
        &s_gpio_task_handle);

    if (task_result != pdPASS) {
        ESP_LOGE(
            TAG,
            "Failed to create GPIO interrupt task");

        return;
    }

    /*
     * タスク生成後、そのハンドルをGPIO割り込みの
     * user argumentとして登録します。
     */
    switch_gpio_initialize(
        s_gpio_task_handle);

    ESP_LOGI(
        TAG,
        "GPIO interrupt sample started");

    ESP_LOGI(
        TAG,
        "Switch GPIO : %d",
        SWITCH_GPIO);

    ESP_LOGI(
        TAG,
        "LED GPIO    : %d",
        LED_GPIO);

    ESP_LOGI(
        TAG,
        "Press the switch to toggle the LED");
}

サンプルコードのポイント

LED用GPIOを初期化する

最初に、LEDを接続したGPIO8を出力として設定します。

const gpio_config_t led_config = {
 .pin_bit_mask = (1ULL << LED_GPIO),
 .mode = GPIO_MODE_OUTPUT,
 .pull_up_en = GPIO_PULLUP_DISABLE,
 .pull_down_en = GPIO_PULLDOWN_DISABLE,
 .intr_type = GPIO_INTR_DISABLE,
 };

pin_bit_maskには、設定するGPIOをビットマスク形式で指定します。

今回はGPIO8を使用するため、1ULL << LED_GPIOを設定しています。

その後、gpio_config()で設定を反映します。

ESP_ERROR_CHECK( gpio_config(&led_config));

起動直後にLEDが点灯しないよう、初期出力はLowへ設定します。

ESP_ERROR_CHECK( gpio_set_level(LED_GPIO, 0));

GPIO割り込みを処理するタスクを生成する

今回のサンプルでは、割り込みハンドラ内でLEDを直接制御しません。

GPIO割り込みを処理する専用タスクを、xTaskCreate()で生成します。

BaseType_t task_result = xTaskCreate( gpio_interrupt_task, "gpio_interrupt_task", 3072, NULL, 10, &s_gpio_task_handle);

各引数の役割は次のとおりです。

  • gpio_interrupt_task:タスクとして実行する関数
  • "gpio_interrupt_task":タスク名
  • 3072:タスクのスタックサイズ
  • NULL:タスクへ渡す引数
  • 10:タスク優先度
  • &s_gpio_task_handle:生成したタスクのハンドル

取得したタスクハンドルは、GPIO割り込みハンドラから通知を送るために使用します。

スイッチ入力用GPIOを設定する

タクトスイッチを接続したGPIO2を、入力として設定します。

const gpio_config_t switch_config = {
 .pin_bit_mask = (1ULL << SWITCH_GPIO),
 .mode = GPIO_MODE_INPUT,
 .pull_up_en = GPIO_PULLUP_DISABLE,
 .pull_down_en = GPIO_PULLDOWN_DISABLE,
 .intr_type = GPIO_INTR_NEGEDGE,
 };

今回の回路では、GPIO2を外付け10kΩ抵抗で3.3Vへプルアップしています。

通常時はHighで、スイッチを押すとGNDへ接続されてLowになります。

そのため、割り込み条件には次の設定を使用します。

.intr_type = GPIO_INTR_NEGEDGE

GPIO_INTR_NEGEDGEは、HighからLowへ変化する立ち下がりエッジで割り込みを発生させる設定です。

外付けプルアップ抵抗を使用しているため、ESP32-C5内部のプルアップ抵抗は無効にしています。

GPIO割り込みサービスをインストールする

GPIOごとに割り込みハンドラを登録するため、GPIOドライバのISRサービスをインストールします。

ESP_ERROR_CHECK( gpio_install_isr_service(0));

引数には、割り込みメモリの割り当て条件を指定できます。

今回のサンプルでは特別な条件を指定せず、0を使用しています。

ISRサービスをインストールすると、各GPIOへ個別の割り込みハンドラを登録できるようになります。

GPIO2専用の割り込みハンドラを登録する

次に、GPIO2で割り込みが発生したときに呼び出す関数を登録します。

ESP_ERROR_CHECK( gpio_isr_handler_add( SWITCH_GPIO, switch_gpio_isr_handler, notification_task));

各引数の役割は次のとおりです。

  • SWITCH_GPIO:割り込みを監視するGPIO
  • switch_gpio_isr_handler:呼び出す割り込みハンドラ
  • notification_task:割り込みハンドラへ渡すユーザー情報

今回のサンプルでは、ユーザー情報として通知先タスクのハンドルを渡しています。

gpio_isr_handler_add()を使用する前には、gpio_install_isr_service()を実行しておく必要があります。

割り込みハンドラからタスクへ通知する

GPIO2で立ち下がりエッジが検出されると、次の割り込みハンドラが実行されます。

static void IRAM_ATTR switch_gpio_isr_handler( void *argument) {
   TaskHandle_t task_handle = (TaskHandle_t)argument;
   BaseType_t higher_priority_task_woken = pdFALSE;
      if (task_handle != NULL) {
          vTaskNotifyGiveFromISR( task_handle, &higher_priority_task_woken);
      } 

      if (higher_priority_task_woken == pdTRUE) {
          portYIELD_FROM_ISR();
      } 
}

割り込みハンドラでは、LED制御やログ出力を行わず、vTaskNotifyGiveFromISR()でタスクへ通知するだけにしています。

割り込みハンドラは通常のタスクとは異なる割り込みコンテキストで実行されるため、処理はできるだけ短時間で終了させることが重要です。

割り込み内では、次のような処理を避けます。

  • 長時間かかる演算
  • 待ち時間が発生する処理
  • 通常のvTaskDelay()
  • 動的メモリの確保
  • 大量のログ出力
  • ブロックする可能性があるAPI

実際の処理は、通知を受け取ったFreeRTOSタスク側で行います。

必要に応じてタスクを即座に切り替える

vTaskNotifyGiveFromISR()によって、現在実行中のタスクより優先度が高いタスクが実行可能になる場合があります。

その結果は、higher_priority_task_wokenへ格納されます。

if (higher_priority_task_woken == pdTRUE) {
    portYIELD_FROM_ISR();
}

pdTRUEの場合にportYIELD_FROM_ISR()を呼び出すことで、割り込み終了後、通知先タスクへ速やかに処理を切り替えられます。

タスク側で割り込み通知を待つ

GPIO割り込み処理用タスクでは、次の関数を使って通知を待ちます。

ulTaskNotifyTake( pdTRUE, portMAX_DELAY);

portMAX_DELAYを指定しているため、GPIO割り込みから通知が届くまでタスクはブロック状態になります。

待機中はCPUを継続的に使用しないため、GPIO状態を繰り返し読み取るポーリング処理よりも効率的です。

第1引数のpdTRUEは、通知を受信したときに蓄積されている通知カウントを0へ戻す設定です。

タクトスイッチのチャタリングによって短時間に複数の割り込みが発生しても、一度の処理で通知をまとめてクリアできます。

チャタリングが収まるまで待機する

機械式のタクトスイッチは、押した瞬間に信号が一度だけ変化するとは限りません。

接点が短時間に何度も接触と非接触を繰り返す現象を、チャタリングと呼びます。

今回のサンプルでは、割り込み通知を受け取った後、30ms待機します。

vTaskDelay( pdMS_TO_TICKS(DEBOUNCE_TIME_MS));

待機後にGPIOレベルを再確認することで、瞬間的な信号変化を除外します。

int switch_level = gpio_get_level(SWITCH_GPIO);

if (switch_level != 0) {
    continue;
}

30ms後もGPIO2がLowであれば、スイッチが実際に押されていると判断します。

短時間の連続入力を無視する

30msの待機だけでは、スイッチの種類や操作方法によって複数回の入力として判定される場合があります。

そこで今回のサンプルでは、前回の有効な入力から200ms以内に発生した入力を無視します。

TickType_t current_tick = xTaskGetTickCount();

現在のFreeRTOS Tick値を取得し、前回受け付けた時刻と比較します。

if ((last_accepted_press_tick != 0) && ((current_tick - last_accepted_press_tick) < pdMS_TO_TICKS(MIN_PRESS_INTERVAL_MS)))
{ 
    continue; 
}

200ms以上経過していれば、今回の入力を有効なスイッチ押下として処理します。

last_accepted_press_tick = current_tick;

この処理によって、チャタリングによる複数回のLED反転を抑えています。

LEDの状態を反転する

有効なスイッチ押下と判断した場合、LEDの状態を反転します。

led_state = !led_state;

その後、GPIO8へ新しい出力状態を設定します。

ESP_ERROR_CHECK( gpio_set_level( LED_GPIO, led_state ? 1 : 0));

スイッチを押すたびに、

  • 消灯中なら点灯
  • 点灯中なら消灯

へ切り替わります。

ログ出力も通常のタスク側で実行します。

ESP_LOGI( TAG, "Switch pressed: LED %s", led_state ? "ON" : "OFF");

実行結果

GPIO2へタクトスイッチ、GPIO8へLEDを接続し、プログラムを実行します。

起動時には、次のようなログが表示されます。

I (310) GPIO_INTERRUPT: GPIO interrupt task started 
I (320) GPIO_INTERRUPT: GPIO interrupt sample started 
I (320) GPIO_INTERRUPT: Switch GPIO : 2 
I (320) GPIO_INTERRUPT: LED GPIO : 8 
I (320) GPIO_INTERRUPT: Press the switch to toggle the LED

タクトスイッチを押すと、LEDの状態が反転し、次のようなログが表示されます。

I (2340) GPIO_INTERRUPT: Switch pressed: LED ON 
I (4150) GPIO_INTERRUPT: Switch pressed: LED OFF 
I (6080) GPIO_INTERRUPT: Switch pressed: LED ON 
I (7930) GPIO_INTERRUPT: Switch pressed: LED OFF

スイッチを1回押すごとにLEDが1回だけ切り替われば、GPIO割り込みとデバウンス処理は正常に動作しています。

実際の記事へ掲載する際は、可能であれば実機で取得したログへ差し替えてください。

GPIO割り込み利用時によくあるトラブル

1.スイッチを1回押しただけでLEDが何度も切り替わる

機械式スイッチのチャタリングが原因として考えられます。

今回のサンプルでは、

  • 割り込み後に30ms待機
  • GPIOレベルを再確認
  • 前回入力から200ms以内の入力を無視

という方法で対策しています。

スイッチの種類や回路によって最適な待機時間は異なります。

必要に応じて、次の値を調整してください。

#define DEBOUNCE_TIME_MS      30
#define MIN_PRESS_INTERVAL_MS 200

より確実な対策が必要な場合は、RCフィルタやシュミットトリガなどを使ったハードウェアデバウンスも検討します。

2.割り込みが発生しない

次の項目を確認してください。

  • GPIO2が入力として設定されているか
  • スイッチ押下時にGPIO2がLowになるか
  • 外付けプルアップ抵抗が接続されているか
  • GPIO_INTR_NEGEDGEが設定されているか
  • gpio_install_isr_service()を実行しているか
  • gpio_isr_handler_add()でハンドラを登録しているか
  • GPIO番号が実際の配線と一致しているか

テスターやオシロスコープを使用し、スイッチを押したときにGPIO2の電圧が3.3Vから0Vへ変化していることも確認しましょう。

3.起動直後に割り込みが発生する

入力GPIOが不安定な状態になっている可能性があります。

外付けプルアップ抵抗の接続や抵抗値を確認してください。

プルアップされていない入力GPIOは、周囲のノイズによってHighとLowの間を不安定に変化することがあります。

内部プルアップを使用する場合は、次のように変更できます。

.pull_up_en = GPIO_PULLUP_ENABLE,

この場合、外付けプルアップ抵抗を使用する回路説明との整合性に注意してください。

4.gpio_install_isr_service()でエラーになる

アプリケーション内の別の場所ですでにGPIO ISRサービスをインストールしている可能性があります。

gpio_install_isr_service()は、通常はアプリケーション全体で一度だけ実行します。

複数のコンポーネントからGPIO割り込みを使用する場合は、ISRサービスをどこで初期化するかを統一してください。

すでにインストール済みの場合、ESP_ERR_INVALID_STATEが返ることがあります。

5.割り込みハンドラ内でエラーやリセットが発生する

ISR内で、割り込みコンテキストに対応していないAPIを呼び出している可能性があります。

ISRからFreeRTOS機能を使用する場合は、原則として末尾にFromISRが付いたAPIを使用します。

今回のサンプルでは、次のAPIを使用しています。

vTaskNotifyGiveFromISR()

通常タスク用のAPIをISR内から呼び出さないよう注意してください。

6.スイッチを長押しすると複数回反応する

今回のサンプルは、立ち下がりエッジだけを検出しています。

スイッチを押した瞬間に一度割り込みが発生するため、正常な回路では長押し中に繰り返し反応しません。

長押し中に複数回反応する場合は、次の原因が考えられます。

  • 接点が不安定
  • 配線が長くノイズを拾っている
  • プルアップ抵抗が適切でない
  • スイッチ入力へ外来ノイズが加わっている

必要に応じて、入力端子へコンデンサを追加するハードウェア対策も検討します。

7.LEDが点灯しない

次の項目を確認してください。

  • LEDの向きが正しいか
  • 330Ωの電流制限抵抗が接続されているか
  • LEDのカソードがGNDへ接続されているか
  • GPIO8が別の機能と競合していないか
  • コード内のGPIO番号と実際の配線が一致しているか

GPIO2とGPIO8は今回のサンプルで使用する例です。

使用するESP32-C5開発ボードの仕様に合わせて変更してください。

実務での設計ポイント

GPIO割り込みは応答性に優れていますが、割り込みが発生するたびに重要な処理を直接実行する設計はおすすめできません。

実務では、次のような構成にします。

  1. GPIOの変化を割り込みで検出する
  2. ISRからタスクへ通知する
  3. タスク側で入力状態を再確認する
  4. デバウンスやノイズ除去を行う
  5. 有効なイベントだけを処理する

この構成にすることで、割り込み処理を短時間で終了させながら、通常のタスク上で安全に処理できます。

また、入力信号の性質によって設計方法を変えることも重要です。

タクトスイッチ

チャタリング対策が必要です。

センサーの割り込み出力

データシートで、信号がパルス出力かレベル出力かを確認します。

リミットスイッチ

ノイズ、配線長、接点寿命を考慮します。

ロータリーエンコーダ

両エッジ検出や複数GPIOの状態管理が必要になる場合があります。

外部ICの割り込み信号

割り込み要因を解除するまで信号がLowを維持するタイプでは、レベル割り込みの使用を検討します。

GPIO割り込みは一律の実装ではなく、接続するデバイスの出力仕様に合わせて設計することが重要です。

GPIO割り込みとGPTimerの使い分け

GPIO割り込みとGPTimerは、どちらもCPUへイベントを通知できますが、発生条件が異なります。

GPIO割り込み

外部信号の変化をきっかけに処理を開始します。

代表的な用途は次のとおりです。

  • ボタン入力
  • センサーの割り込み信号
  • リミットスイッチ
  • 外部機器からの通知
  • パルス入力

処理を開始するタイミングは、外部のイベントによって決まります。

GPTimer

設定した時間が経過したことをきっかけに処理を開始します。

代表的な用途は次のとおりです。

  • 一定周期のセンサー読み取り
  • 周期監視
  • タイムアウト管理
  • 定周期処理の開始
  • 時間計測

処理を開始するタイミングは、内部タイマーによって決まります。

両方を組み合わせる例

実際の製品では、GPIO割り込みとGPTimerを組み合わせる場合もあります。

例えば、スイッチ入力でGPIO割り込みを発生させ、GPTimerを使って一定時間後に入力状態を再確認することで、ハードウェアタイマーを利用したデバウンス処理を構成できます。

また、外部センサーの割り込みをGPIOで検出し、GPTimerで処理のタイムアウトを監視する構成も可能です。

重要なのは、次のように役割を分けることです。

  • 外部イベントの検出:GPIO割り込み
  • 時間経過の検出:GPTimer

用途に合わせて使い分けることで、効率的で応答性の高いシステムを構築できます。

まとめ

GPIO割り込みは、ESP32で外部イベントを効率よく検出するための重要な機能です。

ポーリングのように入力状態を繰り返し読み取る必要がなく、スイッチやセンサーの状態が変化した瞬間に処理を開始できます。

今回のサンプルでは、GPIO割り込みハンドラからFreeRTOSタスクへ通知し、タスク側で次の処理を実行しました。

  • チャタリング待ち
  • 入力レベルの再確認
  • 連続入力の除外
  • LED状態の反転
  • ログ出力

この構成には、次のメリットがあります。

  • ISRを短時間で終了できる
  • 通常のタスク上で安全に処理できる
  • デバウンス処理を追加しやすい
  • 外部センサーやスイッチ入力へ応用しやすい
  • システム全体の応答性を維持しやすい

GPIO割り込みでは、「割り込み内ですべてを処理する」のではなく、「割り込みはイベントを伝えるだけ」と考えることが重要です。

本処理をFreeRTOSタスク側へ分離することで、より安定した組み込みシステムを構築できます。

技術は、経験から価値になる。

DLROW Design | 現場エンジニア

スポンサーリンク

スポンサーリンク

-ESP32, ソフトウェア, 回路設計

Copyright© 江藤良樹の仕事は物づくり , 2026 All Rights Reserved.