Ⅴ. 省電力LoRa端末の開発

 今回のテーマは、ディープスリープ状態で待機する端末を、センター側からの指示でウェークアップさせて計測結果を送信させることです。計測と送信以外の時間は端末をディープスリープにすることで、消費電力を抑えるのが最大の目標です。センター側はテスト用の簡単なコードですが、複数端末に対して「順次送受信方式」を適用することで、端末間の混信を防止しています。
 実験用の計測端末用ボードでは温度と湿度を計測していますが、他にも様々な応用が考えられます。そのベースとなる省電力LoRa端末の原形として、ほぼ完成形が出来上がったのではないかと考えています。

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


1.ディープスリープとウェークアップの方法

 ESP32のディープスリープとウェークアップについては、すでに第3章で取り上げました。そして、タイマーによるウェークアップのテストを行い、それを実装した計測端末の実験をしました。簡潔なコードで計測データの通信が可能でしたが、いったん起動すると後の動作は端末まかせになるため、混信の発生を確実に避けることは難しいことがわかりました。
 今回も同様に ESP32をディープスリープさせますが、ウェークアップは、特定の GPIOピンの変化で起動する「EXT0」を採用します(ESP32で使用できるピンは、RTC GPIOの、0、2、4、12~15、25~27、32~39に限定されます)。EXT0は、センター側からの指示によってインタラクティブに動作させるので混信の心配がありません。

 E220-900T22S(JP)モジュールには、WOR通信機能というディープスリープを活用するための機能が備わっています。WOR(Wake on Radio)通信には、WOR送信モードと WOR受信モードがあります。
 WOR送信モードはセンター側の送信で使用します。送信をすると、メッセージの前にモジュールをウェークアップさせるためのプリアンブルを自動的に付け加えて送信してくれます。一方、端末側は WOR受信モードで待ち受けます。この状態では、 LoRaモジュールはスリープ状態になっていて、WOR送信モードで送信されてきた情報だけが受信可能になっています。
 つまり、ディープスリープ状態の計測用 ESP32に接続された LoRaモジュールは、常に WOR受信モードで待機して、センター側から WOR送信モードで送信されるプリアンブルの到着を待っていればよいわけです。

    

 計測指示の先頭のプリアンブルを受信すると、LoRaモジュールはウェークアップして、同時に AUXピンが Lowレベルになります。AUXピンを「EXT0」の GPIOピンに割り当てておけば、LowになるタイミングでESP32をウェークアップさせることが可能になります。
 ウェークアップした ESP32は必要な処理を実行して、再びディープスリープに移行します。このようにすることで、LoRaモジュールと ESP32は大幅に消費電力を減らすことができます。

2.省電力LoRa端末の構造

 ディープスリープとウェークアップを反復することから、プログラム構造は次のような特徴があります。
① コードはすべて setup関数内に書く

 ウェークアップすると一連の処理を実行して再びディープスリープに入ります。したがって、Arduino IDEのスケッチでは、すべての処理を setup関数内に記述することになります。特に必要としない限り、loop()関数内に記述することはありません。

② 初回実行の判定

 実行を制御するために、初回の起動と2回目以降のウェークアップによる起動を判別する必要があります。このため、ディープスリープしても初期化されないスローメモリ領域にグローバル変数 atFirstを定義します。
    RTC_DATA_ATTR bool atFirst = true;
 RTC_DATA_ATTRは、変数をスローメモリ領域に確保する指定です。初回処理を実行後に atFirst = false;とすることで、2回目以降の(初回でない)実行を判別することができます。

③ ディープスリープの確認コード

 ディープスリープの状態を簡単に視認できるよう、処理の先頭で内蔵LEDを点灯します。ディープスリープに入ると GPIOはリセットされるので、GPIO2に接続されている青色の内蔵LEDは消灯します。ディープスリープ状態ではLEDが消え、動作中は点灯することから、状態を目視で確認することができます。


setup関数内の処理

 setup関数には以下のような処理を記述します。
 なお、⑤の初期化処理ではライブラリがエラーを返しますが、これは処理上問題ないので無視します。


①ディープスリープに入るまでは内蔵LEDを点灯させておきます。

②AUXピンをソフトウェアでプルアップしておきます。

③ここでは DHTによる計測を開始しています。

④シリアル通信を開始して安定するまで待ちます。

⑤既定のコンフィギュレーションでモジュールを初期化します。

⑥(後述)

⑦DHTから温度と湿度の計測値を取得して、ターミナル番号などと共に編集します。

