TWELITE(トワイライト)の自作アプリとソーラーモジュールで無線通信

組み込みエンジニア

こんにちは、ENGかぴです。

TWELITEの開発環境であるMWSTAGEでBME280から取得したデータを間欠動作しながら無線通信する自作のアクトを作りソーラーモジュールと組み合わせることで太陽光によるエナジーハーベスト電源によって無線通信を行いました。

無線タグアプリを使って夜間でも日中の太陽光で電気二重層にチャージした電源を使って無線通信した結果を下記記事にまとめています。

トワイライト(TWELITE)と太陽光パネルによる通信実験

自作アプリとソーラーモジュールで無線通信

ソーラー管理モジュールに付属している太陽光パネルはAM-5815(Panasonic)で最大出力電力が6mW(5.2V-1.1mA)となっています。室内光でもアプリを可能な限り省エネにしておくことで無線通信を繰り返し行うことができます。

TWE-EH-Sの動作

TWE-EH-Sの特徴と動作についてはモノワイヤレス社のHPを引用しながら自作アプリと組み合わせた場合の動作について説明します。

≪動作説明≫
① ソーラーパネルのエネルギーは、内蔵のコンデンサーC1(220uF)へ充電されます。
② C1の電圧(VC1)が約2.9V(VON)になると、TWE_VCCがGNDと接続され、TWELITEが動作 を開始します。
③ TWELITEは起動直後、すみやかにDO1(VBOOT)をLowにします。
④ TWELITEは無線送信します。
⑤ 無線送信後、TWELITEはスリープ状態になります。
④’⑤’スリープ復帰後に無線送信をして、再びスリープする動作を繰り返します。
⑥ エネルギーの供給不足により電圧が約2.0V(VOFF)を下回ると、TWELITEは動作を停止し ます。DO1(VBOOT)のLow状態が解除され、状態①へ戻ります。

モノワイヤレス株式会社:https://mono-wireless.com/jp/products/TWE-EH-SOLAR/EH_manual_v016.pdf
引用:モノワイヤレス株式会社:TWE-EH-SOLARのマニュアル
引用:モノワイヤレス株式会社:TWE-EH-SOLARのマニュアル

内蔵のコンデンサC1(220uF)のみでは TWE_VCCがGNDと接続された瞬間にアプリの初期化処理によって電圧が低下しますが電圧低下が大きくなり過ぎるとVBOOTが安定する前に TWE-EH-SがリセットをかけてしまいTWE_VCCとGNDが切り離されてしまいます。

この状態になると太陽光パネルが直射日光などで大きなエネルギーを持ったない限り安定した動作が期待できなくなります。

②の2.9VでONとありますが誤差もあるのか2.75V程度でONすることもありました。ハードでコントロールしているので周辺温度などの影響による誤差なのかもしれません。

また2.0Vを下回っても直ちにトワイライトが動作を停止することはありませんでした。ぎりぎりで動いている状態であり、この時通信タイミングになると電圧低下して停止してしまいます。

誤差もあると思いますが、1.92V付近までは動いているように感じました。TWE_VCCとTWE_GNDをテスターで当たっていて気付いた点です。

自作アプリとTWE-EH-Sの全体構成

自作アプリのTWELITEとTWE-EH-Sの全体構成
自作アプリのTWELITEとTWE-EH-Sの全体構成

動作説明のVBOOTは5ピンのDOによって制御します。BYPはピンのDOによって制御しますが余剰電力のコンデンサC2をバイパスさせる場合にLOWにします

C1は無線タグアプリの場合は220uFで十分ですが自作アプリで処理を追加している分消費電流が増えてしまい電圧降下が大きくなり起動できなくなることがあるため1000uFを追加し初期起動時の電圧降下の対策としています。

C2は余剰電力をチャージできるため容量の大きな電気二重層コンデンサなどを実装すると効果的です。2.2Fの電気二重層コンデンサでも曇り空でわずかに日が差す程度の環境下において1時間程度でフル充電できます。

