外部イベントの検知~ポーリングと割り込み~ (ESP-IDF 環境 + C 言語)

スイッチが押された・センサが反応したといった,いつ起こるか分からない 「非同期イベント」を検知する方法として,割り込みとポーリングという 2 つの代表的な検知方法がある.

ポーリングは「イベントが発生していないかを定期的にチェックする」という 方法である.while 文などでループを回しながらイベントが発生していないか を if 文で確認するような操作に相当する.この方法ではチェックするための ループが回るのに時間がかかると,リアルタイムに非同期イベントを検知でき ないということが生じる.マルチコアないしマルチスレッドを用いたマルチ タスクなプログラムを作成し,メインループと非同期イベント検知のためのルー プを別々に回すようにすれば,ほぼリアルタイムに非同期イベントの発生を検 知できる.

割り込みは「イベントが発生した瞬間に通知を受け取る仕組み」である.これ にはイベントハンドラーと呼ばれる,監視対象がルールを満たしたことをトリ ガーとしてアクションを実行する機能を用いる.各イベントが起こったときの 処理を記述した割り込みハンドラをあらかじめ用意しておき,異なるタイミン グで生じる様々な非同期イベントを自動的に処理することになる.なお,CPU は割り込み信号を受け付けたら,割り込みが入らなかった場合に実行されるは ずだった番地をスタックに積み,割り込み処理の内容が記述されている番地を プログラム・カウンタにセットすることで割り込み処理ルーチンに「ジャンプ」 し,処理を進める.割り込み処理の終わりを示す割り込み復帰命令を実行する と,CPUはスタックに逃がしておいた番地を取り出してプログラム・カウンタ に入れ,なにごともなかったかのように以前のしごとに戻る.もちろん,戻っ たときに困らないために,割り込み処理を実行する時点で今まで使っていたメ モリやレジスタの内容を保存している.

ESP-IDF では

ESP32 は CPU コアが 2 つあるデュアルコア構成となっているので,これらのコアを 有効活用するためには,タスク管理といったリアルタイム OS の機能が欠かせない. ESP-IDF は OS としてオープンソースのリアルタイム OS である FreeRTOS (Real Time Operating System) を使用している. リアルタイム OS は複雑なことはできないが,処理を途切れることなく実行させることができる. 以下のサンプルでは vTaskDelay, xTaskCreate といった関数が出てくるが,これは freeRTOS の機能である. FreeRTOS が動いているので,ESP32 ではマルチタスク (マルチコア・マルチスレッド) なプログラムを作成するのが容易である.

基本となるプログラム

今回は,LED 2 つとスイッチを使ったプログラムを作成する. LED1 は定期的に点滅させ,LED2 はスイッチの ON/OFF に合わせて点滅させるものとする.

まず,GPIOの説明 で用いた L チカのサンプルを活用する.

$ cd ~/esp

$ cp -r esp-idf/examples/peripherals/gpio .

$ cd gpio

$ cp main/gpio_example_main.c  main/gpio_example_main.c.orig     #バックアップ

以下のようなプログラムを作成する.

