こんにちは、ENGかぴです。
ZigBeeモジュールであるトワイライト(TWELITE)はセンサーの情報を取得するためにSPI通信機能があります。温度・湿度・気圧の情報が取得できるBME280センサーからSPIを使用してデータを取得し無線通信してデータを確認しました。
BME280はI2C通信とSPI通信に対応したモジュールです。下記記事で紹介しているようにBME280用に提供されているAPIをMWSTAGEのアクトに組み込んでSPI通信を行います。
トワイライト(TWELITE)のI2C通信を実装し無線通信する
共通する部分については全体のソースコードには示しますが、考え方は上記記事を参考にしてください。SPIについては下記リンクが参考になります。
トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。
トワイライト(TWELITE)のソフト開発と無線通信でできること
トワイライトの開発ツールであるMWSTAGEのバージョン(MWSTAGE2020_10)がリリースがされたことでBME280のセンサー情報が取得できるBMx280-環境センサSNSが実装されました。
本記事では、追加されたBMx280-環境センサSNSを使用せずに外部のAPIを組み込む方法や考え方についてまとめていますので参考にしていただければと思います。
BME280APIの準備と実装
今回使用するのはストロベリーリナックスが販売しているBME280温湿度・気圧センサモジュール(I2C/SPIタイプ)を使ってSPI通信を実装していきます。センサーはBOSCH社が製作しておりストロベリーリナックス社がモジュール化して販売しているものを使用しています。
MWSTAGEに組み込むため以下の手順に従って実装していきます。
- BME280のAPIをBOSCH社のHPからダウンロード
- BME280のAPIソフトをMWSTAGEに組み込む
- BME280のAPIに合わせてSPIのREAD/WRITEを実装
- モード設定に関する関数を追加する
- トワイライトのシリアルで温度・湿度・気圧の情報を表示する
手順1.2.については「トワイライト(TWELITE)のI2C通信を実装する」にダウンロードの手順をまとめています。以降ではBME280のドライバーをBME280APIと表記します。
MWSTAGEのアクトのフォルダー名やファイル名は任意ですがフォルダー名を「bme280-spi」とし、ファイル名を「bme280-spi.cpp」とし準備するとよいと思います。
BME280APIに合わせてSPIのread/writeを実装
BME280APIにはSPIの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
SPIのAPIを使用する(トワイライト)
SPIのAPIを実装しやすいようにモノワイヤレス社がMWSTAGE開発環境においてAPIを準備しており使用することで簡単にSPIが実装できます。
SPI.begin()を最初の初期化時とスリープ復帰後にコールする必要があります。begin()の引数を指定するとSPIのバススピードやスレーブセレクトピンやSPIモードを指定できます。初期化時のsetup()内やwakeup()内でコールします。
最低でも2か所で使用することになるためSPI.begin()の変更が一か所で済むようにspi_init_set()の関数を作りコールするようにしています。
void spi_init_set(){
SPI.begin(0, SPISettings(2000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));
}
BME280のデータシートによると最大クロックは10MHzまで対応できます。トワイライトは16MHzを偶数で分周したクロックが指定できるので2MHz、4MHz、8MHzの周波数が設定できます。例では2MHzにしています。
SPIの読み書きはヘルパークラス版(stream機能が使用可能)を使用します。オブジェクトを生成を行いオブジェクトを破棄した段階で利用終了の手続きが行えるためbeginTransmisson()、endTransmission()などを呼びだしを気にする必要がないためです。
if(auto&& trs = SPI.get_rwer()){ //Writeの場合
trs << reg_addr;
for(uint16_t i=0; i < len; i++){
trs << reg_data[i];
}
}
if(auto&& trs = SPI.get_rwer()){ //Readの場合
trs.transfer(reg_addr);
for(uint16_t i=0; i < len; i++){
reg_data[i] = trs.transfer( 0x00 );
}
}
Readの場合スレーブに供給するためのクロックを出力するために0x00をダミーとしてセットしてクロックを出力してスレーブのデータを取得しています。
このソースコードのようにif文の中でオブジェクトを作成しif文でwrite/readすることで読み書きができます。if文から抜けたときオブジェクトは破棄されるためソースコードの記述漏れなどが防げます。
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_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
- int8_t user_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
1はAPIとしてコールすると使用できますが、コールする前にユーザー側でread/writeに使用する関数のアドレスや遅延用の関数を登録しておく必要があります。void Bme280Init()という関数を自作して変数の登録とbme280_init()をコールしています。
void Bme280Init(){
int8_t no;
bme280main.dev_id = 0;
bme280main.intf = bme280_intf::BME280_SPI_INTF;
bme280main.read = user_spi_read;
bme280main.write = user_spi_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.3.についてはI2Cのと同様であるため割愛します。「トワイライト(TWELITE)のI2C通信を実装し無線通信する」を参考にしてください。
int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
if(auto&& trs = SPI.get_rwer()){
trs.transfer(reg_addr); //読み込みたいアドレスを送信
for(uint16_t i=0; i < len; i++){
reg_data[i] = trs.transfer( 0x00 ); //ダミーで0を送信(スレーブにクロックを出す)
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
最初にSPIオブジェクトを生成します。生成できなければBME280_E_DEV_NOT_FOUNDを戻り値として返すようにします。スレーブのアドレスを選択して送信して後にリードしたいデータ長分(Len)分だけスレーブデータを読み出します。
トワイライト(マスター)が出力するクロックに同期してBME280(スレーブ)がデータを返信するため読み取る際もマスターからクロックを出力させるためにダミーとして0x00をセットしています。
int8_t user_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0; /* Return 0 for Success, non-zero for failure */
if(auto&& trs = SPI.get_rwer()){
trs << reg_addr; //書き込み先のアドレス
for(uint16_t i=0; i < len; i++){
trs << reg_data[i]; //書き込むデータ
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
Writeの場合スレーブアドレスをセットした後書き込みたいデータをサイズ分送信することで書き込みができます。BME280のSPIは1バイト毎しかライトできない仕様になっているのでLenは常に1になるようです。
引数はポインター指定になっていますが、引数を指定している箇所をソースを見た時配列のアドレスが指定されているので配列のようにreg_data[]としてデータをセットしてもアドレスが更新されるので同じ結果になります。
無線通信を実装する
スリープで消費電流を抑えながら無線通信の実装方法については下記記事にまとめています。BME280の計測とスリープの関係やBME280の読み込みデータの扱い方について説明しています。
トワイライト(TWELITE)のI2C通信を実装し無線通信する
ポイントだけ整理するとショートスリープでBME280の計測をスタートさせスリープすることで消費電流を抑えます。BME280のデータは4バイトデータですが無線通信のため1バイトに分割して無線通信を行う必要があります。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
動作確認
トワイライトのSPI通信を使用してBME280から温湿度と気圧のデータを取得してMONOSTICKにデータを送信します。MONOSTICKはParent-MONOSTICKアクトをそのまま使用しています。
動作確認用の回路図
電源はTWE-EH-Sを使用します。TWELITEが動作開始したらTWE_GNDとGNDが切り離されないようにBOOTをLOWにします。5秒間隔のスリープと100msのスリープを使い分けながらBME280から取得したデータをMONOSTICKに送信します。
TWELITEとBME280の両方がスリープ時に電流を測定すると消費電流(約1.5uA)となっていました。ウェイクアップしたときに瞬時に電流が増加しますがテスターで確認できないほど短い周期で正確ではありませんが、約200uA程度でした。I2C使用時と同じでした。
動作確認(TeraTermでMONOSTICKのシリアルデータを確認)
MONOSTICKのシリアルデータをTeraTermで表示し子機からの通信データを確認しました。FMT PACKETで子機からのデータを確認しました。(RAW PACKETでも子機からデータは同じです)子機から送信されたデータは緑(気温)・青(気圧)・赤(湿度)です。デ
データを見ると気温は0x000009A6(2,470)となっており100倍値なので換算して24.70℃となります。気圧も同様に0x009A4C6E(10,112,110)となり100倍値なので換算して101,121.10Pa=1,011.21hPaになります。
湿度は100倍値でなく1,024倍値となっているので注意が必要です。0x0001101E(69,662)となり換算すると68.03%になります。
無線通信をモード管理
無線通信するまでをモード管理しています。送信セットした後に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{
sleepNow();
}
}
break;
case ZIG_MD_SEND_WAIT:
if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) { //送信が完了したか
sleepNow();
}else{
if(timzigwait == TIME_UP){
timzigwait = TIME_OFF;
if(++errres_cnt >= RES_CNT_MAX){
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回連続で失敗したときシステムリセットするようにしています。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <TWELITE>
#include <NWK_SIMPLE>
#include "bme280.h"
#define UINT8_C(val) val
#define INT8_C(val) val
#define TIME_UP 0
#define TIME_OFF -1
#define ZIG_WAIT_MAX 10
#define DATASZ 12 //uint32のデータを3つ送る
#define SLEEP_MAX 4800
#define RES_CNT_MAX 3
#define FORCED_MODE //省エネモードを使用するとき定義する
enum ZIGBEE_MODE{
ZIG_MD_ILDE = 0,
ZIG_MD_SEND,
ZIG_MD_SEND_WAIT,
ZIG_MD_MAX
};
const uint8_t PIN_DO1 = mwx::PIN_DIGITAL::DIO18; //Boot
/*** Config part */
const uint32_t APP_ID = 0x1234abcd; // application ID
const uint8_t CHANNEL = 13; // channel
// application use
bme280_dev bme280main;
bme280_data sensor_data;
bool b_senser_started = false;
ZIGBEE_MODE zigmode;
bool b_transmit = false;
int16_t timzigwait = TIME_OFF;
int16_t timzigwait2 = TIME_OFF;
uint8_t u8txid = 0;
uint8_t u8devid = 0x00; //parent
byte bme280data[DATASZ];
bool bmeinitflg = false;
MWX_APIRET txreq_stat;
uint8_t errres_cnt =0;
/*** Local function prototypes */
void Bme280Init();
bool startSensorCapture();
void sleepNow();
void napNow();
MWX_APIRET transmit();
void mainTimer();
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);
int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
int8_t user_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
void user_delay_ms(uint32_t period);
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev);
int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev);
void spi_init_set();
/*** the setup procedure (called on boot) */
void setup() {
pinMode(PIN_DO1, OUTPUT_INIT_LOW); //BOOTをLOW
spi_init_set();
txreq_stat = MWX_APIRET(false,0);
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(0xFE);
the_twelite.begin(); // start twelite!
}
/*** the loop procedure (called every event) */
void loop() {
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{
sleepNow();
}
}
break;
case ZIG_MD_SEND_WAIT:
if (the_twelite.tx_status.is_complete(txreq_stat.get_value())) { //送信が完了したか
sleepNow();
}else{
if(timzigwait == TIME_UP){
timzigwait = TIME_OFF;
if(++errres_cnt >= RES_CNT_MAX){
the_twelite.reset_system();
}
else{
sleepNow();
}
}
}
break;
default:
break;
}
if(timzigwait > TIME_UP){
--timzigwait;
}
}
}
/* callback begin */
void begin(){
Bme280Init();
sleepNow();
}
/* callback wakeup */
void wakeup(){
spi_init_set();
if (!b_senser_started) {
#ifdef FORCED_MODE
if( bme280_set_sensor_mode(BME280_FORCED_MODE, &bme280main)==0 && bmeinitflg ){
b_senser_started = true;
napNow();
}else{
Bme280Init();
sleepNow();
}
#else
b_senser_started = true;
napNow();
#endif
}else{
if( startSensorCapture()){
zigmode = ZIGBEE_MODE::ZIG_MD_SEND;
b_transmit = false;
}else{
sleepNow();
}
}
}
/* Bme280 api use function add */
void spi_init_set(){
SPI.begin(0, SPISettings(2000000, SPI_CONF::MSBFIRST, SPI_CONF::SPI_MODE3));
}
/* Bme280 sensor iniialize */
void Bme280Init(){
int8_t no;
bme280main.dev_id = 0;
bme280main.intf = bme280_intf::BME280_SPI_INTF;
bme280main.read = user_spi_read;
bme280main.write = user_spi_write;
bme280main.delay_ms = user_delay_ms;
// if( Wire.probe(bme280main.dev_id)){
no = bme280_init(&bme280main);
if(no ==0){
bmeinitflg = true;
}
#ifdef FORCED_MODE
stream_sensor_data_forced_mode(&bme280main);
#else
stream_sensor_data_normal_mode(&bme280main);
#endif
//}
}
/* sensor data read */
bool startSensorCapture(){
bool ret = true;
int8_t i=0;
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);
}else{
ret = false;
}
return ret;
}
/* perform period sleep */
void sleepNow(){
uint32_t u32ct = 4800 + random(100);
b_senser_started = false;
b_transmit = false;
the_twelite.sleep(u32ct);
}
/* perform short period sleep */
void napNow() {
uint32_t u32ct = 100;
the_twelite.sleep(u32ct, false, false, TWENET::SLEEP_WAKETIMER_SECONDARY);
}
/* transmit a packet */
MWX_APIRET transmit() {
if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
pkt << tx_addr(u8devid)
<< tx_retry(0x3)
<< tx_packet_delay(0,0,2);
for(int i = 0; i < DATASZ; i++){
pack_bytes(pkt.get_payload(),bme280data[i]);
}
return pkt.transmit();
}
return MWX_APIRET(false, 0);
}
/* Bme280 api use function add */
void user_delay_ms(uint32_t period)
{
delay(period);
}
/* Bme280 api use function add */
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
{
int8_t rslt;
uint8_t settings_sel;
uint32_t req_delay;
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, &sensor_data, dev);
//}
return rslt;
}
/* Bme280 api use function add */
int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev)
{
int8_t rslt;
uint8_t settings_sel;
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;
dev->settings.standby_time = BME280_STANDBY_TIME_62_5_MS;
settings_sel = BME280_OSR_PRESS_SEL;
settings_sel |= BME280_OSR_TEMP_SEL;
settings_sel |= BME280_OSR_HUM_SEL;
settings_sel |= BME280_STANDBY_SEL;
settings_sel |= BME280_FILTER_SEL;
rslt = bme280_set_sensor_settings(settings_sel, dev);
rslt = bme280_set_sensor_mode(BME280_NORMAL_MODE, dev);
//while (1) {
dev->delay_ms(70);
rslt = bme280_get_sensor_data(BME280_ALL, &sensor_data, dev);
//}
return rslt;
}
/* Bme280 api use function add */
int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
if(auto&& trs = SPI.get_rwer()){
trs.transfer(reg_addr);
for(uint16_t i=0; i < len; i++){
reg_data[i] = trs.transfer( 0x00 );
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
/* Bme280 api use function add */
int8_t user_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
if(auto&& trs = SPI.get_rwer()){
trs << reg_addr;
for(uint16_t i=0; i < len; i++){
trs << reg_data[i];
}
}else{
rslt = BME280_E_DEV_NOT_FOUND;
}
return rslt;
}
センサーのオーバーサンプリングやフィルタについてはサンプルのままにしています。エナジーハーベスト電源など消費電流が気になる場合はオーバーサンプリングやフィルタをOFFにしておくとよいかもしれません。
関連リンク
トワイライトを太陽光パネルで動作させたことやMWSTAGEの環境でソフト開発して無線通信したことなどについてまとめています。
トワイライト(TWELITE)のソフト開発と無線通信でできること
最後まで、読んでいただきありがとうございました。