BYPを初期起動時にLOWにするとC2が十分にチャージできていない場合C1にチャージした電荷がC2側に電荷が移ってしまうため電圧低下によりTWELITEのBOOTの制御が維持できなくなりTWE-EH-Sがリセット状態となります。

初期化時にはBYPを使用せず一定電圧以上になったことを確認してバイパスすることが効果的です。

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18;    //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19;    //Byp

void setup(){
    pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
    pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //BYPをHIGH
}
//BYPを使用する例
    if( vc2Dosetflg == false){
        if( vc2 >= AD_VCC_CHK3 && vc2 != AD_INIT_VALUE){
            vc2Dosetflg = true;
            pinMode(PIN_DO2, OUTPUT_INIT_LOW); //BYPをLOW
        }
    }

例ではVC2がAD_VCC_CHK3(2400:2.4V相当)以上かつ初期化時の値でない場合にBYPをLOWにしてバイパスしています。余剰電力の充電状況を確認するためVC2をA1に接続しています。

電気二重層コンデンサによるバックアップ時間

電気二重層コンデンサを挿入して電源をバックアップすることを考えていきます。バックアップ時間の考え方については下記記事を参考にしてください。

電気二重層コンデンサの静電容量によるバックアップ時間の考え方

電気二重層コンデンサの漏れ電流ILについて考えます。Q=CVとQ=Itの関係から上記記事を参考にして電気二重層コンデンサの容量を2.2Fとすると、$$I_L=\frac{2.2×0.8}{24×60×60}=20.37uA$$になります。これは最悪の条件での値になります。

次にトワイライトの間欠動作について消費電流を考えます。通信出力は23.4mAとなっているのでマージンを持たせて25mAとして計算します。

送信時間は0.1秒を目安にすることがモノワイヤレス社のHPに記載されているのでこの値を参照すると間欠動作を10分に設定した時の平均送信電流I1は以下のようになります。$$I_{1} = 25×0.1×\frac{1}{600} = 0.42uA$$となります。この電流にスリープ時の電流である1.5uAを加えたときの消費電流I2は、$$I_2= 0.42+1.5= 1.92uA$$となります。

BME280の消費電流I3も同様に考えると温湿度、気圧の計算は最大で3.6uA/秒であるから間欠動作が10分であれば平均電流はI3は以下のようになります。 $$I_{3} = 0.42×\frac{1}{599} = 0.07uA$$となります。この電流にスリーブ時の電流である0.1uAを加えた時の消費電流I4は、 $$I_4= 0.07+0.1= 0.17uA$$となります。

モジュールの消費電流と電気二重層コンデンサの漏れ電流分を合わせたものが失われる電流になるので合計の消費電流は\( I = I_L + I_2 + I_4 = 22.46uA\)となります。Q=Itの関係から$$Q = 22.46uA×s$$となり電気二重層コンデンサの電圧がフル充電の時は3.3Vであったのでトワイライト動作の下限値である2.0Vまでの電位差が1.3VとしてQ=CVの関係から電荷量を求めると$$Q = 2.2 × 1.3$$となることから

$$s = \frac{2.2 × 1.3 }{ 22.46×10^{-6}} = 127337秒 = 2122分 = 35.4時間$$となります。

電気二重層コンデンサは電圧を印加している時間が長いほど電荷が保持しやすくなり漏れ電流が小さくなるため最悪な条件になることはほとんどありません。

エナジーハーベスト電源のように微弱な電力であった場合は全体の電荷が不足することが多いため最悪な条件で考えておけば問題ないと考えています。

動作確認

TWE_VCCがGNDと接続されると初期化処理によってC1間の電圧が約0.05V電圧低下しました。C1に追加した1000uFのコンデンサによって電圧降下を押さえられています。1000uFを入れていない場合は約0.6V程度電圧低下するためリセット状態になり起動が安定しませんでした。

余剰電力はBYPを使用していな限りC1の電圧が3.3V付近になるとハード上の仕組みが動いてC2に電荷が溜まるようになっています。太陽光パネルの最大出力電圧が5Vであることからトワイライトの上限電圧を超えないようにうまく構成されています。

