TWELITEをプログラムする(BME280を動かす その4)

いつのまにかこのシリーズも4回目になってしまいましたが、前回は、子機側のプログラムをいじって、余分なデータを送信しないように修正しました。
なお、BMP280だと思っていたセンサーは実はBME280だということがわかりました。なので、今回からタイトルをBME280と改めています。
alasixosaka.hatenablog.com

今回は、さらに子機側のプログラムをいじって、BME280用のライブラリを使わず、I2C通信のコマンドだけでBME280を動かしてみようと思います。
前回までは、BME280のライブラリを使ったActEx_BME280_SHT3xというプログラムをベースにいじくっていましたが、I2C通信のコマンドで温湿度センサーを動かしているプログラムBRD_I2C_TEMPHUMIDというプログラムがあるのでこちらをベースにします。このプログラムはセンサーとしてSHTC3ないしSHT40を使うようになっているのでセンサーへのコマンド送信やデータの読み出しについてはBME280用に修正する必要があります。
なお、今回は、モノワイヤレスのBRD_I2C_TEMPHUMIDのプログラムの解説を主に参考にしました。また、TWELITEのプログラムについては、他に2つのサイトを参考にしました。
BRD_I2C_TEMPHUMID - MWX Library
農業IoTの準備として、TWELITEを使った環境情報の取得をやってみた-その1(環境情報の取得)
トワイライト(TWELITE)のI2C通信を実装し無線通信する | スマートライフを目指すエンジニア
また、BMP280(BME280)のプログラミングについては下記のサイトを参考にしました。
第35回 温湿度・気圧センサ(BME280) 〜データ取得プログラム〜 | ツール・ラボ
BME280 搭載、温度・湿度・気圧センサーを SPI で動かしてみた( ESP-WROOM-02 ( ESP8266 )使用) | ページ 2 | mgo-tec電子工作
温度・気圧センサーBMP280を使ってみる | 東京お気楽カメラ

まずはIDを取得する

参考にしたツールラボのサイトで、リンクを張っているのは第35回ですが、その前の第31回でチップのIDを読み取っている記事があります。その記事に倣って、まずはチップのIDを読み取ってみたいと思います。
プログラムの始めから順番に説明します。
まず、ライブラリを読み込む部分ですが、今回はSM_SIMPLEというのが加わっています。

// use twelite mwx c++ template library
#include <TWELITE>
#include <NWK_SIMPLE>
#include <STG_STD>
#include <SM_SIMPLE>

これは、モノワイヤレスのプログラムの説明サイトにも書いてありますが、アプリケーションループの記述を簡素化するためのものとのことです。
前回の子機のプログラムでは少し違った方法でループを記述していました。まあ、概ねは似たようなものなんですが。ActEx_BME280_SHT3xでは、enum class E_STATEという構造文を定義して、それを使って、Switch、Caseで分岐を行っていました。
今回のベースにしたBRD_TEMPHUMIDでは、同じように構造文として enum class STATE  を定義していますが、そのあとに、SM_SIMPLE step;という宣言を行って、ループ中では、step.xxxというように呼び出しています。あまり大きく変わらないのでどちらでもよかったのですが、ループ内でのタイムアウトの設定がこちらの方がやりやすそうだったので、こちらのやり方をそのまま使っています。
次が、センサーの種別を選択するところです。

/*** sensor select, define either of USE_SHTC3 or USE_SHT40  */
// use SHTC3 (TWELITE PAL)
#define USE_BMP280
// use SHT40 (TWELITE ARIA)
#undef USE_SHT40

元のプログラムでは、SHTC3とSHT40のどちらかを選択するようになっていました。デフォルトはSHTC3。その部分を#define USE_BMP280として変更しています。まあ、名前だけなのでどうでもいいといえばどうでもいいんですが。
その次が、センサー用の構造文です。この部分でセンサーへのコマンド送信や、測定データの読み取りを記述していますので、この部分をBME280用に変更します。といっても、今回はとりあえずIDを読めればいいので大幅には変更しません。処理は、setup、begin、get_conv、readの4つです。setupはセンサーの初期化処理ですが、もとのSHTC3では特に必要ないようで、I2Cのアドレスと、タイムアウト待ちの待ち時間のみを設定していました。BME280では色々と初期設定が必要です。IDを読むだけなら必要なさそうですが、ここでは一応初期設定を行っています。
初期設定の詳細は参考にしたサイトを見てもらえばわかると思うので詳細は省略します。
まず、初期設定用の変数を定義します。I2C_ADDRはI2Cのアドレス。CONV_TIMEはタイムアウトの待ち時間、t_sbからはBME280用の変数です。詳細はsetupのところで書きます。

