こんにちは、ENGかぴです。
ZigBeeモジュールであるトワイライト(TWELITE)はセンサーの情報を取得するためにI2C通信機能があります。温度・湿度・気圧の情報が取得できるBME280センサーからI2Cを使用してデータを取得し無線通信してデータを確認しました。
BME280の製作メーカーであるBOSCH社から提供されているAPIを使用してセンサー情報の取得するまでの手順を説明しています。BME280はSPIにも対応しています。SPIを実装する方法については下記記事でまとめています。
トワイライト(TWELITE)のSPI通信を実装し無線通信する
トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。
トワイライト(TWELITE)のソフト開発と無線通信でできること
トワイライトの開発ツールであるMWSTAGEの新バージョン(MWSTAGE2020_10)がリリースがされたことでBME280のセンサー情報が取得できるBMx280-環境センサSNSが実装されました。
トワイライト(TWELITE)でBME280のデータを取得する
本記事では、追加されたBMx280-環境センサSNSを使用せずに外部のAPIを組み込む方法や考え方についてまとめています。
I2C通信を実装する
MWSTAGEの開発環境でI2Cでセンサーからの情報を取得し無線通信を実装する方法をまとめていきますが、無線通信を実装する方法については下記記事を参考にしてください。
トワイライト(TWELITE)にAD変換と無線通信を実装する
今回使用するのはストロベリーリナックスが販売しているBME280温湿度・気圧センサモジュール(I2C/SPIタイプ)を使ってI2C通信を実装していきます。センサーはBOSCH社が製作しておりストロベリーリナックス社がモジュール化して販売しているものを使用しています。
ストロベリーにナックス社だけでなく秋月電子やスイッチサイエンスでも同様のモジュールが販売されています。
MWSTAGEに組み込むため以下の手順に従って実装していきます。
- BME280のAPIをBOSCH社のHPからダウンロード
- BME280のAPIソフトをMWSTAGEに組み込む
- BME280のAPIに合わせてI2CのREAD/WRITEを実装
- モード設定に関する関数を追加する
- トワイライトのシリアルで温度・湿度・気圧の情報を表示する
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
BME280のドライバー(APIソフト)の準備
BME280のデータシートを確認すると温度・湿度・気圧に関して計算式や補正値による考え方が記載されていますが、BOSCH社が提供しているAPIを使用することが推奨されています。
データシートに記載されている計算式でも問題ないのですが、提供されているAPIによるものと比較するとAPIによるソースのほうが作りこまれているためAPIを使用したほうが良いでしょう。以降ではBME280のドライバーをBME280APIと表記します。
BME280APIをBOSCH社のHPからダウンロード
BOSCH社のHPからBME280ドライバーをダウンロードします。

下記リンクをクリックするとダウンロードのページに遷移します。
BoschSensortec-BME280_driver(Bosch社のBME280のAPI)
「Code」をクリックすると「Download ZIP」を選択するとファイルをまとめてダウンロードすることができます。ダウンロードが完了したら展開します。
BME280のAPIソフトをアクトに組み込む

