Ⅲ. シンプルな計測用端末の作成

 このシリーズでは、最終的に、複数の計測端末に中央から指示を与えて計測情報を収集できるシステムを目指しています。しかし、計測と送信はすべて端末に任せて、センター側は受信に徹するようなシンプルな仕組みも考えられます。ただしこの場合は、端末からの送信がぶつかる混信をどう防ぐかが課題になります。
 今回は、今後の課題でもある端末の省電力化について、ESP32のディープスリープ機能を試し、シンプルな計測用端末を作成してみることにします。
 なお、複数の端末が必要なことから、新たに LoRaモジュールとアンテナを1セット購入しました。今後は 2セットの端末とセンター側(受信側)1セットで実験を進めます。

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


1.シンプルな計測用端末の仕様

 計測端末に必要と考えられる機能などを列記すると以下のとおりです。

①番号による端末の識別

 複数端末を識別するために 1から 1番ごとに大きくなる(1, 2, ・・・)端末番号を設定すること。識別番号の体系は、送信タイミングの制御にも用いるのでこの原則に従うこと。

②開始信号待ち受け機能

 センター側からの開始シグナルの到着まで、初回の処理開始を待たせる仕組み。ただし、ディープスリープからの復帰時には待ち受けは不要。

③計測機能

 DHT11温度湿度センサーから温度と湿度を計測できること。初回起動時には、センサーが安定するために必要な待ち時間を確保すること。

④ディープスリープ機能

 計測間隔の間はディープスリープ状態を維持できること。復帰はタイマーによるウェークアップを採用する。

⑤初回起動の判定機能

 初回起動を判定できる仕組みを備えること。これには、ディープスリープでも内容を保持可能な RTCメモリー上に確保したカウンターの初期値を利用できる。

⑥稼働状態の確認機能

 実験段階でのみ必要な機能として、動作時とスリープ時を判別できること。例えば動作中に内蔵 LEDを点灯させるなど。

※センター側の機能:混信防止機能

 複数端末が同時に起動すると、計測データが同時に送信されることから混信が発生する。これを防ぐために、センター側は個々の端末に向けて時間差起動するように開始シグナルを送信する。指揮者がタクトを振るように、1番端末、2番端末、・・・・へと十分な間隔をおいて指示を出し、端末は上記の「②開始信号待ち受け機能」から脱出して処理を開始する。
 この処理は初回だけで、以降は受信に専念する。こうすることで、計測データは時間差をおいて届き、混信を避けることができる。

※制約事項

 混信防止機能を維持するためには次の条件を満たす必要があります。
    計測間隔 > ((端末台数 + 1)× 時間差 ) ...... 比較単位:秒

2.計測端末の組立

 送受信機を組み立てたブレッドボードの端に DHT11センサーを取り付けます。ESP32の 3V3ピンと DHT11の (+)ピンを位置合わせして写真のように配置します。そして DHT11の (OUT)と (-)ピンを、それぞれ ESP32の GPIO13と GNDにワイヤーで接続します。写真では、OUTが黄色、(-)が青色ワイヤーになっています。
 回路図では 3本の太線部分が今回の追加配線です。


    


3.計測用スケッチ:testDHT.ino

 DHT11センサーを使用するにはライブラリが必要になります。まだインストールしていない場合は、ここをクリックしてダウンロードしてください。DHT-sensor-library-master.zipがダウンロードされます。IDEで
   [スケッチ]-[ライブラリをインクルード]-[ZIP形式のライブラリをインクルード]
でインストールします。少し待つと「Library installed」と表示されて DHT sensor libraryがインストールされます。

 まず、DHT11センサーで温度と湿度を計測するためのコードを見ておきましょう。

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

 ・8行目: DHT11を使用するためにインクルードします。

  ②初期化処理 setup部

 ・18行目: DHT11センサーを初期化して計測を開始します。

  ③反復処理 loop部

 ・23, 24行目: それぞれ温度と湿度の計測値を取得します。
#include <DHT.h>

#define DHTTYPE     DHT11           // DHTセンサーの型式
 
/* DHTセンサー*/
const int DHTPin = 14;              // DHTセンサーの接続ピン
DHT   dht(DHTPin, DHTTYPE);         // DHTクラスの生成