/*** Sensor Driver */
#if defined(USE_BMP280)
// for SHTC3
struct SHTC3 {
	uint8_t I2C_ADDR;
	uint8_t CONV_TIME;
	uint8_t t_sb;
	uint8_t filter;
	uint8_t spi3or4;
	uint8_t osrs_t;
	uint8_t osrs_p;
	uint8_t Mode;
	uint8_t ctrl_meas_reg;
	uint8_t config_reg;

次からがそれぞれの処理ルーチンになります。まず、setupですが、先ほど定義した変数に値を代入しています。I2Cアドレスは0x76です。アドレスは基板のCSBにつながっている半田ジャンパーをカットすれば0x77に変更できるそうですが、特に必要ないのでそのまま0x76を使います。次がタイムアウトの時間で、BME280は後で出てくるオーバーサンプリングの値を設定することができます。その値によって測定に要する時間が変わるのですが、今回はオーバーサンプリングを行わない設定(値を1にする)で動かすことを予定しているので最大でも10ms待てば測定が終わるので10としています。次のt_sbは、測定の間隔です。ここでは最低の0.5msとしました。その次がfilterでノイズ除去のフィルター設定ですが、通常の温度測定ではoffでよいとのことですので、0としてoffにします。そしてその次が、spiを3線式で接続するか4線式にするかの設定ですが、I2cを使う場合には関係がないので0にしておきます。その次の、osrs_tが温度測定のオーバーサンプリング、osrs_pが気圧測定のオーバーサンプリング設定です。上にも書いたように最低の1にします。なお、ここを0にすると、測定をしないという設定になります。また、センサーを湿度測定のできないBMP280だと思っていたので、湿度測定に関する設定を行っていません。最後がModeでいったんスリープモードにしておきます。Modeは2ビットで、00がスリープ。01ないし10がワンショットの測定、11が連続測定です。測定するときはワンショットの測定を指示しデータを取得します。
これらの値をレジスタに書き込む値に変換しているのがその次の部分です。書き込むレジスタはctrl_measとconfigの2つです(湿度も測定するときはcntl_humにも書き込む必要があります)。そして、ヘルパー関数というのを使って、レジスタ番号をまず出力し、続いてレジスタの値を出力しています。ヘルパー関数を使わず、通常のI2C制御用の関数を使うこともできます。その場合は、Wire.begintransmission()、Wire.write()、Wire.endtransmission()と処理を記述する必要があります(この辺はArduinoのI2Cライブラリとほぼ同じでなじみがありますが)。ヘルパー関数を使う利点は、こういった一連の手続きを一文で記述できることにあります。if(auto&& wrt=Wire.get_writer()...と書くことによって、デバイスのオープンに失敗したときはfalseを返し、オープンに成功した時だけif分の中を実行し、実行が終了すれば自動的に閉じる動作を行います。(おそらく、マルチマスタで通信がバッティングしたときはfalseになるのではと思っています。もちろん、ノイズやデバイスが正しく接続されていないとかの状況でエラーになってもfalseになると思います)

	bool setup() {
		// here, initialize some member vars instead of constructor.
		I2C_ADDR = 0x76;
		CONV_TIME = 10;
		t_sb = 0;  //stanby 0.5ms
          filter = 0; //filter O = off
    	  spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
          osrs_t = 1; //OverSampling Temperature x1
  		osrs_p = 1; //OverSampling Pressure x1
  		Mode = 0; //Sleep mode
 
  		ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
  		config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4;   //register address
			wrt << ctrl_meas_reg; 
		} else {
			return false;
		}
		//return true;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF5;   //register address
			wrt << config_reg; 
		} else {
			return false;
		}
		return true;
	}

次は、begin()で測定開始の指示です。
しかし、今回はIDを読むだけなので単にリターンするだけにしてあります。

	bool begin() {
		// start read
		/*
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0x60; // SHTC3_TRIG_H
			wrt << 0x9C; // SHTC3_TRIG_L
		} else {
			return false;
		}
		*/

		return true;
	}

その次は、タイムアウトの時間を返す関数です。

	int get_convtime() {
		return CONV_TIME;
	}