⑧ノーマルモードに移行して編集結果をセンター宛に送信します。

⑨センター側からの次の計測指示を待つため、WOR受信モードに切り替えます。

⑩(後述)

⑪AUXピンがLowになるとウェークアップするように設定します。

⑫「初回処理」フラグを解除します。

⑬ディープスリープを開始します。内蔵LEDが消えます。

 全体の記述はシンプルなのですが、前章の末尾で述べたように、ディープスリープが思うように制御できず手間取ってしまいました。具体的な現象としては、ウェークアップできなかったり、スリープ後にリブートを繰り返したりします。またコードの記述順序によっては、スリープ後に即再起動して動作状態のまま受信待ちをする現象も発生します。
 これらの原因は、動作モードを設定する M0, M1ピンなどの状態が、ディープスリープに入ると変化してしまうためでした。対策として、ディープスリープの前に上図⑩のように関連GPIOをホールドすること、そして⑥ 2回目以降の起動、つまりディープスリープからのウェークアップ時にホールド状態を解除することでこの問題は解決しました。

3.コードの解説

(1) センター側のタスク:testTransmitter.ino

 センター側のタスクは、複数の省力化LoRa端末に計測指示メッセージを送るだけの簡単なテストプログラムです。送受信はマルチタスクで実行させています。前章の順次送受信用の loraCentral.inoと似ていますが、一部を変更しています。その部分に限定して解説します。

 コードの解説
  ①ヘッダーファイルとデータ等の定義

 ・9行目: テスト用の計測端末は2台です。
 ・11行目: 計測間隔は10秒です。
 ・22行目: 計測データの受信完了を表すフラグです。
 ・23行目: 端末からのデータ受信の完了フラグです。

  ②初期化処理 setup部

 ・47行: 端末の LoRaモジュールをウェークアップするために、WOR送信モードを設定します。

  ③反復処理 loop部

 ・57~58行: お馴染みの計測時間到来のチェックで、到来すれば doSendSignalを trueにします。

  ④受信タスク LoRaRecvTask部

 ・81行目: データを受信すると receiveCompletedフラグを trueにします。

  ⑤送信タスク LoRaSendTask部

 ・109~115行: 送信後は計測端末からのデータ着信まで待ちます(マルチタスクの受信を待ち受ける)。
#include "esp32_e220900t22s_jp_lib.h"

#define TERMINAL_NUMBER 0
#define LED_BUILTIN 2                 // 内蔵LED
#define NUMBER_OF_TERMINALS   2       // 端末台数
#define SEND_DATA_SIZE 29             // 32-3
#define MEASURE_INTERVAL 10           // 計測間隔(整数:秒)

CLoRa lora;                           // LoRaクラスのインスタンス
struct LoRaConfigItem_t config;       // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体

// Function prototype
void LoRaRecvTask(void *pvParameters);
void LoRaSendTask(void *pvParameters);

unsigned long prev_time = 0;          // 前回の計測時刻
bool doSendSignal = false;            // シグナル送信せよ!
bool receiveCompleted = false;        // データ受信完了!
unsigned long sendCtr = 0;            // 計測指示回数

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  SerialMon.begin(115200);
  delay(1000);

  // 既定のLoRaコンフィギュレーションを取得して
  lora.SetDefaultConfigValue(config);
  delay(10);

  // E220-900T22S(JP)を初期設定する
  config.own_address = TERMINAL_NUMBER;
  if (lora.InitLoRaModule(config)) {
    SerialMon.printf("LoRa init error\n");
    return;                                               
  }

  // マルチタスクを設定して
  xTaskCreateUniversal(LoRaRecvTask, "LoRaRecvTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
  xTaskCreateUniversal(LoRaSendTask, "LoRaSendTask", 8192, NULL, 1, NULL, APP_CPU_NUM);

  // WOR送信モードへ移行する
  lora.SwitchToWORSendingMode();
  delay(10);
  SerialMon.println("Start sending message!");
}

void loop() {
  int differ;

  unsigned long now = millis();
  differ = (now - prev_time) / 1000;
  if (differ >= (int)MEASURE_INTERVAL && doSendSignal == false) {
    doSendSignal = true;
    prev_time = now;
    Serial.printf("> Request to send message .... %d\n", now / 1000);
  }
  delay(1000);
}

