1.LoRaモジュールの通信機能
繰り返しになりますが、このプロジェクトで使用する E220-900T22S(JP)は Private LoRa用です。LoRa変調モデムを使用してデジタル無線を使用する点では、LoRaWANや他の Private LoRa通信モジュールと同じですが、変調データの作成手順やデータエンコード、暗号方式などが異なっていて、それらと相互通信することはできません。簡単に自前の LoRa通信システムを構築して利用することを前提にしています。
(1) デバイスアドレスと通信方法
E220-900T22S(JP)の通信では、ソフトウェアの構造体で定義されたコンフィギュレーションによって、デバイスごとに IDを指定します。これはデバイスアドレスとも呼ばれ、複数のデバイスを識別するための番号で、符号無し16ビットです。
送信時には、送信チャンネルとターゲットのデバイスアドレスを指定してパケットを送信します。受信側は、待ち受けチャンネルと自身のデバイスIDを指定して受信待機します。
LoRaのパケットのフレーム構造は次のようになっています。
・Preamble: LoRa標準の同期用
・Header: ヘッダー(制御情報)
・Payload: 利用者データ(最大200バイト)
・CRC: 整合性チェック用ビット
送信先のデバイスアドレスとチャンネル情報は、ヘッダー部分に次のように設定されます。
・送信先アドレス 2 byte
・チャンネル情報 1 byte
ただし、アプリケーションレイヤーで独自のプロトコルを実装する場合は、ペイロードにアドレス情報を埋め込むことも可能です。
なお、内部的な制御情報はフレーム内に組み込まれていて、モジュール同士で適切に処理が行われます。

