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の「マイコンボードに書き込む」アイコンをクリックしてください。コンパイルとマイコンボードへの書き込みが終わると、シリアルモニターに次のように表示されます(土壌湿度センサーは卓上に放置しているので異常値を示しています)。
以上!