/*
  受信タスク
*/
void LoRaRecvTask(void *pvParameters) {
  struct tm *tm;
  time_t t;
  char str[256];

  while (1) {
    if (lora.ReceiveFrame(&data) == 0) {  // 受信データが届くと表示処理を行う
      t = time(NULL);   tm = localtime(&t);
      sprintf(str, "< Get responce: %02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
      SerialMon.println(str);
      data.recv_data[data.recv_data_len] = '\0';
      sprintf(str, "Receive data: %s\n", data.recv_data);
      SerialMon.println(str);
      receiveCompleted = true;
      digitalWrite(LED_BUILTIN, HIGH);
      delay(200);
      digitalWrite(LED_BUILTIN, LOW);
    }
    delay(1);
  }
}

/*
  送信タスク
*/
void LoRaSendTask(void *pvParameters) {
  char msg[SEND_DATA_SIZE] = { 0 };
  
  while (1) {
    if (doSendSignal) {       // 送信要求があると送信処理を行う
      doSendSignal = false;
      sprintf(msg, "Go ahead! (%ld)", ++sendCtr);

      for (int i=1; i<=NUMBER_OF_TERMINALS; i++) {
        config.target_address = i;
        int state = lora.SendFrame(config, (uint8_t *)msg, sizeof(msg));
        if (state == 0)
          SerialMon.printf("Terminal-%d kick off succeeded!\n", i);
        else
          SerialMon.printf("Terminal-%d kick off failed!\n", i);
        // 受信完了(計測端末からの着信)を待つ
        while(1) {
          if (receiveCompleted) {
            receiveCompleted = false;
            break;
          }
          delay(10);
        }
      }
    }
    delay(1);
  }
}

(注意事項)

・計測指示の送信からデータ受信まで 4秒少々かかります。
 したがって現在の計測間隔10秒は余裕がなく、端末台数に応じて計測間隔を十分にとる必要があります。
・センター側と端末側で、帯域幅、チャンネル、ターゲットアドレスの組、暗号化キーが合致していることが必要です。

(2) 計測用端末側のタスク:esp32loraTerminal.ino

 setup関数では、前述の「setup関数の処理」に従ってコードを記述しています。以下ではポイントだけを解説します。

 コードの解説
  ①ヘッダーファイルとデータ等の定義

 ・26~27行: 各端末ごとに重複しない識別番号(1,2,3,・・・・)を設定します。
 ・39行目: ディープスリープ中でも値をキープできるスローメモリ領域に、初回判定用のフラグを設定します。
 ・40行目: 同じくスローメモリ領域に計測回数のカウンターを設定します。

  ②初期化処理 setup部

 ・45行目: 内蔵LEDを点灯します。
 ・48行目: pinMode関数を使って AUXピンをプルアップしておきます。
 ・64行目: 2回目からの LoRaモジュール初期化関数を実行するとエラーになるので、無視します。
 ・68~73行: 2回目以降では、前回にホールドした GPIOの状態を解除します。
 ・78行目: 端末番号、計測回数、温度、湿度を送信用データとして編集します。
 ・89行目: 確実に送信を完了するために十分な待ち時間が必要です。
 ・92行目: 次のウェークアップに備えて、WOR送信メッセージが受信できるよう WOR受信モードに切り替えます。
 ・96~100行: ディープスリープ中に特定の GPIOが変更されないように、GPIOをホールドします。
 ・103行目: AUXピンが Lowになるとウェークアップするように、ディープスリープの復帰条件を設定します。
 ・107行目: ここで初回判定用のフラグをリセットします。
 ・108行目: ディープスリープに突入します。

  ③反復処理 loop部

  すでにディープスリープ状態にあり、ここが実行されることはありません。
#include "esp32_e220900t22s_jp_lib.h"
#include <DHT.h>

#define TERMINAL_NUMBER 1
//#define TERMINAL_NUMBER 2
#define LED_BUILTIN 2                 // 内蔵LED
#define SEND_DATA_SIZE 61             // 64-3
#define DHTTYPE     DHT11             // DHTセンサーの型式

/* DHTセンサー*/
const int DHTPin = 14;                // DHTセンサーの接続ピン
DHT   dht(DHTPin, DHTTYPE);           // DHTクラスの実体

CLoRa lora;                           // LoRaクラスのインスタンス
struct LoRaConfigItem_t config;       // LoRaコンフィギュレーション
struct RecvFrameE220900T22SJP_t data; // 受信用構造体
RTC_DATA_ATTR bool atFirst = true;    // 初回起動判定用
RTC_DATA_ATTR int replyCount = 0;     // 返答(計測)回数
char msg[SEND_DATA_SIZE] = { 0 };

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  // AUXピンをプルアップしておく
  pinMode(LoRa_AUXPin, INPUT_PULLUP);

  // 計測を開始する
  dht.begin();

  // シリアル通信を開始して
  Serial.begin(115200);
  while (!Serial) {
    ;       // 安定を待つ
  }

  // 既定のLoRaコンフィギュレーションで初期設定をする
  lora.SetDefaultConfigValue(config);
  config.own_address = TERMINAL_NUMBER;
  if (lora.InitLoRaModule(config)) {
    SerialMon.printf("LoRa init error\n");
    //  return;       // 2回目以降で発生するエラーを無視する
  }

  // 2回目以降は GPIOのホールドを解除する
  if (!atFirst) {
    SerialMon.println("Wake up from deep sleep!");
    gpio_hold_dis((gpio_num_t)LoRa_ModeSettingPin_M0);
    gpio_hold_dis((gpio_num_t)LoRa_ModeSettingPin_M1);
    gpio_deep_sleep_hold_dis();
  }

  // 温度と湿度を計測して編集し
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  sprintf(msg, "%02d,%04d,%4.1f,%4.1f", TERMINAL_NUMBER, ++replyCount, t, h);
  SerialMon.printf("Transmission data: %s\n", msg);

  // ノーマルモードに移行して送信する
  lora.SwitchToNormalMode();
  SerialMon.println("Send data ... ");
  int state = lora.SendFrame(config, (uint8_t *)msg, sizeof(msg));
  if (state == 0)
    SerialMon.println(" ... send succeeded!");
  else
    SerialMon.println(" ... send failed!");
  delay(1000);     // <--- 重要!

  // WOR受信モードに移行する
  lora.SwitchToWORReceivingMode();
  SerialMon.println("Start sleep!");

  // ディープスリープ中の GPIOのホールド機能を設定する
  if (ESP_OK != gpio_hold_en((gpio_num_t)LoRa_ModeSettingPin_M0))
    Serial.println("HOLD [M0] ... failed!");
  if (ESP_OK != gpio_hold_en((gpio_num_t)LoRa_ModeSettingPin_M1))
    Serial.println("HOLD [M1] ... failed!");
  gpio_deep_sleep_hold_en();

  // スリープからの復帰条件を設定して
  esp_sleep_enable_ext0_wakeup((gpio_num_t)LoRa_AUXPin, LOW);

  // ディープスリープに入る
  Serial.println("Going to sleep now!\n");
  atFirst = false;
  esp_deep_sleep_start();
}