$ vi main/gpio_example_main.c

  #include <stdio.h>
  #include "freertos/FreeRTOS.h"
  #include "freertos/task.h"
  #include "freertos/queue.h"
  #include "driver/gpio.h"

  #define INPUT_PIN 34
  #define LED_PIN1 13
  #define LED_PIN2 14

  void app_main()
  {
    int count = 0;

    //LED 初期化
    gpio_pad_select_gpio(LED_PIN1);
    gpio_set_direction(LED_PIN1, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(LED_PIN2);
    gpio_set_direction(LED_PIN2, GPIO_MODE_OUTPUT);

    //スイッチ初期化
    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(INPUT_PIN, GPIO_PULLUP_ONLY);

    //メインルーチン
    while (true) {
      gpio_set_level(LED_PIN1, count%2);
      vTaskDelay(5000 / portTICK_PERIOD_MS);  // 5秒待つ
      count += 1;
      printf("main loop %d \n", count);

      if ( gpio_get_level(INPUT_PIN) == 1){
        gpio_set_level(LED_PIN2, 1);
      }else{
        gpio_set_level(LED_PIN2, 0);
      }
    }
  }

プログラムを実行する.

$ make flash monitor

実行結果として,スイッチを入れてもすぐに LED2 が点灯しないということが得られるはずである. 最大で 5 秒程度 (メインルーチンを回す時間間隔) 待つことになる.これはあまりうれしくない.

割り込み (interrupt) を使ったプログラム

上記プログラムを割り込みを使ったプログラムにしてみる.

$ cp main/gpio_example_main.c  main/gpio_example_main.c.bk1     #バックアップ

$ vi main/gpio_example_main.c

  #include <stdio.h>
  #include "freertos/FreeRTOS.h"
  #include "freertos/task.h"
  #include "freertos/queue.h"
  #include "driver/gpio.h"

  #define INPUT_PIN 34
  #define LED_PIN1 13
  #define LED_PIN2 14

  //割り込み
  static void IRAM_ATTR gpio_interrupt_handler(void *args)
  {
    if ( gpio_get_level(INPUT_PIN) == 1){
      // gpio_hold_en(LED_PIN1);
      gpio_set_level(LED_PIN2, 1);
    }else{
      // gpio_hold_dis(LED_PIN1);
      gpio_set_level(LED_PIN2, 0);
    }
  }

  void app_main()
  {
    int count = 0;

    //LED 初期化
    gpio_pad_select_gpio(LED_PIN1);
    gpio_set_direction(LED_PIN1, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(LED_PIN2);
    gpio_set_direction(LED_PIN2, GPIO_MODE_OUTPUT);

    //スイッチ初期化
    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(INPUT_PIN, GPIO_PULLUP_ONLY);

    //割り込みのトリガーの設定
    //  GPIO_INTR_DISABLE = 0,     /*!< Disable GPIO interrupt                             */
    //  GPIO_INTR_POSEDGE = 1,     /*!< GPIO interrupt type : rising edge                  */
    //  GPIO_INTR_NEGEDGE = 2,     /*!< GPIO interrupt type : falling edge                 */
    //  GPIO_INTR_ANYEDGE = 3,     /*!< GPIO interrupt type : both rising and falling edge */
    //  GPIO_INTR_LOW_LEVEL = 4,   /*!< GPIO interrupt type : input low level trigger      */
    //  GPIO_INTR_HIGH_LEVEL = 5,  /*!< GPIO interrupt type : input high level trigger     */
    gpio_set_intr_type(INPUT_PIN, GPIO_INTR_ANYEDGE);

    //GPIOの割り込みハンドラサービスをインストールする
    //引数はとりあえず 0 を入れておけばよい.
    gpio_install_isr_service(0);

    //指定したGPIOに対して割り込みハンドラを追加する
    gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);

    //メインルーチン
    while (true) {
      gpio_set_level(LED_PIN1, count%2);
      vTaskDelay(5000 / portTICK_PERIOD_MS);  //5秒待つ
      count += 1;
      printf("main loop %d \n", count);
    }
 }

プログラムを実行する.

$ make flash monitor

今度はスイッチを ON/OFF すると,即座に LED が ON/OFF することが確かめられるはずである.

試してみよう

  1. プログラム中の gpio_hold_en(LED_PIN1), gpio_hold_dis(LED_PIN1) のコメントアウトを外してみよ.何が起きるか確認すること.
  2. 割り込みトリガーの設定で, GPIO_INTR_ANYEDGE 以外を指定すると何が起きるか確認すること.

ポーリング (マルチタスク) を使ったプログラム

ポーリングはマルチタスクで実現する.メインループ以外の非同期イベント 検知用ループを用意しておけばよい. マルチタスクのプログラム中では xTaskCreate 関数を用いる必要がある. この関数の詳細は, FreeRTOSのドキュメント を参照されたい.

プログラムを作成する.

$ cp main/gpio_example_main.c  main/gpio_example_main.c.bk2     #バックアップ

$ vi main/gpio_example_main.c

  #include <stdio.h>
  #include "freertos/FreeRTOS.h"
  #include "freertos/task.h"
  #include "driver/gpio.h"
  #include "sdkconfig.h"

  #define INPUT_PIN 34
  #define LED_PIN1 13
  #define LED_PIN2 14


  // LED1 を 5 秒間隔で回すタスク
  static void task1(void * pvParameters) {
    int count = 0;
    while (1) {
      gpio_set_level(LED_PIN1, count % 2);
      vTaskDelay( 5000 / portTICK_PERIOD_MS ); //5秒間隔
      count++;
    }
  }

  // LED2 の ON/OFF
  static void task2(void * pvParameters) {
    while (1) {
      if ( gpio_get_level(INPUT_PIN) == 1){
        // gpio_hold_en(LED_PIN1);
        gpio_set_level(LED_PIN2, 1);
      }else{
        // gpio_hold_dis(LED_PIN1);
        gpio_set_level(LED_PIN2, 0);
      }
      vTaskDelay( 100 / portTICK_PERIOD_MS ); //若干待つ.
    }
  }

  void app_main(void)
  {
    //LED 初期化
    gpio_pad_select_gpio(LED_PIN1);
    gpio_set_direction(LED_PIN1, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(LED_PIN2);
    gpio_set_direction(LED_PIN2, GPIO_MODE_OUTPUT);

    //スイッチ初期化
    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(INPUT_PIN, GPIO_PULLUP_ONLY);

    //マルチタスク.(引数の 1 は優先順位)
    xTaskCreate(task1, "LED1", 2048, NULL, 1, NULL);
    xTaskCreate(task2, "LED2", 2048, NULL, 1, NULL);
  }

プログラムを実行する.

$ make flash monitor

上記の割り込みのプログラムと同様に,スイッチの ON/OFF に従って LED2 が即座に点灯/消灯することが確認できるだろう.

やってみよう

  • xTaskCreate 関数を用いて 3 つ以上のタスクを動かせるか試してみよ.
  • ESP32 マイコンの CPU はデュアルコアである.以下のように xTaskCreatePinnedToCore 関数を使って最終引数にコア番号を与えることができる.このような変更をしてマルチコアでプログラムを動かせるか試してみること.

    xTaskCreatePinnedToCore(task1, "LED1", 2048, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(task2, "LED2", 2048, NULL, 1, NULL, 1);
    xTaskCreatePinnedToCore(task3, "LED3", 2048, NULL, 1, NULL, 1);

マルチコアとマルチスレッドは同時に設定できるので,上記のように CPU 0 と CPU 1 を両方動かし, さらに CPU 1 で複数のスレッドを動かすことが可能となっている.

  • 関数 task2 に含まれる以下の行をコメントアウトしてみよ.実行した時に何が起きるか試してみること.

    vTaskDelay( 100 / portTICK_PERIOD_MS ); //若干待つ.

make monitor したときに,おそらく以下のようなメッセージが表示され,マイコンが再起動を繰り返してしまうと思う.

E (10278) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:
E (10278) task_wdt:  - IDLE1 (CPU 1)
E (10278) task_wdt: Tasks currently running:
E (10278) task_wdt: CPU 0: IDLE0
E (10278) task_wdt: CPU 1: LED2
E (10278) task_wdt: Print CPU 0 (current core) backtrace

マイコンにはワッチドック (watchdog) という仕組みがあり, システム側で無限ループに陥っているスレッドを監視し, 無限ループに陥っていると判定すると自動的にシステムをリセットする. スレッド内の vTaskDelay がないと,watchdog は無限ループに陥っていると判断してしまう. また,vTaskDelay 中に他の処理を行うことで並行処理を実現させているらしく, その意味でも各スレッドに vTaskDelay が入っていると良い.

割り込み (interrupt) とマルチタスクの両方を使う

ESP-IDF のサンプル (esp-idf/examples/peripherals/gpio) は,マルチタスクと 割り込みの両方を使ったようなプログラムとなっている.

以下のような ESP-IDF のサンプルを簡略化したものを作成し, これまでと同様に動作することを確かめてみよ.

$ cp main/gpio_example_main.c  main/gpio_example_main.c.bk2     #バックアップ

$ vi main/gpio_example_main.c

  #include <stdio.h>
  #include "freertos/FreeRTOS.h"
  #include "freertos/task.h"
  #include "freertos/queue.h"
  #include "driver/gpio.h"

  #define INPUT_PIN 34
  #define LED_PIN1 13
  #define LED_PIN2 14

  xQueueHandle interruptQueue;

  //割り込みサービスルーチン内で xQueueSendFromISR() 関数を呼び出す.
  static void IRAM_ATTR gpio_interrupt_handler(void *args)
  {
    int pinNumber = (int)args;
    xQueueSendFromISR(interruptQueue, &pinNumber, NULL);
  }

  //xQueueReceive() 関数を使用してキューから項目を受信したかどうかを確認する
  //第一引数が受信元となるキューハンドルとなっている.
  void LED_Control_Task(void *params)
  {
    int pinNumber, count = 0;
    while (true)
    {
      if (xQueueReceive(interruptQueue, &pinNumber, portMAX_DELAY))
      {
        printf("GPIO %d was pressed %d times. The state is %d\n", pinNumber, count++, gpio_get_level(INPUT_PIN));
        if ( gpio_get_level(INPUT_PIN) == 1){
          //gpio_hold_en(LED_PIN1);
          gpio_set_level(LED_PIN2, 1);
        }else{
          //gpio_hold_dis(LED_PIN1);
          gpio_set_level(LED_PIN2, 0);
        }
      }
    }
  }

  void app_main()
  {
    int count = 0;

    //LED 初期化
    gpio_pad_select_gpio(LED_PIN1);
    gpio_set_direction(LED_PIN1, GPIO_MODE_OUTPUT);
    gpio_pad_select_gpio(LED_PIN2);
    gpio_set_direction(LED_PIN2, GPIO_MODE_OUTPUT);

    //スイッチ初期化
    gpio_pad_select_gpio(INPUT_PIN);
    gpio_set_direction(INPUT_PIN, GPIO_MODE_INPUT);
    gpio_set_pull_mode(INPUT_PIN, GPIO_PULLUP_ONLY);

    //割り込みのトリガーの設定
    gpio_set_intr_type(INPUT_PIN, GPIO_INTR_ANYEDGE);

    //GPIOの割り込みハンドラサービスをインストールする
    gpio_install_isr_service(0);

    //指定したGPIOに対して割り込みハンドラを追加する
    gpio_isr_handler_add(INPUT_PIN, gpio_interrupt_handler, (void *)INPUT_PIN);

    //キューの作成
    interruptQueue = xQueueCreate(10, sizeof(int));

    //タスク (スレッド) を作る
    xTaskCreate(LED_Control_Task, "LED_Control_Task", 2048, NULL, 1, NULL);

    //メインルーチン
    while (true) {
      gpio_set_level(LED_PIN1, count%2);
        vTaskDelay(5000 / portTICK_PERIOD_MS);  //1秒待つ
          count += 1;
          printf("main loop %d \n", count);
    }
  }

やってみよう

  • GPIO の初期化で gpio_config 関数と gpio_cofig_t 構造体を使ってみよ.

esp-idf/examples/peripherals/gpio のオリジナル (main/gpio_example_main.c.orig) を 確認すると,GPIO の定義の方法が異なっていることが分かる.これまで GPIO の初期化は

gpio_pad_select_gpio(LED1);
gpio_set_direction(LED1, GPIO_MODE_OUTPUT);

という形で書いてきたが,オリジナルでは関数 gpio_config を使っており, gpio_config の引数は gpio_config_t 構造体である. gpio_cofig_t 構造体のメンバーの意味は以下の通りである.

typedef struct {
   uint64_t pin_bit_mask;        //設定するピンのビットに1を立てる
   gpio_mode_t mode;             //入力 or 出力
   gpio_pullup_t pull_up_en;     //プルアップを有効にするか否か
   gpio_pulldown_t pull_down_en; //プルダウンを有効にするか否か
   gpio_int_type_t intr_type;    //割り込みタイプの設定
} gpio_config_t;

LED では具体的には以下のような書き方になる.こちらを使うと 複数のピンをまとめて初期化するといったことも可能である.

#define GPIO_OUTPUT_IO_0    12
#define GPIO_OUTPUT_IO_1    33
#define GPIO_OUTPUT_PIN_SEL  ((1ULL<<GPIO_OUTPUT_IO_0) | (1ULL<<GPIO_OUTPUT_IO_1))

...(略)...

  //構造体定義
  gpio_config_t io_conf;

  //disable interrupt
  io_conf.intr_type = GPIO_PIN_INTR_DISABLE;

  //set as output mode
  io_conf.mode = GPIO_MODE_OUTPUT;

  //bit mask of the pins that you want to set,e.g.GPIO18/19
  io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;

  //disable pull-down mode
  io_conf.pull_down_en = 0;

  //disable pull-up mode
  io_conf.pull_up_en = 0;

  //configure GPIO with the given settings
  gpio_config(&io_conf);

参考