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

TWELITEで温湿度センサーを動かすの五回目です。とうとう五回目まで来てしまいましたが、温湿度センサーの記事は今回が最後です。
前回は、BME280用のライブラリを使わず、I2CコマンドのみでBME280のIDを読むところまでやりました。
alasixosaka.hatenablog.com

今回は、実際に温度を測定してみます。湿度も気圧も測れるのですが、最終目的はRTCモジュールをI2Cで動かすことで、BME280を動かすのはあくまでI2Cモジュールをプログラムして動かす練習なので、とりあえず温度だけを測ってみました。
前回IDがちゃんと読めたのですぐに動くと思っていたのですが、単純なことにはまってしまいずいぶん時間がかかりました。
プログラムは、前回のBRD_I2C_TEMPHUMIDに手を入れたのをベースにします。初期設定の書き込みまではできているので、補正係数の読み込み、温度測定、補正係数を使った計算をプログラムすれば良いということになります。前回からの変更点を中心に説明します。

補正係数の読み込み

実はここで引っかかってなかなかうまくいきませんでした。なぜそうなったのかは後で詳しく書きます。
まず、グローバル変数を定義します。sensor driverの始めの部分に書きます。温度用の補正係数は3つで、dig_T1、dig_T2、dig_T3の3つ。dig_T1は符号なし16ビット変数、digT2とdig_T3は符号付き16ビット変数です。何故なのかはわかりません。データシートの例がこうなっています。

uint16_t dig_T1;
int16_t  dig_T2;
int16_t  dig_T3;

続いて、sensor driverのsetupの所で、補正係数を読み込みます。
まず、引数として16ビット変数を2つ用意しています。何に使うのかは後で書きます。

bool setup(uint16_t &i15Temp, uint16_t &i15Humd) {....
}

次に、補正係数読み込み用の変数として、符号なし8ビット変数を6つ用意します。

uint8_t dig_T1tempL;
uint8_t dig_T1tempH;
uint8_t dig_T2tempL;
uint8_t dig_T2tempH;
uint8_t dig_T3tempL;
uint8_t dig_T3tempH;

始めは16ビットなのだからと、そのままdig_T1、dig_T2、dig_T3に読み込もうとしました。まず、符号付き変数をwire.get_readerの後に指定すると何故かコンパイラが途中で落ちてしまいます。この理由を見つけるのに相当悩みました。正常にコンパイルできてたコードに戻して一つ一つチェックしていったところ、符号付き変数を使うとコンパイラが落ちることがわかりました。その後、関数の呼び出しの引数などで、変数の型が一致しない時もコンパイラが落ちることがわかりました。エラーを出してくれるとありがたいのですがまだ完成度が低いようです。そこで、uint16_t dig_T2tempとして、16ビットの符号なし変数でいったん受けて、後で符号付き変数に変換することを行いましたが、そのままでは上位バイトと下位バイトが入れ替わってしまい数値がおかしくなることがわかり、8ビット変数で受けて16ビットに変換するようにしました。これに気づくのにはもっと時間がかかってしまいました。ちゃんと測定できていないのか、補正の計算がおかしいのか、補正係数がおかしいのかいろいろ悩んだ挙句、ESP8266で測っている下記のサイトを参考に(というかそのままま)、ESP8266で順番にプログラムを動かしてみたところ、補正係数を取得して表示するプログラムがあって、TWELITE側でも取得した補正係数をシリアル出力するようにしてみたところ、ようやく上位バイトと下位バイトが入れ替わっていることに気づきました。
www.denshi.club
ESP8266はアマゾンで買ったNodeMCUというのを使いました。

ESP8266モジュールはいろいろ持っているのですが、こいつはUSB端子があって、シリアル変換ができるようになっているのでテスト用に動かすときには便利です。デバイスとして組む時は余分な回路が消費電流を増すのでベアなESP8266に変換基板をくっつけただけのシンプルな奴が良いですが。
念のため配線を書いておきます。