void loop() {
}


4.省電力LoRa端末の動作確認

 まず、esp32loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。続いてセンター側の ESP32をパソコンに USB接続してArduino IDEを開き、testTransmitter.inoを書き込みます。

(1) センター側のタスク:testTransmitter.ino

 シリアルモニターに "Start sending message!"と表示して、少し待つと "> Request to send message .... "に続いて送信時刻(起動からの経過秒数)、次に「Go ahead!」の送信メッセージ(計測指示)が表示されます。端末番号 1の LEDが点灯すると、やや待ってシリアルモニターに1号機の計測結果が次のように表示されます。
    Receive data: 01,0002,23.3,66.0
 1号機から2番目の計測結果、気温23.3℃、湿度66.0%が届いています。続いて2号機に対して同じような計測指示の送信と計測結果の受信が行われます。センター側の ESP32は、データ受信時に内蔵LEDが0.2秒点灯します。
 計測指示の送信から計測データの受信までの所要時間は約4.22秒です。
    


(2) 端末側のタスク:esp32loraTerminal.ino

 電源投入またはスケッチの書き込み完了による初回の実行では、内蔵LEDが点灯して計測とセンター宛の送信が行われます。必要な GPIOをホールドして、ウェークアップ条件を設定した後にディープスリープに入ります。内蔵LEDは消えて、WOR受信モードでセンターからの計測指示を待つ状態になります。
 計測指示が届くとウェークアップして内蔵LEDが点灯します。GPIOのホールドを解除し、計測結果を編集してノーマルモードで送信して、再び WOR受信モードでディープスリープに突入します。
 シリアルモニターにはこの状態が慌ただしく表示されます。"LoRa init error"のメッセージが気になりますが、これは前述のとおり問題ないので無視してください。
    

 下図は、省電力端末の受信時と送受信のシーケンスをオシロスコープで観察したものです。
    

 今回はこれで終わりです。かなり悪戦苦闘しましたが、省電力LoRa端末をシンプルなコードで実現することができました。LoRa通信デバイスとしてひとまず完成形ができたのではと思います。次回はセンター側の仕上をする予定です。
 お楽しみに!


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