void setup() {
  Serial.begin(9600);
  dht.begin();
  delay(1000);
}

void loop() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();

  // 温度を出力  
  Serial.printf("温度: %4.1f (℃)  湿度: %4.1f (%%)\n", t, h);
  delay(2000);
}

 実行するとシリアルモニターに温度と湿度を表示します。
    


4.ESP32のディープスリープとウェークアップ:testTimerWakeUp.ino

 計測用端末は電池駆動が中心になるので、いかに省電力化して電池寿命を延ばすかが重要になります。そのためには、処理が不要な間は CPUやその他の資源を休止状態にする必要があります。

(1) ESP32の省電力モード

 ESP32 Espressifデータシートによると、ESP32は、
  ・アクティブモード
  ・モデムスリープモード
  ・ライトスリープモード
  ・ディープスリープモード
  ・ハイバネーションモード
  ・休止モード
の電力モードを切り替えることができます。
 次の表は、休止状態を除くそれぞれの電力モードで何が起きているかを示すものです。モードによって、CPU、Wi-Fi・Bluetoothベースバンド、RTCメモリとRTC周辺機器、ULPコプロセッサーの動作状態がわかります。なお、ULP(Ultra Low Power)コプロセッサーは、ESP32がデュアルコアとは別に装備しているもう一つの低消費電力プロセッサーです。
 また、ハイバネーションは電源を切る直前の状態から動作を再開できる機能です。
 次の表は、それぞれの電力モードでの消費電流です。
 今回は、Arduino IDEで簡単に扱えて効果が大きいDeep sleepを使用することにします。

(2) Deep SleepとWake Up制御

 Deep Sleepからもとの状態に戻す、つまりウェークアップさせるには次の5種類の方法があります。
  ・タイマー
  ・タッチセンサー
  ・1つのGPIOピン(EXT0)
  ・複数のGPIOピン(EXT1)
  ・ULP(Ultra-Low-Power)コプロセッサ
 先の仕様で示したように、この計測用端末ではタイマーを使用して一定の時間ごとにディープスリープとウェークアップを反復させることにします。ウェークアップすると、リセットした時と同じように初期化処理から実行を再開します。実際の動作を次のスケッチで確認しておきましょう。このスケッチは Arduino IDEの ESP32「スケッチ例」にある DeepSleepの TimerWakeUp.inoを編集したものです。

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

 ・12行目: RTC_DATA_ATTRで定義した変数はスローメモリ領域に確保されて、ディープスリープ中でも内容が
      保持されます。
 ・17行目: ウェークアップの原因をシリアルモニターに表示する関数。
 ・20行目: ウェークアップの原因を取得します。

  ②初期化処理 setup部

 ・37行目: カウントアップの値はディープスリープ後も継続して保持されます。
 ・44行目: スリープ開始からウェークアップまでの時間指定。単位はμ秒。
 ・49行目: ディープスリープの開始指示です。直ちにスリープし、次の行が実行されることはありません。

  ③反復処理 loop部

  ここが実行されることはありません。
#define uS_TO_S_FACTOR 1000000ULL /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP  5          /* Time ESP32 will go to sleep (in seconds) */

RTC_DATA_ATTR int bootCount = 0;

/*
Method to print the reason by which ESP32 has been awaken from sleep
*/
void print_wakeup_reason() {
  esp_sleep_wakeup_cause_t wakeup_reason;

  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch (wakeup_reason) {
    case ESP_SLEEP_WAKEUP_EXT0:     Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case ESP_SLEEP_WAKEUP_EXT1:     Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case ESP_SLEEP_WAKEUP_TIMER:    Serial.println("Wakeup caused by timer"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD: Serial.println("Wakeup caused by touchpad"); break;
    case ESP_SLEEP_WAKEUP_ULP:      Serial.println("Wakeup caused by ULP program"); break;
    default:                        Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); break;
  }
}

void setup() {
  Serial.begin(9600);
  delay(1000);

  //Increment boot number and print it every reboot
  ++bootCount;
  Serial.println("\nBoot number: " + String(bootCount));

  //Print the wakeup reason for ESP32
  print_wakeup_reason();

  // First we configure the wake up source We set our ESP32 to wake up every 5 seconds.
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Setup ESP32 to sleep for every " + String(TIME_TO_SLEEP) + " Seconds");

  Serial.println("Going to sleep now\n");
  Serial.flush();
  esp_deep_sleep_start();
  Serial.println("This will never be printed");
}