(2) ユニキャスト送信(デバイス指定送信)
ユニキャストは、送信チャンネルと送信先アドレスを指定してペイロードを送信します。LoRa通信パラメータ(BW: Band width 帯域幅、SF: Spreading Factor 拡散係数)、チャンネル、ターゲットアドレスの組がマッチしていて、なおかつ暗号化キーが合致している受信デバイスが、そのパケットを受信できます。
この仕組みを利用して、設計者は、マルチキャストやエニーキャストのような振る舞いをさせることが可能になります。また確認応答パケットを返信したいような場合に、待ち受けチャンネルと返信先のターゲットアドレスをペイロードに含めて送信し、受信側の MCUでそれをデコードして返信処理をするといった使い方もできます。
(3) ブロードキャスト送信
送信時のターゲットアドレスに「0xFFFF」を指定することで、全デバイスにパケットを送信することができます。ただしこの場合も、LoRa通信パラメータ、チャンネルと暗号化キーが合致していなければ受信できません。
また確認応答をさせることは可能ですが、複数のモジュールが同時に受信するため、返信タイミングが重複して混信の発生リスクをともなうことに注意しなければなりません。
(4) 動作状態のモニタ(AUXピンの機能)
E220-900T22Sの AUXピンを監視して、動作状態を確認することができます。送信予定の未送信データがあるかどうか、すべてのデータが UARTを介して送信されているかどうか、また、モジュールが通電直後の初期化、もしくはモード切替中などのセルフチェックの過程にあるかどうかのインジケーターとして使用できます。
これを行うためには TXD, TRDと AUXのタイミングシーケンスを確認して、TXDと RXDの動作状態と AUXの High/Low状態との関係を把握しておく必要があります。今回はさほどシビアな実験は行わないこと、また、多くの場合は delay()関数を使ってタイミング調整をすることで十分なことから、以下に主な機能だけをまとめておきます。
・動作状態の通知
Highレベル: モジュールがアイドル状態であることを示す。
Lowレベル: モジュールが作業中(送信または受信中)であることを示す。
・送信完了の通知
データ送信が完了すると、AUXが Lowから Highに変化する。
これによって、MCUは送信完了のタイミングを正確に把握できる。
・受信バッファの状態
受信バッファにデータがある場合、AUXは Lowレベルになる。
バッファが空になると Highレベルに復帰する。
・モジュールの初期化完了の通知
電源投入時やディープスリープからの復帰時、初期化が完了すると AUXは Highレベルになる。
2.LoRaモジュールの制御方法
(1) 利用手順
モジュールへの給電後はリセット動作を待ち、M0、M1ピンをいずれも Highレベルに保持します。この状態はコンフィギュレーションモードと呼び、LoRaモデムや電力増強アンプは停止状態にあり、消費電力が最も小さいディープスリープ状態です。
コンフィギュレーションモードにおいては、UARTのボーレートは 9600bpsに固定されています。
続いて M0、M1ピンを操作します。最もシンプルな使用方法は受信待機です。M0、M1ピンを Lowレベルに切り替えて保持すると、LoRaモデムが受信待ち受け状態になります。受信すると TXDピンから約 2ms後に受信パケットが出力され、MCUの RXDからデータを読み取ることができます。モジュールから出力されたデータは自動的に削除されるため、再度同じ受信パケットを読み込むことはできません。
(2) 送信モード
データの送信および受信を行う際には、送受信双方ともモジュールの LoRa変調の設定を合わせておく必要があります。データの送信は次の2つのモードで動作させることができます。
・固定(Fixed-block)送信モード
宛先デバイスを指定して通信を行う通常のモードです。
ペイロードデータの前に宛先デバイスアドレスと待ち受けチャンネルを指定する必要があります。
・透過(Transparent)送信モード
自身のデバイスアドレスおよび自身の待ち受け周波数チャンネルと同じ設定をした、他のモジュールに向けて
送信するモードです。
このモードではペイロードデータの前に宛先情報を付加する必要がなく、ペイロードだけを送信できます。
通常の固定送信モードでは、ペイロードのサイズ超えたデータは次の送信の制御情報と見なされるため、正しく送信できません。透過モードの場合は、ペイロードサイズが 200byteを超えた場合でも、超えたデータは次のパケットの送信データとして送信されます。
(3) WOR通信機能
WOR(Wake on Radio)機能は、受信電力の大幅な削減を可能にします。
WOR受信においては、8mAの消費電流を費やすわずかな時間の受信アンプ動作と、5μAの待機電流による長い待機動作の組み合わせを繰り返すことで、平均消費電流を大きく削減します。これは WORサイクル設定値によって、待機動作の動作時間比率が変わります。
WOR受信状態での動作比率の内、受信アンプ稼働時間内に WORプリアンブルが受信できるように、送信パケットにプリアンブルが追加されて送信します。WORサイクルの設定値が適切であれば、送信側での送信タイミングは気にする必要はありません。WORサイクルの設定値が送信側・受信側で同じでない場合、WOR送信プリアンブルと受信アンプの動作周期がかみ合わなくなり、パケット送達が低下したり不安定になるので注意が必要です。
(4) 動作モードの選択
M1と M0によって 4つの動作モードを設定することができます。
・mode0: M1=0、M0=0 (通常送受信)
通常のデータ送受信が可能です。
デフォルトでは通常送信モード(固定送信モード)が有効になります。
・mode1: M1=0、M0=1 (WOR送信)
送信時に WOR受信モードのデバイスをウェイクアップさせるためのプリアンブルが自動的に追加されます。
通常のデータ受信が可能で、受信機能は mode0と同じです。
・mode2: M1=1、M0=0 (WOR受信)
送信機能はオフになります。
WOR送信モード(mode1)で送信されたデータのみ受信可能です。
・mode3: M1=1、M0=1 (Configuration/DeepSleep)
コマンドでパラメータを設定することができます。
このモードでの UARTパラメータは固定されており、レジスタ設定に影響を受けません。
設定は、baud rate=9600、parity=8N1で行う必要があります。
これらの動作モードは、ライブラリ関数を使用することで簡単に設定できます。
3.LoRa通信ライブラリの使い方
(1) ヘッダーファイルの変更
ESP32と E220-900T22S(JP)モジュールとの接続関係を定義するために、先に一度修正した esp32_e220900t22s_jp_lib.hファイルをテキストエディターで開いてください。そしてピンアサインを、送受信機の配線に合わせて以下のように変更します。
// E220-900T22S(JP)へのピンアサイン
#define LoRa_ModeSettingPin_M0 25
#define LoRa_ModeSettingPin_M1 26
#define LoRa_RxPin 16
#define LoRa_TxPin 17
#define LoRa_AUXPin 27
(2) コンフィギュレーションの設定
モジュールの動作はすべて設定レジスターの内容に従います。設定レジスターは不揮発性で、給電を断った後も内容を保持します。
設定レジスターの内容は、ライブラリのヘッダーファイルにコンフィギュレーション構造体として、以下のように定義されています(コメントは筆者、[ ] 内はデフォールト値とその16進またはバイナリ値)。
struct LoRaConfigItem_t {
uint16_t own_address; // デバイスアドレス [0, 0x0000]
uint8_t baud_rate; // UARTのボーレート [9600bps, 0b11]
uint8_t air_data_rate; // [1758bps:SF=9:BW=125kHz, 0b10000]
uint8_t subpacket_size; // ペイロード長 [200byte, 0b00]
uint8_t rssi_ambient_noise_flag; // 環境ノイズ [無効, 0b0
uint8_t transmitting_power; // 送信出力 [13dBm, 0b01]
uint8_t own_channel; // 周波数チャンネル [0, 0x00]
uint8_t rssi_byte_flag; // RSSバイトの有効化 [無効, 0b0]
uint8_t transmission_method_type; // 送信方法 [トランスペアレントモード, 0b0]
uint16_t wor_cycle; // WORサイクル [2000ms, 0b11]
uint16_t encryption_key; // 暗号化キー [なし, 0x0000]
uint16_t target_address; // ターゲットアドレス [0, 0x0000]
uint8_t target_channel; // ターゲットチャンネル [0, 0x0000]
};
これらの設定は個別に行うこともできますが、次の関数を使って標準的な既定値を一括設定することができます。
void SetDefaultConfigValue(struct LoRaConfigItem_t &config)
※この関数を実行すると、上記コンフィギュレーション構造体の一部が次のように変更されます。
uint8_t rssi_ambient_noise_flag; // 環境ノイズ [有効, 0b1]
uint8_t rssi_byte_flag; // RSSバイトの有効化 [有効, 0b1]
uint8_t transmission_method_type; // 送信方法 [固定送信モード, 0b1]
※コンフィギュレーションの設定内容の詳細は、標記テクニカルデータシート「9.1 設定レジスタ」の表8~12に記載されています。
(3) クラスと初期化処理関数
ライブラリの使用にあたっては次のようにヘッダーファイルをインクルードし、ライブラリ CLoRaクラスのインスタンスを作成します。さらに、LoRa設定値を格納するコンフィギュレーション構造体の実体を定義します。
#include <esp32_e220900t22s_jp_lib.h>
CLoRa lora;
struct LoRaConfigItem_t config;
以上の準備をした後に、次のようにコンフィギュレーションに既定値を設定し、デバイスの初期化を行います。既定値の設定と初期化関数は、どちらもLoRa設定値格納先 configを引数で指定します。
lora.SetDefaultConfigValue(config);
if (lora.InitLoRaModule(config)) {
SerialMon.printf("LoRa init error\n");
return;
}
初期化関数は次のように定義されていて、初期化が成功すれば 0、失敗すれば 1を返します。
int InitLoRaModule(struct LoRaConfigItem_t &config);
(4) 送受信関数
送信処理ではデータをそのまま送信しますが、受信処理は既定の形式の構造体を使って受信します。構造体は次のような内容で、ヘッダーファイルで定義されています。
struct RecvFrameE220900T22SJP_t {
uint8_t recv_data[201];
uint8_t recv_data_len;
int rssi;
};
送信関数 SendFrameは、LoRa設定値を格納した configとデータ領域のアドレス、データのサイズを引数とします。データが文字列型データ配列 send_dataに格納されていて、データ長が sizeの場合、
int retv = SendFrame(struct LoRaConfigItem_t &config, uint8_t *send_data, int size);
のように指示します。
送信が成功すれば retには 0、失敗すれば 1が返されます。
受信では、その前に受信用構造体の実体を定義する必要があります。例えば次のように、構造体の実体に dataという名前を与えます。
struct RecvFrameE220900T22SJP_t data;
受信関数 ReceiveFrameの引数は、この dataのアドレスを指定して
int retv = ReceiveFrame(struct RecvFrameE220900T22SJP_t &data);
とすれば、受信が成功すると retvにゼロが、data.recv_dataには受信データが、また data.recv_data_lenには受信データのサイズ、data.rssiには受信RSSI(Received Signal Strength Indicator)が収納されています。失敗すると retvに 1が返されます。
(5) 動作モードの設定関数
目的の動作を行うように M0と M1を設定するため、以下の4つの関数が設置されています。
・ノーマルモード(M0=0,M1=0)へ移行する
void SwitchToNormalMode(void);
・WOR送信モード(M0=1,M1=0)へ移行する
void SwitchToWORSendingMode(void);
・WOR受信モード(M0=0,M1=1)へ移行する
void SwitchToWORReceivingMode(void);
・コンフィグモード(M0=1,M1=1)へ移行する
void SwitchToConfigurationMode(void);
4.簡単な送受信の実験
以上で見たように、LoRa通信ライブラリ関数はきわめてシンプルです。さっそくこれらを使って送受信の実験を行ってみましょう。まずは手始めに、2台の送受信機の1台を送信機として、もう1台は受信機として単一方向の通信を試します。
その前に、ESP32と LoRaライブラリのシリアル通信についてもう少し詳しく見ておきましょう。
ESP32には、UART0,UART1,UART2の 3つのシリアルポートがあります。各ポートには送信用の TX(または TXD)と、受信用の RX(または RXD)の 2つの信号端子があります。3つのシリアルポートの RXと TXは次のような GPIOピンが対応しています。
UART | RX | TX | 備 考 | |
UART0 | GPIO3 | GPIO1 | ArduinoIDEのシリアルモニタの入出力に使用 | |
UART1 | GPIO9 | GPIO10 | デフォルトでは使用できない | |
UART2 | GPIO16 | GPIO17 | ユーザー用に使用可 |
前回の送受信機の配線で明らかなように、ここでは UART2を使用しています。UART0はデフォールトでシリアルモニターに割り付けられていて、
Serial.begin(baud_rate);
を実行すれば、入出力(シリアル通信)を Serial.println()や Serial.read()のように行うことができます。これと同様に、2つ目のシリアル通信では次のような初期化処理が必要になります。
Serial1.begin(baud, config, RXD2, TXD2);
このように serial通信オブジェクト Serial1に引数の内容を割り当てます。引数の意味([ ]内は実際の指定値)は次のとおりです。
baud: 通信速度 [9600]
config: 既定の定数値 [SERIAL_8N1] 8ビット、パリティなし、1ストップビットの意味。
RXD2: UART2のRX [16]
TXD2: UART2のRX [17]
こうすると、Serial1.println()や Serial1.read()のようにして2つ目のシリアルポートと通信できるようになります。
しかし実際は(ライブラリ esp32_e220900t22s_jp_libを使用する限りは)、この初期化を利用者が行う必要はありません。前節「3.LoRa通信ライブラリの用法」の(1)でヘッダーファイルの LoRa_RxPinと LoRa_TxPinのアサインを設定しましたが、この値を使ってライブラリ内で初期化処理を行っているためです。
また、Serialと Serial1では可読性が良くないので、ライブラリヘッダーで次のように定義されています。Serialはそのまま使ってもかまいませんが、SerialMonと SerialLoRaとすることでコードが読みやすくなります。
// Set serial for debug console (to the Serial Monitor)
#define SerialMon Serial
// Set serial for LoRa (to the module)
#define SerialLoRa Serial1
(1) 送信タスクの作成
10秒毎に、送信回数と起動からの経過時間(秒数)を送信するタスク、loraTransmitter.inoを作成します。
なお、送信時には1秒間だけ内蔵LEDを点灯します。
コードの解説
①ヘッダーファイルとデータ等の定義
・12行目: LoRa通信ライブラリのヘッダーファイルをインクルードします。
・14行目: 内蔵(ビルトイン)LEDピンの指定です。
・15行目: データサイズを指定します。予定サイズ64バイトからヘッダーサイズの3バイトを差し引いています。
・17行目: ライブラリ CLoRaクラスのインスタンスを作成します。
・18行目: コンフィギュレーションを定義しています。
②初期化処理 setup部
コードのコメントのとおりです。LoRaの既定のコンフィギュレーション値でモジュールを初期化します。
そして、ノーマルモードに移行します。
③反復処理 loop部
一連の処理を実行後に内蔵LEDを1秒間点灯し、合わせて 10秒の待ちをつくっています。
・44行目: mills関数で ESP32の起動時からの経過時間(単位:ms)を取得します。
・48行目: メッセージの編集結果を送信します。
#include "esp32_e220900t22s_jp_lib.h"
#define LED_BUILTIN 2 // 内蔵LED
#define SEND_DATA_SIZE 61 // 64-3
CLoRa lora;
struct LoRaConfigItem_t config;
int sendCount = 0;
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();
SerialMon.println("Start transmit message!");
}
void loop() {
char msg[SEND_DATA_SIZE] = { 0 };
unsigned long tm = millis();
sprintf(msg, "Send count %04d: Passed time(sec) = %d", ++sendCount, tm / 1000);
SerialMon.println(msg);
if (lora.SendFrame(config, (uint8_t *)msg, sizeof(msg)) == 0) {
SerialMon.println("send succeeded.\n");
} else {
SerialMon.println("send failed.\n");
}
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(9000);
}
(2) 受信タスクの作成
次は受信タスク loraReceiver.inoの作成です。
常に受信待機し、受信があればシリアルモニターに内容と RSSI(受信信号強度: Received Signal Strength
Indicator)を表示して内蔵LEDを0.5秒間点灯します。
コードの解説
①ヘッダーファイルとデータ等の定義
・18行目: コンフィギュレーションとあわせて受信用の構造体を定義します。
②初期化処理 setup部
送信タスクの setup部と全く同じです。
③反復処理 loop部
・41行目: 受信が成功するまで(受信データが到着するまで)待ちます。
・43行目: 受信データをシリアルモニターに表示します。
・44行目: 続いて RSSIを表示します。
#include "esp32_e220900t22s_jp_lib.h"
#define LED_BUILTIN 2 // 内蔵LED
CLoRa lora;
struct LoRaConfigItem_t config;
struct RecvFrameE220900T22SJP_t data;
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();
SerialMon.println("Start receive message!");
}
void loop() {
if (lora.ReceiveFrame(&data) == 0) {
SerialMon.println("Received!");
SerialMon.printf(" ... %s\n", data.recv_data);
SerialMon.printf(" RSSI: %d dBm\n\n", data.rssi);
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
}
delay(1);
}
(3) 送受信の実験
以上でわかるように、送受信タスクのコードはきわめてシンプルです。このような簡潔な記述でデータ通信ができるのも LoRaの魅力です。
実験は、送信機と受信機を USBハブを介してノートブックPCに接続しています。Arduino IDEを立ち上げて、先に作成した送信タスクと受信タスクをロードし、それぞれ別の COMポートを割り付けてコンパイルと書き込みを行います。

【追記】
上の写真ではシンプルな USBハブを使用していますが、消費電力不足でしばしば USB接続が切れるなど不安定になりました。そこで、写真下のセルフパワーモード付きのものに置き換えました。これで問題が解消しました。

送信タスクのシリアルモニターを開くと、次のように送信が進行していることがわかります。

続いて受信機側のシリアルモニターを開くと、受信したメッセージ本文と RSSIの値が表示されています。送信機を見ると10秒毎(9秒おき)に内蔵 LEDが1秒間点灯し、それに被せるように受信機の LEDが短く点灯しています。

このように簡単な LoRa通信用ハードウェアで、少量データをスムーズに伝送できることが確認できました。
次回は当初の予定を変更して、シンプルな計測用端末を組み立ててみましょう。 お楽しみに!