その次は、read()で測定値の読み込みです。
SHT3Cの場合は、測定値は、6バイトのデータで、温度が16ビット、チェックサムが8ビット、湿度が16ビット、チェックサムが8ビットで計6バイトのデータを読んでいます。今回は、IDを読むだけですので、読みだすデータは1バイトのみです。ただし、BME280の場合は、読みだすレジスタを指定してやる必要があります。レジスタの値を書き込んで、読みだされたデータがそのレジスタのデータです。ですので、先にレジスタの値0xD0を書き込みます。それから、Wire.get_readerを使って、書き込みの時と同様にif(auto && rdr = Wire.get_reader(I2C_ADDR, 1)){ ...} としています。引数の2つ目は読み込むデータのバイト数です。複数のデータを読む場合はここに例えば6といった風に書きます。今回は、温度チェックサム用の変数u8temp_csumが8ビットで定義されていたので、それにIDを読み込みました。そして、i16Temp = (int16_t)(u8temp_csum); として、16ビット変数のi16Tempにキャストしています。TWELITEで送信するデータが2バイトなので、それに合わせるためです。

	bool read(int16_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint16_t u16temp, u16humd;
		uint8_t u8temp_csum, u8humd_csum;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xD0; // reg address for ID
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 1)) {
			rdr >> u8temp_csum; // skip the crc8 check
		} else {
			return false;
		}

		// check CRC and save the values
		/*
		if (   (CRC8_u8CalcU16(u16temp, 0xff) == u8temp_csum)
			&& (CRC8_u8CalcU16(u16humd, 0xff) == u8humd_csum))
		{
			i16Temp = (int16_t)(-4500 + ((17500 * int32_t(u16temp)) >> 16));
			i16Humd = (int16_t)((int32_t(u16humd) * 10000) >> 16);
		} else {
			return false;
		}
		*/
		i16Temp = (int16_t)(u8temp_csum);

		return true;
	}
} sensor_device;
#endif

その次の部分はSHT40を使う場合の処理の設定ですので省略します。
次に、無線通信で必要な設定を行います。まず、アプリケーションIDを0x1234sbcdとしています。これは、サンプルACT共通の設定になっています。次がチャンネルで13です。これもサンプルACTでは共通の設定のようです。そして、オプションビットの設定ですが、0ですので特に設定しないということになります。その次が論理IDで0を指定しています。0は親機の指定ですが、後の方の処理で0xFE(IDを割り振らない子機)に設定しなおしています。何故こうなっているのかはわかりません。それからアプリ固有の4文字です。これは元はBTH1でしたが、親機の方で適切に処理するように以前使ったSBS1としています。

/*** Config part */
// application ID
const uint32_t DEFAULT_APP_ID = 0x1234abcd;
// channel
const uint8_t DEFAULT_CHANNEL = 13;
// option bits
uint32_t OPT_BITS = 0;
// logical id
uint8_t LID = 0;

// application use
const char FOURCHARS[] = "SBS1";

次はセンサーの値を格納するための変数構造体です。16ビットの変数を温度用と湿度用に用意しています。sensor という名前を付けています。

// stores sensor value
struct {
	int16_t i16temp;
	int16_t i16humid;
} sensor;

それから次が、上の方で少し書いた、SM_SIMPLEで使う処理の設定です。処理の内容はINTERACTIVE、INIT、SENSOR、TX、TX_WAIT_COMP、GO_SLEEPと前のActEx_BME280_SHT3xとほぼ同じです。そのあとがシンプルステートの宣言でstepという名前を付けていますので、以降はstep.next(STATE::TX)などと使います。

// application state defs
enum class STATE : uint8_t {
	INTERACTIVE = 255,
	INIT = 0,
	SENSOR,
	TX,
	TX_WAIT_COMP,
	GO_SLEEP
};

// simple state machine.
SM_SIMPLE<STATE> step;

その次は、スリープ関数のプロトタイプ宣言です。

/*** Local function prototypes */
void sleepNow();

そして、setupです。以前の子機の所と基本的には同じなので詳細は省略します。中ほどの所で、論理ID(LID)が0の場合、強制的に0xFEを設定しています。はじめにLIRを0としているので、0xFEに設定されてしまいます。インタラクティブモードで書き換えれば違うIDになると思います。