MWSTAGEのAct_samples内のact0をコピーしてフォルダー名を変更します。フォルダー名は任意でも問題ありませが今回は「bme280」にしています。
このフォルダの中にダウンロードした「BME280_driver-master」の中から以下の3つをコピーして追加します。
- bme280.c
- bme280.h
- bme280_defs.h
ついでにact0.cppを「twe-bme280.cpp」に変更しています。
BME280APIに合わせてI2Cのread/writeを実装
BME280APIにはI2Cのreadとwriteの機能は実装されていません。ユーザー側で準備する必要があり、API側がコールしている型に合わせて関数を実装する必要があります。
actフォルダー内のbme280のAPIをコールするためにbme280.hを最初にインクルードします。
#include "bme280.h"
また計算結果をFloatを使わずにuint32_t形式で表示するために「bme280_defs.h」の以下をコメントアウトします。Floatで表示したい場合はコメントアウトの必要はありません。integerが32bit対応の場合は#define BME280_32BIT_ENABLEを追加します。
#ifndef BME280_64BIT_ENABLE /* Check if 64-bit integer (using BME280_64BIT_ENABLE) is enabled */
#ifndef BME280_32BIT_ENABLE /* Check if 32-bit integer (using BME280_32BIT_ENABLE) is enabled */
//#define BME280_32BIT_ENABLE //integerが32bit対応なら追加
#ifndef BME280_FLOAT_ENABLE /* If any of the integer data types not enabled then enable BME280_FLOAT_ENABLE */
//#define BME280_FLOAT_ENABLE
#endif
#endif
#endif
WireのAPIを使用する
I2Cを使用するにはWireに関するAPIを使用します。WireのAPIを実装しやすいようにモノワイヤレス社がMWSTAGE開発環境において準備しているAPIを使用することで簡単に使用することができます。
Wire.begin()を一番最初にコールする必要があります。begin()の引数を指定するとI2Cを使用するポートやバススピードを変更できますが、変更せずに使用したいので引数の指定はしません。初期化時に一度だけコールしておけばよいのでsetup()内でコールします。
void setup() {
Wire.begin();
}
I2Cの読み書きはヘルパークラス版(stream機能が使用可能)を使用します。オブジェクトを生成を行いオブジェクトを破棄した段階で利用終了の手続きが行えるためbeginTransmisson()、write()、endTransmission()などを呼びだしを気にする必要がないためです。
if(auto&& wrt = Wire.get_writer(bme280main.dev_id)){
wrt << reg_addr;
}
if(auto&& rdr = Wire.get_reader(bme280main.dev_id,(uint8_t)len)){
for(uint16_t i = 0; i < len; i++){
*reg_data = rdr();
reg_data++;
}
}
このソースコードのようにif文の中でオブジェクトを作成しif文でwrite/readすることで読み書きができます。if文から抜けたときオブジェクトは破棄されるためソースコードの記述漏れなどが防げます。
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
BME280APIに使用する変数とユーザーが準備する関数の実装
BME280APIを使用するためには2つの変数を準備する必要があります。
bme280_dev bme280main;
bme280_data sensor_data;
bme280_devはAPIを使用するための情報を格納する変数です。この変数に必要な情報を記述することでAPIとリンクすることができます。bme280_dataはAPIが計算した温度、湿度、気圧に関するデータを格納する変数です。
ユーザーが準備する関数についてはダウンロードしたファイルの中になる「README.md」をテキストもしくはVsCodeで開くと内容が確認できます。ポイントとなる関数は以下の通りになります。
- bme280_init(&dev)
- int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
- int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev)
- void user_delay_ms(uint32_t period)
- int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
- int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
1はAPIとしてコールすると使用できますが、コールする前にユーザー側でIDやread/writeに使用する関数のアドレスや遅延用の関数を登録する必要があります。void Bme280Init()という関数を自作してその中で変数の登録とbme280_init()をコールしています。
void Bme280Init(){
int8_t no;
bme280main.dev_id = BME280_I2C_ADDR_PRIM;
bme280main.intf = bme280_intf::BME280_I2C_INTF;
bme280main.read = user_i2c_read;
bme280main.write = user_i2c_write;
bme280main.delay_ms = user_delay_ms;
no = bme280_init(&bme280main);
if(no ==0){
bmeinitflg = true;
stream_sensor_data_forced_mode(&bme280main);
Serial << "init_ok" << mwx::crlf;
}else{
Serial << "init_ng "<< int(no) << mwx::crlf;
}
}
2は1回センサーの情報を取得するとスリープする機能です。3は繰り返してセンサーの値を計算する機能です。基本的に消費電力を抑えたいため2のforce modeを使用するためにセットアップします。2と4の関数の実装例は以下の通りです。
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
{
int8_t rslt;
uint8_t settings_sel;
uint32_t req_delay;
struct bme280_data comp_data;
dev->settings.osr_h = BME280_OVERSAMPLING_1X;
dev->settings.osr_p = BME280_OVERSAMPLING_16X;
dev->settings.osr_t = BME280_OVERSAMPLING_2X;
dev->settings.filter = BME280_FILTER_COEFF_16;
settings_sel = BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL;
rslt = bme280_set_sensor_settings(settings_sel, dev);
req_delay = bme280_cal_meas_delay(&dev->settings);
//while (1) {
rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, dev);
//dev->delay_ms(req_delay);
//rslt = bme280_get_sensor_data(BME280_ALL, &comp_data, dev);
//}
return rslt;
}
基本的に「READ.md」の内容はそのままでよいのですが、不要な部分をコメントアウトしている部分があります。while(1)をコメントアウトしておかないとループから抜け出せなくなります。
設定した後ウェイトを置いていますがこれも設定後トワイライトをスリープさせているときに設定が完了したらよいのでコメントアウトしています。3を使用する際も同様に使用できます。
void user_delay_ms(uint32_t period)
{
delay(period);
}
4の関数はmsのウェイトを持たせるために引数が指定されているのでdelay()関数を追加しています。
5の関数についてはセンサーのデータを通信する際に使用するものですが、引数が指定されているの引数を利用しながら組み込みAPIのステータスがリターンするように組み込む必要があります。
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
if( Wire.probe(dev_id)){ //デバイスが存在するか
if(auto&& wrt = Wire.get_writer(dev_id)){ //読み込みたいアドレスを指定
wrt << reg_addr;
}
if(auto&& rdr = Wire.get_reader(dev_id,(uint8_t)len)){ //len分データをread
for(uint16_t i = 0; i < len; i++){
*reg_data = rdr();
reg_data++; //ポインターを更新
}
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
最初にプロープ関数で対象のデバイスが存在するかの確認を行います。存在しなければBME280_E_DEV_NOT_FOUNDを戻り値として返すようにします。存在する場合はI2Cのフォーマットに従って通信を行います。
データを格納する変数はポインターで指定されているのでWire.get_reader()で読みだしたデータをポインタアドレスを更新しながらセットするようにしています。
int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
if( Wire.probe(dev_id)){ //デバイスが存在するか
if(auto&& wrt = Wire.get_writer(dev_id)){ //読み込みたいアドレスを指定
wrt << reg_addr;
for(uint16_t i = 0; i < len; i++){ //len分データをwrite
wrt << reg_data[i]; //*reg_data;でもよいがポインタの更新が必要
}
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
プローブ関数でデバイスの存在を確認した後で書き込みたいアドレスを指定します。次に書き込みたいデータをセットしていきます。
引数はポインター指定になっていますが、引数を指定している箇所をソースを見た時配列のアドレスが指定されているので配列のようにreg_data[]としてデータをセットしてもアドレスが更新されるので同じ結果になります。
無線通信を実装する
無線通信の実装方法については下記記事にまとめています。
トワイライト(TWELITE)にAD変換と無線通信を実装する
今回はセンサーの値を取得するためにショートスリープ(100ms)とスリープ(5秒)を実装します。
BME280の計測とスリープの関係

BME280の測定は数十ms必要です。この間TWELITEが動作していると消費電流が増えてしまいます。そこでBME280の測定をスタートしたときにTWELITEを100ms間スリープさせて消費電流を減らします。100msのスリープからウェイクアップした時にはBME280の測定は完了しているためBME280から測定データを受け取り無線通信を行います。
BME280のデータの読み込み
BME280APIでセンサーの情報を読み込むにはbme280_get_sensor_data()を使います。
if(bme280_get_sensor_data(BME280_ALL,&sensor_data, &bme280main) == 0 ){
bme280data[i++] = byte(sensor_data.temperature >> 24);
bme280data[i++] = byte(sensor_data.temperature >> 16);
bme280data[i++] = byte(sensor_data.temperature >> 8);
bme280data[i++] = byte(sensor_data.temperature & 0xFF);
bme280data[i++] = byte(sensor_data.pressure >> 24);
bme280data[i++] = byte(sensor_data.pressure >> 16);
bme280data[i++] = byte(sensor_data.pressure >> 8);
bme280data[i++] = byte(sensor_data.pressure & 0xFF);
bme280data[i++] = byte(sensor_data.humidity >> 24);
bme280data[i++] = byte(sensor_data.humidity >> 16);
bme280data[i++] = byte(sensor_data.humidity >> 8);
bme280data[i++] = byte(sensor_data.humidity & 0xFF);
}
BME280からデータが取得できた後は変数であるbme280data[]に値をセットしていきます。
uint32_tの型で計算していますが通信する際にペイロードとしてセットする際にバイトデータに変換する必要があるため演算をシフトしてbyteサイズにしてセットしています。
無線通信をモード管理
無線通信するまでをモード管理しています。送信セットした後にthe_twelite.tx_status.is_complete()で送信が完了したのを確認してスリープします。loop()から抜けながら送信管理していますが、アクトのSlp_Wk_and_Txのようにwhileで一気に送信した方が消費電流が抑えられるかもしれません。
if( TickTimer.available()){
switch (zigmode){
case ZIG_MD_ILDE:
break;
case ZIG_MD_SEND:
if( !b_transmit ){
txreq_stat = transmit();
if( txreq_stat ){
b_transmit = true;
//u8txid = ret.get_value() && 0xFF; //PALのACTサンプルを参照した
zigmode = ZIGBEE_MODE::ZIG_MD_SEND_WAIT;
timzigwait = ZIG_WAIT_MAX;
}else{
Serial << "..chk2." << mwx::crlf << mwx::flush;
sleepNow();
}
}
break;
case ZIG_MD_SEND_WAIT:
if (the_twelite.tx_status.is_complete(txreq_stat.get_value())){ //送信が完了したか
errres_cnt = 0;
sleepNow();
}else{
if(timzigwait == TIME_UP){
timzigwait = TIME_OFF;
if(++errres_cnt >= RES_CNT_MAX){ //3回連続で失敗したらリセット
the_twelite.reset_system();
}
else{
sleepNow();
}
}
}
break;
default:
break;
}
if(timzigwait > TIME_UP){
--timzigwait;
}
}
PAL_AMB-usenapのアクトの例のように送信したIDを保存して管理する方法でソースを組んでいましたがthe_twelite.tx_status.is_complete()を満たしても無線通信データを送信しないままスリープに遷移することがありました。
Slp_Wk_and_TxのようにMWX_APIRETの型で定義した変数に送信時の戻り値を保存するようにして送信IDをチェックしながら無線通信の送信完了を確認するように無線通信が終了したタイミングでスリープするようになりました。
マイコンはスタートアップ時に消費電流が増えるため無線通信がうまくいかなかった場合はスリープするようにし3回連続で失敗したときシステムリセットするようにしています。

動作確認
トワイライトのI2C通信を使用してBME280から温湿度と気圧のデータを取得してMONOSTICKにデータを送信します。MONOSTICKはParent-MONOSTICKアクトをそのまま使用しています。
回路図を考える

電源はTWE-EH-Sを使用します。TWELITEが動作開始したらTWE_GNDとGNDが切り離されないようにBOOTをLOWにします。5秒間隔のスリープと100msのスリープを使い分けながらBME280から取得したデータをMONOSTICKに送信します。
TWELITEとBME280の両方がスリープ時に電流を測定すると消費電流(約1.5uA)でした。ウェイクアップしたときに瞬時に電流が増加しますがテスターで確認できないほど短い周期で正確ではありませんが、約200uA程度でした。
BME280のデータシート見ると4.7kΩが推奨されているため起動できないなどの現象が生じた場合はR1とR2のプルアップを追加すると解消できる可能性があります。起動時にBME280に電源を確実に投入できていればこの現象は発生せずマイコンのプルアップでも十分です。
動作確認(TeraTermでMONOSTICKのシリアルデータを確認)

MONOSTICKのシリアルデータをTeraTermで表示し子機からの通信データを確認しました。FMT PACKETで子機からのデータを確認しました。(RAW PACKETでも子機からデータは同じです)子機から送信されたデータは緑(気温)・青(気圧)・赤(湿度)です。データの換算方法はBME280API関係の「README.md」内に記述があります。
- int32_t for temperature with the units 100 * °C
- uint32_t for humidity with the units 1024 * % relative humidity
- uint32_t for pressure
If macro "BME280_64BIT_ENABLE" is enabled, which it is by default, the unit is 100 * Pascal
If this macro is disabled, Then the unit is in Pascal
データを見ると気温は0x00000A01(2,561)となっており100倍値なので換算して25.61℃となります。気圧も同様に0x00991701(10,032,897)となり100倍値なので換算して100,328.97Pa=1,003.29hPaになります。
湿度は100倍値でなく1,024倍値となっているので注意が必要です。0x0000EB01(60,161)となり換算すると58.75%になります。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
リンクからZIPファイル形式のファイルをダウンロードし、任意の場所に展開していただくとテキストファイルが生成されます。
TWELITEのソースコードはMWSTAGEの環境でアクトのソースファイルにコピーすると使用できます。
センサーのオーバーサンプリングやフィルタについてはサンプルのままにしています。エナジーハーベスト電源など消費電流が気になる場合はオーバーサンプリングやフィルタを最低限にしておくとよいかもしれません。
関連リンク
トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。
トワイライト(TWELITE)のソフト開発と無線通信でできること
PR:アクセンチュアの転職なら【コンサルアクシスコンサルティング】
最後まで、読んでいただきありがとうございました。
偶々発見した現象ですがBME280の電源を入れ忘れた状態でI2Cが動作するとTWELITEのI2Cの出力が1.6Vと中途半端な出力になり消費電流が約8mAとなっておりTWELITEが起動できない状態になりました。