窓際でも直射日光が当たるほど明るい場合は抵抗で電力を消費しきれず電圧が上昇してしまいトライライトの上限電圧である3.6V付近まで電圧が上昇していたため注意が必要です。

動作確認の方法

昼間の明るい時間帯に窓際にモジュールを設置しMWSTAGEフォルダー内のtools内のTWELITEプログラマを起動して通信状況を確認します。TWELITEプログラマはターミナルログを保存することができるためターミナルで表示しながらログを保存します。

  1. 電気二重層コンデンサの容量は2.2F
  2. 自作アプリを利用し間欠動作の時限を10~15分
  3. 窓際に設置(就寝時間22:00以降はカーテンで室内光を遮断)
  4. 余剰電力が一度でもフル充電になっていることを確認
  5. 就寝前にC2の電圧を測定し、起床時(8:00)に電圧とターミナルを確認

自作アプリではTWE_VCCの値によって間欠動作のタイミングを切り替えているため平均して10分~15分で無線通信となります。

余剰電力の変化と確認
余剰電力の変化と確認

昼間の窓際で明るい場所にソーラーモジュールを設置して無線通信による電源の変化を確認すると1時間ほどでほぼフル充電になりました。曇り空の中で数分間太陽光パネルに直射日光が当たっていたため充電時間が早くなっています。

2.4VでBYPを使用するためC1とC2が並列に接続されるためTWE_VCC(C1)とVC2(C2)の電圧が同じになっています。

動作確認の結果

ターミナルによる動作確認
ターミナルによる動作確認

TWELITEプログラマのターミナル機能を使用して動作確認を行っています。22:00にカーテンを閉めて室内光を遮断してから起床時間である8:00の電源の様子を確認しました。

夜間動作の電圧値の変化の様子
夜間動作の電圧値の変化の様子

22:00時点でテスターTWELITE_VCCの電圧を測定すると3.26Vでしたログを確認して電圧をプロットしていくと30000(6:30位)まで電圧低下して3.16Vになっていました。8:00にテスターで電圧を測定すると3.19Vでした。明るくなるにつれて電圧が上昇しフル充電時の電圧に戻っています。

温度・湿度の変化
温度・湿度の変化

夜間に雨が降っており朝から曇り空であったため湿度が高めに出ています。朝窓を開けて換気したため温度と湿度が上昇しています。気圧については1000hPaからほとんど変化がないため省いています。

条件にもよりますが曇り空でも余剰電力をチャージすることができ室内光においても同様であるため理論上部品の寿命が来ない限り動作させることができそうです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

TWELITE-DIPに書き込むアクトのソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>
#include <SNS_BME280>
#include <STG_STD>

#define TIME_UP 0
#define TIME_OFF -1
#define TIME_OUT_MAX 100             
#define SLEEP_DUR_INIT 1000 //1s                  
#define SLEEP_DUR 299900 //5min
#define SLEEP_DUR2 599900 //10min        
#define SLEEP_DUR3 899900 //15min          
#define SLEEP_DUR4 1199900 //20min
#define AD_INIT_VALUE 0xFFFF
#define AD_VCC_CHK 3000 //3.0V
#define AD_VCC_CHK2 2700 //2.7V
#define AD_VCC_CHK3 2400 //2.4V

const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18;    //Boot
const uint8_t PIN_DO2 = mwx::PIN_DIGITAL::DIO19;    //Byp
const uint8_t PIN_AD1 = mwx::PIN_ANALOGUE::A1;		//TWE-EH-SのVCC

enum  APP_MODE{
    INTERACTIVE = 0,
    NORMAL
};

typedef union{
    uint8_t byte[6];
    struct{
        uint32_t app_di;
        uint8_t channel;
        uint8_t logical_id;       
    }dat;
}SYSTEM_TYP;

typedef struct{
    int16_t temp;
    int16_t humi;
    int16_t press;
}SENSOR_TYP;

