Ⅴ. ESP32でのタイマー割り込みによる自動計測

 2つのタイマー割り込みを使って、設定した時間間隔で計測・記録と、NTPサービスによるシステムクロックの定期的な時刻補正を行わせます。ESP8266では標準ライブラリーのTickerクラスを用いて、タイマーを簡単に利用することができました。ESP32では、タイマー割り込みは標準関数としてサポートされています。したがってヘッダーファイルのインクルードなどは不要になりますが、グローバル変数の定義や割込関数の記述、タイマーの設定方法など、ESP32での流儀を理解することが必要になります。

 ※スケッチのダウンロードは右側の[Download]ボタンをクリックしてください。    

1.ソフト開発のポイント

 ESP32でタイマー割り込みを使うには、次の4つのポイントを押さえておく必要があります。
    ・グローバル変数の準備
    ・セットアップで行う初期化処理
    ・割込処理(ISR: Interrupt Service Routin)関数の構成
    ・メインループでの割込検知処理
  ※次の(1)~(3)はtechtutorialsx『ESP32 Arduino: Timer interrupts』を参考にさせていただきました。

(1)グローバル変数の準備
 ①割込通知カウンター

 メインループとISRで共用するためのカウンターです。コンパイラーの最適化によって消去されないように、volatile宣言をします。
    volatile int interruptCounter;

 ②タイマー設定用ポインター

 タイマーを設定するために必要なhw_timer_t型(タイマー管理用の構造体)のポインターを設定します。これはセットアップ関数で使用します。
    hw_timer_t *timer = NULL;

 ③同期用変数

 portMUX_TYPE型の変数で、メインループとISRで共用変数を変更する時の同期処理に使用します。
    portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

(2)セットアップで行う初期化処理
 ①使用タイマーの指定と初期化

 ESP32では4つのタイマーを使用することができます。タイマーの使用に先立って、まずtimerBegin関数で使用するタイマーの番号などを指定して初期化処理をします。この関数はesp-hal-timer.h内で次のようにプロトタイプ宣言されています。
    hw_timer_t * timerBegin(uint8_t timer, uint16_t divider, bool countUp);
        ・第1引数: 使用するタイマーの番号(0~3)
        ・第2引数: プリスケーラー(1マイクロ秒ごとにインクリメントさせたいなら80を指定)
        ・第3引数: 割込カウンターのインクリメント(カウントアップ)指定
 例えば次のように指定します。
    timer = timerBegin(0, 80, true);

 ②割込処理関数(ISR)の設定

 ISR関数を割込処理に結びつけます。この関数もesp-hal-timer.h内で次のようにプロトタイプ宣言されています。
    void timerAttachInterrupt (hw_timer_t * timer, void (* fn)(void), bool edge);
        ・第1引数: 初期化されたタイマー設定用のポインター
        ・第2引数: ISR関数のアドレス
        ・第3引数: エッジ割込指定
 onTimer()という名前の割込処理関数を記述するのであれば、次のようなコードになります。第1引数には、グローバル変数で定義したタイマー設定用ポインターを指定します。
    timerAttachInterrupt(timer, &onTimer, true);

 ③タイマー動作間隔の指定

 タイマーの動作間隔を指定します。この関数もesp-hal-timer.h内で次のようにプロトタイプ宣言されています。ただし第2引数の単位は、timerBegin()の第2引数のプリスケーラーで80を指定、つまりマイクロ秒指定に基づいていることに注意してください。
    void timerAlarmWrite (hw_timer_t * timer, uint64_t alarm_value, bool autoreload);
        ・第1引数: タイマーへのポインター
        ・第2引数: 割込発生までの時間(マイクロ秒)
        ・第3引数: カウンターのリロード指定(定期的に割り込みを生成させる)
 1秒間隔の割込の場合は次のように指定します。
    timerAlarmWrite(timer, 1000000, true);

 ④タイマーの有効化

 タイマー変数を引数として渡してタイマーの有効化を宣言します。esp-hal-timer.h内で次のようにプロトタイプ宣言されています。
    void timerAlarmEnable (hw_timer_t * timer);  ここでは次のように指定します。
    timerAlarmEnable(timer);