void loop() {
  //This is not going to be called
}

 再起動を繰り返していますが、スローメモリに確保されたカウンターの値は加算されて Boot number:に表示されています。
    


5.シンプル計測システムの開発とテスト

 以上で試したことを利用して計測用端末を作成します。システム全体の概要は下図のとおりです。
 センター側では LoRaの初期化処理を行ってから、各計測端末向けに時間差をおいて開始シグナルを送信します。このために関数 kickOffTerminals()を設置しています。それが終わると専ら計測端末からの受信をし続けます。
 一方、計測端末は電源が入るかディープスリープから目覚めると、まず LoRaの初期化処理を行います。初回の起動であればセンターからの開始シグナルを待ち、開始シグナルを受信すると、センサーが安定するだけの時間待ちをします。これらの処理は2回目以降の動作時には実行されません。
 続いて温度と湿度を計測して送信用のフォーマットに編集し、センターへ送信します。そしてタイマー起動のタイマーを設定して、直ちにディープスリープに移行して電力消費を抑制します。タイマーでウェークアップすると、リセットした時と同様に先頭から処理を再開します。ただし、スローメモリ領域に確保されたデータは内容が保持されており、これを利用して初回の起動かそうでないかを判断しています。

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

 このタスクの肝は、各計測端末に時間差をおいて開始信号を送信する関数で、その他は前回の送信・受信タスクと変わりばえするものはありません。データを受信すると、内蔵LEDを0.2秒だけ点灯します。


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

 ・13行目: 今回は2台の端末でテストをします
 ・14行目: 1.5秒間隔で各計測端末への開始シグナルを送信します。
 ・24行目: これが計測端末への開始シグナル送信関数です。
 ・27行目: 端末台数分だけ以下を実行します。
 ・28行目: 初期化済みのコンフィギュレーションの送信先アドレス(target_address)に端末番号を設定します。
 ・29行目: 開始シグナルとしてメッセージを送信します。
 ・34行目: 次の端末を起動するまでの時間差分だけ処理を待たせます。

  ②初期化処理 setup部

 ・58行目: kickOffTerminals関数を実行するまでに、計測用端末の受信準備ができている必要があります。

  ③反復処理 loop部

 ・63行目: 計測端末からの受信があればメッセージ(データ)の内容を表示し、内蔵LEDを短く点灯します。
#include "esp32_e220900t22s_jp_lib.h"

#define LED_BUILTIN 2                 // 内蔵LED
#define NUMBER_OF_TERMINALS   2       // 端末台数
#define KICK_OFF_INTERVAL 1.5         // キックオフ間隔(秒)
#define SEND_DATA_SIZE 29             // 32-3

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

/*
  各端末を順次キックオフする
*/
void kickOffTerminals() {
  char msg[SEND_DATA_SIZE] = "Go ahead!";

  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);
    delay(KICK_OFF_INTERVAL * 1000);
  }
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(9600);
  delay(1000);
 
  // 既定のLoRaコンフィギュレーションを取得して
  lora.SetDefaultConfigValue(config);             

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

  // ノーマルモードへ移行して
  SerialMon.printf("switch to normal mode\n");
  lora.SwitchToNormalMode();
  delay(10);

  // 計測端末を始動する
  kickOffTerminals();
  SerialMon.println("Start receive data!");
}