enum  E_STATE {
    INIT = 0,
    WORK_JOB,
    WORK_JOB2,
    TX,
    WAIT_TX,
    EXIT_NORMAL,
    EXIT_FATAL
};

/* 変数宣言 */
APP_MODE appmode = APP_MODE::NORMAL;
SNS_BME280 sns_bme280;
SYSTEM_TYP twesystem;
MWX_APIRET txreq_stat;
E_STATE eState;
SENSOR_TYP getsensor;
bool b_senser_started = false;
bool b_transmit = false;
bool b_found_bme280 = false;
bool vc2Dosetflg = false;
uint32_t u32millis_tx;
uint32_t sleepTime = SLEEP_DUR;
int16_t timTxTimeout = TIME_OFF;
uint16_t twevcc = AD_INIT_VALUE;
uint16_t vc2 = AD_INIT_VALUE;

/* プロトタイプ宣言 */
void mainTimer(void);
MWX_APIRET transmit();
void napNow(void);
void sleepNow(void);

/*** the setup procedure (called on boot) */
void setup() {

    Serial.begin();

    auto&& set = the_twelite.settings.use<STG_STD>();
    set << SETTINGS::appname("MY_APP"); //アプリ名
    set << SETTINGS::appid_default(0x1234cdef); //デフォルトID

    pinMode(PIN_DIGITAL::DIO12, PIN_MODE::INPUT_PULLUP);
    if (digitalRead(PIN_DIGITAL::DIO12) == LOW) {
        set << SETTINGS::open_at_start(); //インタラクティブモード起動
	appmode = APP_MODE::INTERACTIVE; //loop()で動作しないようにするため                               
    }
    else{
        appmode = APP_MODE::NORMAL;
        pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
        pinMode(PIN_DO2, OUTPUT_INIT_HIGH); //BYPをHIGH

        Analogue.setup(true, ANALOGUE::KICK_BY_TICKTIMER);
        Analogue.begin(pack_bits(PIN_AD1,PIN_ANALOGUE::VCC),5);
       
        Wire.begin();
        sns_bme280.setup();
        if(sns_bme280.probe()){
            b_found_bme280 = true;
            //Serial << "bme280-ok" << crlf << mwx::flush;
        }

        set.reload();
        twesystem.dat.app_di = set.u32appid();
        twesystem.dat.channel = set.u8ch();
        twesystem.dat.logical_id = set.u8devid();
    
        txreq_stat = MWX_APIRET(false, 0);
	the_twelite
            << TWENET::appid(twesystem.dat.app_di)
	    << TWENET::channel(twesystem.dat.channel)
            << TWENET::tx_power(0x03);

        auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
	nwksmpl << NWK_SIMPLE::logical_id(twesystem.dat.logical_id);
	the_twelite.begin();
        //Serial << " MyApp-Start" << crlf << mwx::flush;
    }
}
/*** the loop procedure (called every event) */
void loop() {

    switch (appmode){
        case APP_MODE::INTERACTIVE:
            //インタラクティブモード
            break;
        case APP_MODE::NORMAL:
            bool loop_more;

  	    do{
	        loop_more = false;

                switch(eState){
                    case E_STATE::INIT: //デバイスの確認
                        sns_bme280.setup();
                        if(sns_bme280.probe()){
                            b_found_bme280 = true;
                            loop_more = true;
                            eState = E_STATE::EXIT_NORMAL;
                            //Serial << "bme280-ok" << crlf << mwx::flush;
                        }
                        else{
                            sleepNow();
                        }
                        break;
                    case E_STATE::WORK_JOB: //経過時間の通知
                        if (!sns_bme280.available()) {
                            sns_bme280.process_ev(1); 
                            //napにより100ms以上経過しているため1をセット0ではavailableとならない
                        }
                        loop_more = true;
                        eState =  E_STATE::WORK_JOB2;
                        break;
                    case E_STATE::WORK_JOB2: //測定データの取得
                        if(sns_bme280.available() && Analogue.available()){
                            twevcc = Analogue.read(PIN_ANALOGUE::VCC);
                            vc2 = Analogue.read(PIN_AD1);
                            vc2 = vc2 * 2; //TWE-EH-SのVC2は1/2なので2倍
                            getsensor.temp = sns_bme280.get_temp_cent();
                            getsensor.humi = sns_bme280.get_humid_per_dmil();
                            getsensor.press = sns_bme280.get_press();

                            //Serial 
                            //    << crlf << format("..%04d/finish sensor capture.", millis() & 8191)
                            //    << crlf << "  BME280: T=" << sns_bme280.get_temp() << "C"
                            //    << " H=" << sns_bme280.get_humid() << "%" 
                            //    << " P=" << sns_bme280.get_press() << "hPa" << crlf;
                            eState =  E_STATE::TX;
                            loop_more = true;
                        }
                        break;
                    case E_STATE::TX:
                        txreq_stat = transmit(); //データの取得準備
                            if (txreq_stat) {
                                timTxTimeout = TIME_OUT_MAX;
                                eState = E_STATE::WAIT_TX;
                                loop_more = true;
                            }
                            else {
                                timTxTimeout = TIME_OFF; 
                                eState = E_STATE::EXIT_FATAL;
                                loop_more = true;
                            }
                        break;
                    case E_STATE::WAIT_TX:
                        if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) { //送信完了ステータス待ち
                            //Serial << int(millis()) << ":tx completed! (" << int(txreq_stat.get_value()) << ')' << crlf;
                            eState = E_STATE::EXIT_NORMAL;
                        }
                        else if (timTxTimeout == TIME_UP ) {
                            //Serial << int(millis()) << "!FATAL: tx timeout." << crlf;
                            eState = E_STATE::EXIT_FATAL;
                            loop_more = true;
                        }
                        break;
                    case E_STATE::EXIT_NORMAL:
                        timTxTimeout = TIME_OFF; 
                        sleepNow();
                        break;
                    case E_STATE::EXIT_FATAL:
                        //Serial << crlf << "!FATAL: RESET THE SYSTEM.";
			delay(100);
			the_twelite.reset_system(); //異常とみなしリセット
                        break;
                }
            }while(loop_more);

            mainTimer();

            break;    
    }
}
/* callback begin */
void begin(){
    if(appmode != APP_MODE::INTERACTIVE){
        sleepNow();
    }
}
/* callback wakeup */
void wakeup(){

    if (b_senser_started) { //napからwakeup
        if( b_found_bme280 ){
            eState = E_STATE::WORK_JOB;
        }
        else{
            eState = E_STATE::INIT;
        }

        b_transmit = false;
        //Serial << "nap-wakeup" << crlf << mwx::flush;
    }
    else{ //sleepからwakeup
        b_senser_started = true;
        sns_bme280.begin();
        //Serial << "sleep-wakeup" << crlf << mwx::flush;
        napNow();
    }
}
/* ナップ(ショートスリープ)処理 */
void napNow(){
    uint32_t u32ct = 100;
    the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
/* スリープ処理 */
void sleepNow(){
    uint32_t u32ct;

    if( twevcc == AD_INIT_VALUE ){
        sleepTime = SLEEP_DUR_INIT;
    }
    else if( twevcc >= AD_VCC_CHK){
        sleepTime = SLEEP_DUR;
    }
    else if( (twevcc < AD_VCC_CHK) && ( twevcc > AD_VCC_CHK2) ){
        sleepTime = SLEEP_DUR2;
    }
    else if( (twevcc <= AD_VCC_CHK2) && (twevcc > AD_VCC_CHK3) ){
        sleepTime = SLEEP_DUR3;
    }
    else{
        sleepTime = SLEEP_DUR4;
    }

    if( vc2Dosetflg == false){
        if( vc2 >= AD_VCC_CHK3 && vc2 != AD_INIT_VALUE){
            vc2Dosetflg = true;
            pinMode(PIN_DO2, OUTPUT_INIT_LOW); //BYPをLOW
        }
    }

    u32ct = sleepTime + random(0,200);
    b_senser_started = false;
    b_transmit = false;
    the_twelite.sleep(u32ct);
}
/* 送信パケット準備 */
MWX_APIRET transmit() {

    if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
        pkt << tx_addr(0x00)  
	    << tx_retry(0x2) 
	    << tx_process_immediate();
             
        pack_bytes(pkt.get_payload(),
                   twevcc,
                   vc2,
                   (uint16)getsensor.temp,
                   (uint16)getsensor.humi,
                   (uint16)getsensor.press);
                   
	return pkt.transmit();
    }
    return MWX_APIRET(false, 0);
}
/* タイマ管理処理 */
void mainTimer(void){

    if( TickTimer.available()){
        if( timTxTimeout > TIME_UP ){
            --timTxTimeout;
        }
    }
}