(3)ISR関数の構成

 ISR関数のコードを記述するとき、次の事項を守らなくてはなりません。
    ・ISR関数は引数を伴わないvoid型でなければならない。
    ・ISRは IRAMに配置するため、IRAM_ATTR属性が必要。
    ・関数内では割込通知カウンターをインクリメントして、メインループに割り込みの発生を通知する。
    ・共用変数のインクリメントなので、portENTER_CRITICAL_ISRと portEXIT_CRITICAL_ISRで挟んだ
     クリティカルセクション内で行う。
 以上のルールに従うISR関数は次のようになります。
    void IRAM_ATTR onTimer() {
      portENTER_CRITICAL_ISR(&timeMux);
      interruptCounter++;
      portEXIT_CRITICAL_ISR(&timeMux);
    }

(4)メインループでの割込検知処理

 割込通知カウンターにより割り込みの発生を感知してカウンターをクリアーし、必要な割込処理を行います。カウンターのクリアには、portENTER_CRITICALと portEXIT_CRITICALで挟んだクリティカルセクション内でデクリメントを行います。
 例えば以下のようなコードを記述します。
void loop {
if (interruptCounter > 0) {
  portENTER_CRITICAL(&timeMux);
  interruptCounter--;
  portEXIT_CRITICAL(&timeMux);
  // Interrupt handling code
        :
        :
  }
}

2.既存コードの変更

〔変更点〕
  ※以下のコードは変更部分だけをピックアップしたものです。
  ・2~3行: ESP8266用のヘッダーファイルをESP32用に変更。
  ・7~8行: SdFat未完につきSDを利用。
  ・9行目: Tickerクラスは使わない(存在しない)。
  ・24行目:  SdFatは使えないので削除。
  ・29~33行: Tickerは使わないので削除。
  ・34~38行: ESP32用のタイマー関連グローバル変数の定義(2つのタイマーを設定)。
  ・41~56行: 2つのISR関数を記述。
  ・65行目:  Wire.begin()をWire.begin(SDA, SCL)に変更。
  ・84~87行: Tickerは使わないので削除。
  ・88~89行: 2つのtimerBegin関数でタイマーを初期化。
  ・92~93行: 2つのtimerAttachInterrupt関数で、それぞれISR関数onTimer1()とonTimer2()を結合。
  ・96~97行: 2つのtimerAlarmWrite関数で、それぞれのタイマーに計測間隔とNTPアクセス間隔を設定。
  ・101~102行: 2つのtimerAlarmEnable関数でタイマーを有効化。
  ・117~124行: Tickerは使わないので削除。
  ・127~132行: タイマー0から割込があればカウンターをデクリメントしてkickRoutineWork()を実行する。
  ・133~138行: タイマー1から割込があればカウンターをデクリメントしてadjustTime()を実行する。
  ・161~165行: Tickerは使わないので削除。
  ・171~175行: 計測値の変換ロジックを修正。
#include <Wire.h>
////#include <ESP8266WiFi.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <SPI.h>
////#include <SdFat.h>
#include <SD.h>
////#include <Ticker.h>

// I2C Address
#define DS1307_ADDRESS 0x68   // Realtime clock
#define BME280_ADDRESS 0x76   // Humidity, Pressure and Temperature sensor
#define BH1750_ADDRESS 0x23   // Illuminance sensor

// SD card drive & File name
#define SDCARD_DRIVE    5     // SD card chip select number
#define ANALOG_PIN     A0     // Analog input pin (SENSOR_VP/GPIO36)
#define DATA_FILE      "/datafile32.txt"

bool bAtFirst = true;

// SD card control
////SdFat SD;
bool bSD_Enabled;
		:
		:
// Timer interruption
/*
Ticker ticker1;               // For measurement
Ticker ticker2;               // For time adjustment
bool  bReadyTicker = false;
*/
volatile int timeCounter1;
volatile int timeCounter2;
hw_timer_t *timer1 = NULL;    // For measurement
hw_timer_t *timer2 = NULL;    // For time adjustment
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
		:
		:
/*****************************************************************************
 *                          Interrupt Service Routin                         *
 *****************************************************************************/
void IRAM_ATTR onTimer1(){
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter1++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

void IRAM_ATTR onTimer2(){
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  timeCounter2++;
  portEXIT_CRITICAL_ISR(&timerMux);
}

/*****************************************************************************
 *                          Predetermined Sequence                           *
 *****************************************************************************/
void setup() {
    Serial.begin(115200);

    // Prepare I2C protocol.
    Wire.begin(21,22);    // Define(SDA, SCL)
    delay(50);

    getDateTime(&dtClock);
    String sTime = editTime(dtClock);
    Serial.print("\r\nStart "); Serial.println(sTime);
		:
		:
    // Recreate NTPClient object.
    timeClient = NTPClient(ntpUDP, sNtpUrl, iNtpOffset);
    Serial.println("It is just in time!!");

    // Adjust time.
    adjustTime();

    // Do 1'st measurement.
    kickRoutineWork();

    // Timer: interrupt time and event setting.
/*
    ticker1.attach(iIntervalTime, kickRoutineWork);
    ticker2.attach(iNtpInterval, kickTimeAdjust);
*/
    timer1 = timerBegin(0, 80, true);
    timer2 = timerBegin(1, 80, true);

    // Attach onTimer function.
    timerAttachInterrupt(timer1, &onTimer1, true);
    timerAttachInterrupt(timer2, &onTimer2, true);

    // Set alarm to call onTimer function every second (value in microseconds).
    timerAlarmWrite(timer1, (iIntervalTime * 1000000), true);
    timerAlarmWrite(timer2, iNtpInterval * 1000000, true);

    // Start an alarm
    timerAlarmEnable(timer1);
    timerAlarmEnable(timer2);
}

void loop() {
    if (!bSD_Enabled) {
        Serial.println("Can't work, the SD drive is disabled!");
        delay(60000);
        return;
    }

    // Check date input ('hh/mm/dd/w') from serial buffer.
    if (Serial.available() >= 8) {
        String date = Serial.readString();
        setDate(date);
    }

////    if (bReadyTicker) {
    /*
     * [Timer interrupt process]
     *    Match measurement timing at time correction.
     */
/*        adjustTime();
        bReadyTicker = false;
    }*/

    // Timer interrupt process
    if (timeCounter1 > 0) {
      portENTER_CRITICAL(&timerMux);
      timeCounter1--;
      portEXIT_CRITICAL(&timerMux);
      kickRoutineWork();
    }
    if (timeCounter2 > 0) {
      portENTER_CRITICAL(&timerMux);
      timeCounter2--;
      portEXIT_CRITICAL(&timerMux);
      adjustTime();
    }
}

/****************************< Interrupt handler >****************************/

/*
 * Timer interrupt event handler1
 *    <Start measurement>
 */
void  kickRoutineWork()
{
    // Measurment & write file
    doMeasurement();
    String buf =editMeasuredResult(rstMeasured);
    writeMeasurementResult(DATA_FILE, buf);
    Serial.println(buf);
}

/*
 * Timer interrupt event handler1
 *    <Start time adgustment>
 */
/*
void kickTimeAdjust()
{
    bReadyTicker = true;
}
*/
		:
		:
int measureSoilMoisture(int pin_no)
{
    int val = analogRead(pin_no);
    int res = (int)map(val, 2650, 4100, 100, 0);
    if (res < 0)
        res = 0;
    else if (res > 100)
        res =100;
    return res;
}

3.スケッチの実行

 変更後のスケッチをesp32Measure.inoの名称で保存して、[ツール]タブからシリアルモニタを開きます。
 ESP32開発ボードの右ボタン[FLASH]を押したまま左ボタン[RESET]を押下した後、IDEの「マイコンボードに書き込む」アイコンをクリックしてください。コンパイルとマイコンボードへの書き込みが終わると、シリアルモニターに次のように表示されます(土壌湿度センサーは卓上に放置しているので異常値を示しています)。


以上!

 
Copyright (C) 2011-2021 Marchan, All rights reserved.