/*** setup procedure (run once at cold boot) */
void setup() {
	/*** SETUP section */
	/// init vars or objects
	step.setup(); // initialize state machine
	
	/// load board and settings objects
	auto&& set = the_twelite.settings.use<STG_STD>(); // load save/load settings(interactive mode) support
	auto&& nwk = the_twelite.network.use<NWK_SIMPLE>(); // load network support

	/// configure settings
	// configure settings
	set << SETTINGS::appname(FOURCHARS);
	set << SETTINGS::appid_default(DEFAULT_APP_ID); // set default appID
	set << SETTINGS::ch_default(DEFAULT_CHANNEL); // set default channel
	set.hide_items(E_STGSTD_SETID::OPT_DWORD2, E_STGSTD_SETID::OPT_DWORD3, E_STGSTD_SETID::OPT_DWORD4, E_STGSTD_SETID::ENC_KEY_STRING, E_STGSTD_SETID::ENC_MODE);

	// if SET(DIO12)=LOW is detected, start with intaractive mode.
	if (digitalRead(PIN_DIGITAL::DIO12) == PIN_STATE::LOW) {
		set << SETTINGS::open_at_start();
		step.next(STATE::INTERACTIVE);
		return;
	}

	// load values
	set.reload(); // load from EEPROM.
	OPT_BITS = set.u32opt1(); // this value is not used in this example.
	
	// LID is configured DIP or settings.
	LID = set.u8devid(); // 2nd is setting.
	if (LID == 0) LID = 0xFE; // if still 0, set 0xFE (anonymous child)

	/// configure system basics
	the_twelite << set; // apply settings (from interactive mode)

	/// configure network
	nwk << set; // apply settings (from interactive mode)
	nwk << NWK_SIMPLE::logical_id(LID); // set LID again (LID can also be configured by DIP-SW.)	

	/*** BEGIN section */
	Wire.begin(); // start two wire serial bus.
	
	// let the TWELITE begin!
	the_twelite.begin();

	// setup sensor device
	sensor_device.setup();

	/*** INIT message */
	Serial << "--- TEMP&HUMID:" << FOURCHARS << " ---" << mwx::crlf;
	Serial	<< format("-- app:x%08x/ch:%d/lid:%d"
					, the_twelite.get_appid()
					, the_twelite.get_channel()
					, nwk.get_config().u8Lid
				)
			<< mwx::crlf;
	Serial 	<< format("-- pw:%d/retry:%d/opt:x%08x"
					, the_twelite.get_tx_power()
					, nwk.get_config().u8RetryDefault
					, OPT_BITS
			)
			<< mwx::crlf;
}

次はスリープから起動したときの処理です。シリアル出力以外何もしていません。

// wakeup procedure
void wakeup() {
	Serial	<< mwx::crlf
			<< "--- TEMP&HUMID:" << FOURCHARS << " wake up ---"
			<< mwx::crlf
			<< "..start sensor capture again."
			<< mwx::crlf;
}

その次がループ関数です。長いので少しずつ説明します。
まず、switch(step.state())として、step.state()の中身で分岐をします。最初はINTERACTIVEでインタラクティブモードの処理ですが、インタラクティブモードでは何もせず終了しています。次がINITで初期化処理です。ここで、sensor_device.bigen()を呼び出して、センサーを初期化しています。そのあと、step.set_timeout( )でタイムアウト待ちをします。タイムアウトの時間は上記で設定した10msです。TWELITEにもArduinoと同様にdelay()関数が用意されているので、delay(10)としてもよさそうなものですが、ここでタイムアウトを使う意味はよくわかりません。センサーなどある時間内に応答がなければ次に進むという処理の場合は意味があるようにも思いますが。