void loop() {
  if (lora.ReceiveFrame(&data) == 0) {
    SerialMon.println("Received!");
    SerialMon.printf(" ... %s\n", data.recv_data);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);
    digitalWrite(LED_BUILTIN, LOW);
  }
  delay(1);
}

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

 初回の起動なら、コンフィギュレーションに自身の端末番号(own_address)を設定して、センターからの開始メッセージの着信を待ちます。開始メッセージが届くとセンサーが安定するまで5秒間待ちます。この初めての起動を判定するためには、RTCメモリに確保したブート回数のカウンターを利用します。
 計測結果は次の形式に編集して送信します。
   aa:nnnn=tt.t,hh.h
     aa: 端末識別番号(01~99)
     nnnn: ブート回数(0001~9999)
     tt.t: 温度
     hh.h: 湿度
 センター側の own_addressは既定でゼロであり、端末のコンフィギュレーションの送信先アドレス(target_address)も既定値がゼロになっているので、特に指定しなくてもセンターへ向けて送信されます。
 送信後はウェークアップ用のタイマーをセットして、直ちにディープスリープに入ります。


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

 ・14~15行: スケッチ書き込み前に、各端末ごとに重複しない識別番号(1,2,3,・・・・)を設定します。
 ・20行目: 計測間隔を10秒に設定しています。

  ②初期化処理 setup部

 ・35行目: ディープスリープ状態でなければ内蔵LEDが点灯し続けます。動作状態の確認用です。
 ・45~46行: own_addressに自身の端末番号を設定して初期化します。
 ・56行目: 初回を判定します。
 ・58行目: 開始メッセージを受信するまで待ちます。
 ・61行目: センサーが安定するまで5秒だけ待ちます。
 ・67行目: bootCountをカウントアップして計測結果を編集します。
 ・71行目: 計測データを送信します。
 ・78行目: ウェークアップタイマーを設定します。
 ・80行目: ここでディープスリープに入ります。

  ③反復処理 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センサーの型式
#define uS_TO_S_FACTOR 1000000ULL     // μSec--->Sec変換係数
#define TIME_TO_SLEEP  10             // スリープ時間(秒)
 
/* 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 int bootCount = 0;      // RTCメモリ上に確保

void setup() {
  char msg[SEND_DATA_SIZE] = { 0 };

  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);

  Serial.begin(9600);
  delay(1000);
  dht.begin();

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

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

  // ノーマルモードへ移行して
  SerialMon.printf("switch to normal mode\n");
  lora.SwitchToNormalMode();

  // 初回なら起動指示を受信してセンサーの安定を待つ
  if (bootCount == 0) {
    while (1) {
      if (lora.ReceiveFrame(&data) == 0)
        break;
    }
    delay(5000);
  }

  // 温度と湿度を計測して編集する
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  sprintf(msg, "%02d:%04d=%4.1f,%4.1f", TERMINAL_NUMBER, ++bootCount, t, h);
  SerialMon.printf("送信データ: %s\n", msg);

  // 計測データを送信する
  if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
    SerialMon.println("send succeeded.\n");
  } else {
    SerialMon.println("send failed.\n");
  }

  // タイマーを設定してディープスリープに入る
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("Going to sleep now\n");
  esp_deep_sleep_start();
}

void loop() {
}

(3) 送受信の実験

 まず、loraTerminal.inoを書き込んだ 2台の計測用端末の電源を入れます。内蔵LEDが点灯した状態になります。
 センター側の ESP32を USB接続してシリアルモニターを開き、loraCentral.inoを書き込みます。少し待つと 1号端末の LEDが消えて、同時にセンター側の LEDが瞬間的に点灯します。1.5秒後に 2号端末で同様の状態が起きます。
 センター側タスクのシリアルモニターを開くと、次のように各計測用端末からのデータ受信が進行していることがわかります。計測用端末の LEDはウェークアップ時だけ点灯するので、ディープスリープの状態を確認することができます。

    


※今後の課題

 このように簡潔なコードを記述するだけで、複数の計測端末からのデータ収集システムを構築できます。しかし、実際の運用にあたっては、さらにいくつかの点について考慮する必要があります。最後にそれらを列記しておきます。
 ○計測データの欠測値への対応
   計測に失敗すると、計測値として文字列 "nan"が通知されるので対応が必要です。
 ○計測時刻の付加
   計測には日時情報が不可欠です。センター側でデータ受信の正確な日時を付加する必要があります。
 ○計測間隔と端末台数の検討
   前述しましたが、すべての端末の起動間隔の合計は計測間隔よりも小さいことが必要です。
 ○その他
   この方式では、計測端末の性能のバラツキなどによる送信タイミングのズレで発生する混信に対応できません。
   その心配が無い方式については次回以降で検討することになります。


 次回は ESP32のマルチタスク機能を利用して、センターと計測端末の双方向通信を試みます。 お楽しみに!


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