ESP8266はArduino IDEで動かすときはソフトでI2Cをコントロールするので、SCLとSDAを明示的に示す必要があります。今回は、Wire.begin(2,14); というように、GPIOの2番と14番を使いました。この部分さえ変更しておけば参考サイトのスケッチはそのまま動きました。
データシートをもう一度よく見ると、温度補正用の係数dig_T1はレジスタの0x88と0x89に書かれていて、0x88が下位バイト、0x89が上位バイトになっています。つまり、下位バイト上位バイトの順に読みだされます。一方、このサンプルプログラムのベースになっているセンシリオンのSHT40のデータシートを見ると、例えば温度のデータは16ビットで提供されますが、上位バイト、下位バイトの順に読みだされます。この順であれば、TWELITEの読み出しに16ビット変数を使っても問題なく読むことができます。しかし、BME280では逆に下位バイト、上位バイトの順に読みだされるのでそのまま16ビット変数で受けてしまうと上位バイトと下位バイトが入れ替わった数字になってしまいます。これが、うまく測定できない原因でした。
なので、補正係数の取得の部分は、まず、

//get compensation data
		
if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
	wrt << 0x88; //register address
} else {
	return false;
}

として、温度補正係数のアドレスを書き込み、

if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
	rdr >> dig_T1tempL; 
	rdr >> dig_T1tempH; 
	rdr >> dig_T2tempL;
	rdr >> dig_T2tempH;
	rdr >> dig_T3tempL;
	rdr >> dig_T3tempH;
	} else {
	return false;
}

として、補正係数6バイトをすべて8ビット変数で受けています。
そして、

dig_T1 = uint16_t(dig_T1tempH<<8)|dig_T1tempL;
dig_T2 = uint16_t(dig_T2tempH<<8)|dig_T2tempL;
dig_T3 = uint16_t(dig_T3tempH<<8)|dig_T3tempL;
i15Temp = dig_T1;
i15Humd = dig_T2;

として、16ビット変数に変換しています。最後の、i15Tenpとi15Humdは、setupの冒頭に書いた、2つに引数に相当していて、それぞれ、dig_T1とdig_T2をシリアル出力するために返す値としています。デバッグ用なので本来不要です。
また、このルーチンの最後に

ctrl_meas_reg = ctrl_meas_reg | 1;  //set one shot mode

として、ワンショットモードをレジスタ(cntl_meas_reg)に書き込む用のデータの第0ビットを立てて、これを測定指示の所で書き込むようにしています。
次が測定を指示する部分です。0xF4レジスタに先ほどのcntl_meas_regを書き込んで測定指示を出します。

bool begin() {
// start read
		
  if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
	  wrt << 0xF4; // register address
	  wrt << ctrl_meas_reg; // register data
  } else {
	  return false;
  }
  return true;
}

測定データの読み出しの部分です。こちらも引数は2つで16ビット変数のi16Tempとi16Humdです。測定されたデータから補正係数を使って計算して温度、湿度を返すのですが、湿度は測定していませんので0が返ります。温度データは3バイトあり(実際は最下位の4ビットは無効で計20ビット)、8ビット変数を3つ用意します。また、途中の計算で32ビット必要になるので32ビット変数も3つ用意します。

bool read(int32_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint32_t u32temp1, u32temp2, u32temp3;
		uint8_t u8temp_1, u8temp_2, u8temp_3;

そして、温度データの先頭アドレス0xFAを書き込んで、3バイトのデータを順番に読み出します。ここも最初は32ビット変数で受けて4ビットシフトをしていたんですが、どうも値が変になるので8ビットで受けて計算するように変えています。しかしデータシートを見ると、ここの部分は上位バイト、下位バイト、最下位バイトの順で読みだされるのでそのまま32ビットで受けてもよかったと思います。面倒なので直していませんが。
最後は計算用に受けた8ビット変数を32ビット変数にキャストしています。

		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xFA; // register address for temp
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 3)) {
			rdr >> u8temp_1; // read temperature
			rdr >> u8temp_2;
			rdr >> u8temp_3;
		} else {
			return false;
		}
		u32temp1 = uint32_t(u8temp_1);
		u32temp2 = uint32_t(u8temp_2);
		u32temp3 = uint32_t(u8temp_3);

その次が、受けたデータを32ビット変数に直すところで、

temp_raw = (u32temp1 << 12) | (u32temp2 << 4) | (u32temp3 >> 4);

として、最終的にtemp_rawに測定した生データが入ります。
次が補正計算の部分で、参考サイト(前回のブログに記載)やデータシートの式と同じです。

//calculate temperature
int32_t var1, var2, T;
var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
t_fine = var1 + var2;
T = (t_fine * 5 + 128) >> 8;

最後に計算結果を16ビット変数i16Tempにキャストして代入しています。

i16Temp =int_16t(T);

後の部分は上に書いたように、読みだした補正係数をシリアル出力する部分を書き加えているだけでほぼそのままです。
これで、親機で受けたときに正しく温度が表示されました。

温度表示が正しく表示された。

プログラムの全文です。

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

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

/*** 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;
	uint16_t dig_T1;
	int16_t  dig_T2;
	int16_t  dig_T3;
	int32_t temp_raw;
	int32_t  t_fine;
	uint8_t ID;

	bool setup(uint16_t &i15Temp, uint16_t &i15Humd) {
		// 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 x4
  		osrs_p = 1; //OverSampling Pressure x4
  		Mode = 0; //Normal mode
		uint8_t dig_T1tempL;
		uint8_t dig_T1tempH;
		uint8_t dig_T2tempL;
		uint8_t dig_T2tempH;
		uint8_t dig_T3tempL;
		uint8_t dig_T3tempH;
  		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; // register data
		} else {
			return false;
		}
		//return true;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF5; // register address
			wrt << config_reg; // register data
		} else {
			return false;
		}
		delay(1000);
		
		//get compensation data
		
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0x88; //register address
		} else {
			return false;
		}
		
		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 6)) {
			rdr >> dig_T1tempL; 
			rdr >> dig_T1tempH; 
			rdr >> dig_T2tempL;
			rdr >> dig_T2tempH;
			rdr >> dig_T3tempL;
			rdr >> dig_T3tempH;
		} else {
			return false;
		}
		
		dig_T1 = uint16_t(dig_T1tempH<<8)|dig_T1tempL;
		dig_T2 = uint16_t(dig_T2tempH<<8)|dig_T2tempL;
		dig_T3 = uint16_t(dig_T3tempH<<8)|dig_T3tempL;
		i15Temp = dig_T1;
		i15Humd = dig_T2;
		

		ctrl_meas_reg = ctrl_meas_reg | 1;  //set one shot mode
		return true;
	}

	bool begin() {
		// start read
		
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xF4; // register address
			wrt << ctrl_meas_reg; // register data
		} else {
			return false;
		}
		

		return true;
	}

	int get_convtime() {
		return CONV_TIME;
	}

	bool read(int16_t &i16Temp, int16_t &i16Humd) {
		// read result
		uint32_t u32temp1, u32temp2, u32temp3;
		uint8_t u8temp_1, u8temp_2, u8temp_3;
		if (auto&& wrt = Wire.get_writer(I2C_ADDR)) {
			wrt << 0xFA; // register address for temp
		} else {
			return false;
		}

		if (auto&& rdr = Wire.get_reader(I2C_ADDR, 3)) {
			rdr >> u8temp_1; // read temperature
			rdr >> u8temp_2;
			rdr >> u8temp_3;
		} else {
			return false;
		}
		u32temp1 = uint32_t(u8temp_1);
		u32temp2 = uint32_t(u8temp_2);
		u32temp3 = uint32_t(u8temp_3);

		temp_raw = (u32temp1 << 12) | (u32temp2 << 4) | (u32temp3 >> 4);
		
		//calculate temperature
		int32_t var1, var2, T;
		var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    	var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
    	t_fine = var1 + var2;
    	T = (t_fine * 5 + 128) >> 8;
		
		i16Temp = int16_t(T);
		
		return true;
	}
} sensor_device;
//#endif

/*** 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";

// stores sensor value
struct {
	int16_t i16temp;
	int16_t i16humid;
	uint16_t i15temp;
	uint16_t i15humid;
} sensor;

// 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 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(sensor.i15temp,sensor.i15humid);
	Serial 	<< format("     : T1temp= %04x"   //補正係数のシリアル出力
					,sensor.i15temp)
					<< mwx::crlf;
	Serial	<< format("     : T2temp=%04x"
					,sensor.i15humid)
					<< mwx::crlf;
					
				//Serial.flush();

	/*** 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;
}

/*** 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;

		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;

		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;

		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.
}

// 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);
}

ようやくBME280が動かせるようになったので、次はRTCモジュールを動かしてみます。一回で終わるかどうかわかりませんが。