/*** loop procedure (called every event) */
void loop() {
	do {
		switch (step.state()) {
		case STATE::INTERACTIVE:
		break;
		
		case STATE::INIT: // starting state
			// start sensor capture
			sensor_device.begin();
			Serial << sensor_device.get_convtime() << mwx::crlf;
			step.set_timeout(sensor_device.get_convtime()); // set timeout
			step.next(STATE::SENSOR);
		break;

次は、SENSORでセンサーに測定指示をだして、値を読み取っています。本来は温度がi16temp、湿度がi16humidに返されるのですが、上に書いたように、ここではIDだけを読みだしているので、i16tempにIDが返されます。i16humidは何も書き込まれていないので0が返ります。

		case STATE::SENSOR: // starting state
			if (step.is_timeout()) {
				// the sensor data should be ready (wait some)
				sensor_device.read(sensor.i16temp, sensor.i16humid);

				Serial << "..finish sensor capture." << mwx::crlf
					<< "     : temp=" << div100(sensor.i16temp) << 'C' << mwx::crlf
					<< "       humd=" << div100(sensor.i16humid) << '%' << mwx::crlf
					;
				Serial.flush();

				step.next(STATE::TX);
			}
		break;

次が無線パートでTXです。基本は前の子機のプログラムと同じです。送るデータは、通信元のID、通信先のID、アプリ固有の4文字とi16tempとi16humidになりますが、ここではセンサーのIDのみを送信するためにi16humidはコメントアウトしています。

		case STATE::TX:
			step.next(STATE::GO_SLEEP); // set default next state (for error handling.)

			// get new packet instance.
			if (auto&& pkt = the_twelite.network.use<NWK_SIMPLE>().prepare_tx_packet()) {
				// set tx packet behavior
				pkt << tx_addr(0x00)  // 0..0xFF (LID 0:parent, FE:child w/ no id, FF:LID broad cast), 0x8XXXXXXX (long address)
					<< tx_retry(0x1) // set retry (0x1 send two times in total)
					<< tx_packet_delay(0, 0, 2); // send packet w/ delay

				// prepare packet payload
				pack_bytes(pkt.get_payload() // set payload data objects.
					, make_pair(FOURCHARS, 4)  // just to see packet identification, you can design in any.
					, uint16_t(sensor.i16temp)
					//, uint16_t(sensor.i16humid)
				);

				// do transmit
				MWX_APIRET ret = pkt.transmit();
				Serial << "..transmit request by id = " << int(ret.get_value()) << '.' << mwx::crlf << mwx::flush;

				if (ret) {
					step.clear_flag(); // waiting for flag is set.
					step.set_timeout(100); // set timeout
					step.next(STATE::TX_WAIT_COMP);
				}
			}
		break;

次が、無線通信の終了待ちです。これも前の子機のプログラムと基本的には同じですが、step.is_flag_ready()を見て終了判断をしています。

		case STATE::TX_WAIT_COMP: // wait for complete of transmit
			if (step.is_timeout()) { // maybe fatal error.
				the_twelite.reset_system();
			}
			if (step.is_flag_ready()) { // when tx is performed
				Serial << "..transmit complete." << mwx::crlf;
				Serial.flush();
				step.next(STATE::GO_SLEEP);
			}
		break;

次がスリープ処理です。

		case STATE::GO_SLEEP: // now sleeping
			sleepNow();
		break;

		default: // never be here!
			the_twelite.reset_system();
		}
	} while(step.b_more_loop()); // if state is changed, loop more.
}

その次は、通信の終了判断のフラグを立てる処理です。割り込み処理で通信終了時にフラグを立てています。上のループ内のTX_WAIT_COMPがstep.is_flag_ready()で、ここではstep.set_flag(ev.bStatus)となっていて少し違うのが何故かよくわかりません。

// when finishing data transmit, set the flag.
void on_tx_comp(mwx::packet_ev_tx& ev, bool_t &b_handled) {
	step.set_flag(ev.bStatus);
}

最後がスリープ処理の実態部分です。前の子機のプログラムと同じです。

// perform sleeping
void sleepNow() {
	step.on_sleep(false); // reset state machine.

	// randomize sleep duration.
	uint32_t u32ct = 1750 + random(0,500);

	// output message
	Serial << "..sleeping " << int(u32ct) << "ms." << mwx::crlf;
	Serial.flush(); // wait until all message printed.
	
	// do sleep.
	the_twelite.sleep(u32ct);
}

IDを読んだらBME280だった

冒頭に結論を書いたのでだいたい想像はついていると思いますが、このプログラムを子機に書き込んで、動かしてやると、親機にIDが送られてくるのですが、それを見るとIDが0x60となっていました(最後から2つ目のバイト)。

IDを読むと0x60でBME280であることが分かった

BMP280であればIDは0x58となるところですが、IDは0x60ですので、BME280ということがわかりました。BME280は温度、気圧に加えて、湿度も測れるセンサーでBMP280よりかなり高いので、てっきりBMP280と思っていました。これは、以前M5Stickで環境センサーを作った時に使ったやつですが、最近は使ってなかったのでここから持ってきました。以前に書いた記事を読むとVOCセンサーを補正するのに温度も湿度も必要ということでBME280を買ったとしっかり書いてありました。
alasixosaka.hatenablog.com
人間の記憶というものはあいまいなものですね。特に年を取るとよく感じます。こうやってブログに書いておいて見返すということが役立つということを改めて感じました。
ちなみに、子機側にUSBシリアル変換器を繋ぐと子機のシリアル出力が見れるのですが、こんな感じになります。

子機のシリアル出力をモニタしているところ

温度が0.96℃と出ていますが、これがIDである0x60を10進数に直して100で割った値です。
IDは無事に読み取れたので、次は温度を測定するプログラムにチャレンジします。