MONOSTICKに書き込むアクトのソースコード:

#include <TWELITE>
#include <NWK_SIMPLE>
#include <MONOSTICK>

#define CNT_MAX 1000
/*** Config part */
const uint32_t APP_ID = 0x11223344;
const uint8_t CHANNEL = 13;

int16_t cnt1ms;
uint32_t ts;
bool rcvflg;
/*** function prototype */
void receive();

/*** setup procedure (run once at cold boot) */
void setup() {

    auto&& brd = the_twelite.board.use<MONOSTICK>();
    brd.set_led_red(LED_TIMER::ON_RX, 200); // RED (on receiving)
    brd.set_led_yellow(LED_TIMER::BLINK, 500); // YELLOW (blinking)

    the_twelite
        << TWENET::appid(APP_ID)   
	<< TWENET::channel(CHANNEL) 
	<< TWENET::rx_when_idle(); 

    auto&& nwksmpl = the_twelite.network.use<NWK_SIMPLE>();
    nwksmpl << NWK_SIMPLE::logical_id(0x00);
    the_twelite.begin(); // start twelite!
    Serial << "--- MONOSTICK_bme280 act ---" << mwx::crlf;
}

/*** loop procedure (called every event) */
void loop() {

    if(TickTimer.available()){
        ++cnt1ms;
    }

    while (the_twelite.receiver.available()) {
        receive();
	rcvflg = true;
    }

    if( cnt1ms >= CNT_MAX){
	cnt1ms -= CNT_MAX;
	ts = (ts + 1 ) % 100000;

	if(rcvflg){
            rcvflg = false;
	}
        else{
	    Serial << "ts:" <<  int(ts) << mwx::crlf << mwx::flush;;
	}
    }
}

/* 無線通信データ受信処理*/
void receive() {
    uint8_t u8DI_BM_remote = 0xff;
    uint16_t vcc;
    uint16_t vc2;
    uint16_t temp;
    uint16_t humi;
    uint16_t press;

    auto&& rx = the_twelite.receiver.read();
    Serial << "ts:" << int(ts) << ":" << int(rx.get_addr_src_lid());
    auto&& np = expand_bytes(rx.get_payload().begin(),
                             rx.get_payload().end()
                             ,vcc
	 	             ,vc2
		             ,temp
                             ,humi
		             ,press);

    Serial << " Vcc: " << (double)vcc/1000 << "V" 
           << " Vc2: " << (double)vc2/1000 << "V" 
	   << " temp: " << (double)temp/100 <<"C" 
	   << " humid: " << (double)humi/100 << "%" 
	   << " press: " << (int)press << "hPa"
	   << mwx::crlf << mwx::flush;
}

関連リンク

エナジーハーベストはIoT社会実現のために必要な技術であると考えています。電池レスでIoTモジュールが起動できるようになれば応用範囲が広がることが期待できます。エナジーハーベストの検討の一環として検討しているトワイライト(TWELITE)に関する記事をまとめています。興味があればご覧ください。

エナジーハーベスト技術がIoT社会実現に必須になりえる理由

トワイライト(TWELITE)のソフト開発と無線通信でできること

TECH::CAMPプログラミング教養【無料体験会】

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました