スマートホーム構築記

IoTの恩恵を受けたいけど家庭用はなかなか出てこない。なら作ろうというブログ

K09421(BME280)のSwitch Sensorサンプルプログラムの詳細分析

目的

前回は定期的にログを取得するプログラムを作ると書いたのですが取得プログラムの仕様がわからないままでは勉強になりませんので
サンプルプログラムを詳細に見ていくことにしました。

サンプルプログラム

GitHub - SWITCHSCIENCE/BME280github.com
rev a43306eのものを参考とします。
以下に示した表はサンプルプログラムの関数一覧になります。

関数 機能
writeReg registryへ書き込む
get_calib_param registryから校正パラメータ読み込み
readData センサ出力の生値を取得する
compensate_P 圧力補正値を出力する
compensate_T 温度補正値を出力する
cmopensate_H 湿度補正値を出力する
set_up センサの設定を書き込む

これからそれぞれの関数について詳しく見ていきます。

writeReg

def writeReg(reg_address, data):
 bus.write_byte_data(i2c_address,reg_address,data)

i2c_addressは7行目にグローバルで定義されていて、通常であれば0x76となっています。
なのでreg_addressに変更するレジストリのアドレス、dataにそのアドレスに書き込む値(8bit)をいれることでレジストリの値が変更できるようになっています。
呼び出されているのはsetup()です。

get_calib_param

calib = []
for i in range (0x88,0x88+24):
	calib.append(bus.read_byte_data(i2c_address,i))
calib.append(bus.read_byte_data(i2c_address,0xA1))
for i in range (0xE1,0xE1+7):
        calib.append(bus.read_byte_data(i2c_address,i))

校正値が格納されているレジストリから値を読みだしてcalib配列に格納しています。
しかし格納されているのは0x88~0x9F,0xA1,0xE1~0xE7となっています。
取り扱い説明書に記載されているMemory Mapには0x88~0xA1,0xE1~0xF0にあると書かれています。
http://trac.switch-science.com/wiki/BME280
BME280 – Attachments – スイッチサイエンス
これではなぜこのアドレスが読まれているのかわかりません。そこでBME280のデータシートを確認してみます。
https://ae-bst.resource.bosch.com/media/_tech/media/datasheets/BST-BME280_DS001-11.pdf
すると22~23ページに校正値の保存されているアドレスが記述されています。
この表を簡易的にまとめると以下の表のようになります。

補正値種類 アドレス calib番号
温度 0x88~0x8D 0~5
圧力 0x8E~0x9F 6~23
湿度 0xA1,0xE1~0xE7 25~32

これでどうしてあのようなアドレス指定で校正値を取得しているのかがわかりました。

digT.append((calib[1] << 8) | calib[0])
digT.append((calib[3] << 8) | calib[2])
digT.append((calib[5] << 8) | calib[4])

digTという温度校正値を取得する配列にそれぞれ校正値を代入しています。

calib[1] << 8 | calib[0]

この処理でmsbとlsbを結合する処理をしています。
'<<8'で8bit左にシフト、'|'で論理和という処理になっています。
圧力補正値digPについても特に特殊な処理はなくdigP1~9までdigTと同様の処理で取得しています。
湿度については若干特殊なアドレス構成になっているのでシフトが4ビットとなっているものがありますがほぼ同様の処理をが行われています。

for i in range(1,2):
	if digT[i] & 0x8000:
		digT[i] = (-digT[i] ^ 0xFFFF) + 1

for i in range(1,8):
	if digP[i] & 0x8000:
		digP[i] = (-digP[i] ^ 0xFFFF) + 1

for i in range(0,6):
	if digH[i] & 0x8000:
		digH[i] = (-digH[i] ^ 0xFFFF) + 1 

次の処理はunsigned→signedの変換処理ですがこの処理は誤りがあります。
データシートP22からp23に書かれた表から型と校正変数についてのみ抜粋すると以下のようになります。

校正値
T1 unsigned
T2,3 signed
P1 unsigned
P2-9 signed
H1,H3 unsigned
H2,H4,H5 signed

これに従うと上記コードだけでは変換が不十分であることがわかります。以下のように書きなおす必要があります。

for i in range(1,3): #T2,T3
	if digT[i] & 0x8000:
		digT[i] = (-digT[i] ^ 0xFFFF) + 1

for i in range(1,9): #P2~P9
	if digP[i] & 0x8000:
		digP[i] = (-digP[i] ^ 0xFFFF) + 1

for i in [1,3,4]: #H2,H4,H5
	if digH[i] & 0x8000:
		digH[i] = (-digH[i] ^ 0xFFFF) + 1 

readData

	data = []
	for i in range (0xF7, 0xF7+8):
		data.append(bus.read_byte_data(i2c_address,i))

data配列に測定値が格納されている0xF7~0xFEのメモリの値を格納します。

	pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
	temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
	hum_raw  = (data[6] << 8)  |  data[7]

それぞれをMemoryMapに従ってビットシフトと論理和で結合してそれぞれの補正前の値を生成します。

	compensate_T(temp_raw)
	compensate_P(pres_raw)
	compensate_H(hum_raw)

最後に補正値を出力するプログラムを呼び出しています。

compensate_T

global t_fine

圧力や湿度の補正用の温度をグローバルで定義しています。

	v1 = (adc_T / 16384.0 - digT[0] / 1024.0) * digT[1]
	v2 = (adc_T / 131072.0 - digT[0] / 8192.0) * (adc_T / 131072.0 - digT[0] / 8192.0) * digT[2]
	t_fine = v1 + v2
	temperature = t_fine / 5120.0

ここが補正になっています。しかしこれではどういった補正なのかわからないので実際にこの式を何かしらの意味がある形に変形して数式に起こしていきます。
{
T  =\frac{T_{adc}-digT_{1} \times 2^{4}}{2^{20}}
 }
温度は32bitのsigned型に対して20bitのフォーマットで出力されているのでdig_T1(16bit)を4bit左にビットシフトして20bitフォーマットにして減算し、20bit右にbitシフトさせます。
{
T_{comp}=\frac{digT_{3}\times 2^{4}\times T^{2} + digT_{2}\times 2^{4} \times T}{2^8 \times 5}
}
このTに対して2次近似式で補正値を出力しています。
digT3の16bitを同様に4bit左にビットシフトして20bitにし2次の係数に、同様にdigT2の4bitシフトして20bitにして1次の係数として演算し、最後に8bit右にビットシフトしたものを5で除算して最終出力値としています。
{
T_{fine} = T_{comp} \times 5 \times 2^{10}
}
最後に圧力や湿度の補正用のt_fineはTcompを10bit左へビットシフト×5倍となっています。
とても難解でこの解釈は私の推測でしかないので気になる方は自分でもう少し掘り下げていただければと思います。

最後に以下のprint出力で温度を出力しています。

print "temp : %-6.2f ℃" % (temperature) 

compensate_P

global  t_fine
pressure = 0.0
v1 = (t_fine / 2.0) - 64000.0
v2 = (((v1 / 4.0) * (v1 / 4.0)) / 2048) * digP[5]
v2 = v2 + ((v1 * digP[4]) * 2.0)
v2 = (v2 / 4.0) + (digP[3] * 65536.0)
v1 = (((digP[2] * (((v1 / 4.0) * (v1 / 4.0)) / 8192)) / 8)  + ((digP[1] * v1) / 2.0)) / 262144
v1 = ((32768 + v1) * digP[0]) / 32768

こちらも同様に数式に起こしていきたいと思います。
先ほどの温度での{t_{fine}}{T_{comp}}の関係から

{
 v1 = (T_{comp}-25)\times 10 \times 2^{8}
}

最初に気温から25減算して 10を乗算し8bit左シフト下値がv1となります。ここから25℃を基準に補正をしていることがわかります。
ここで{T'=(v1 / 2^{16}) }とすると

{
 v2 = (\frac{digP_{5} \times T'^2}{2} + \frac{digP_{4} \times T'}{2}+digP_3) \times 2^{16}
}

と表わされます。2次近似式が生成されます。若干意味が通らない式なので何かしらの意図があってこのような値になっているのでしょう。

次に{T''=(v1/2^{19})}とすると

{
 v1 = digP_0 \times (\frac{(digP_2 \times T''^{2}+digP_1 \times T'')}{2^{15}}+1)
}

となります。こちらも温度の2次近似式ではありますが非常に小さい値になっていると考えられます。

if v1 == 0:
	return 0

v1で除算するため0の場合returnで抜けるようになっています。

pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125

これを数式に起こすと
{
 pressure = (2^{20}-P_{adc})-\frac{v2}{2^{12}})*10^5/2^5
}
となります。adc_Pも20bit仕様ですので2^20と減算をしてv2のオフセット項でオフセットさせた後、10万倍して5bit右シフトしています。v2に対して12bitも右シフトをしています。

if pressure < 0x80000000:
	pressure = (pressure * 2.0) / v1
else:
	pressure = (pressure / v1) * 2

この部分はオーバーフロー対策と精度を両立させるために計算順序を入れ替えているようです。
pythonでこの処理をする必要性がないので以下のように書き換えます。

 pressure = pressure * 2.0 /v1

最後に以下の処理を数式に起こします。

v1 = (digP[8] * (((pressure / 8.0) * (pressure / 8.0)) / 8192.0)) / 4096
v2 = ((pressure / 4.0) * digP[7]) / 8192.0
pressure = pressure + ((v1 + v2 + digP[6]) / 16.0)  

ここで{P' = pressure/2^{16}}とすると
{
 P_{comp} = pressure + \frac{(digP_8 \times P'^2 + digP_7 \times P')}{2^3}+\frac{digP_6}{2^4}
}
ここでは演算されたpressureに2次の近似補正式で補正を行っています。
分解すればするほど非常に難解な補正が入っていることがわかります。正直補正値を書き変えるのは難しいと感じます。

print "pressure : %7.2f hPa" % (pressure/100)

最後もprintで表示しています。 pressureはPa単位ですのでhPaにするために100で割っています。

compensate_H

global t_fine

補正用温度 t_fineを定義しています。

var_h = t_fine - 76800.0

t_fineとT_compの関係から数式に起こすと
{
 var_h = (T_{comp}-30)\times 10 \times 2^8
}
となります。ここから温度30度を基準にして補正していることが読み取れます。

if var_h != 0:
	var_h = (adc_H - (digH[3] * 64.0 + digH[4]/16384.0 * var_h)) * (digH[1] / 65536.0 * (1.0 + digH[5] / 67108864.0 * var_h * (1.0 + digH[2] / 67108864.0 * var_h)))
else:
	return 0
var_h = var_h * (1.0 - digH[0] * var_h / 524288.0)

この処理を数式に起こします。

{
 h_{temp} = (H_{adc}- (digH_3 \times 2^6 + \frac{digH_4 \times var_h}{2^{14}})) \times \frac{digH_1}{2^{16}} \times (1 + \frac{digH_5}{2^{26}}\times var_h + digH_2 \times digH_5 \times \frac{var_h}{2^{26}}^2 )
}

{
 humidity = h_{temp} * (1 - digH_0 \times \frac{h_{temp}}{2^{19}})
}
ここまで起こしてみましたが流石にこの補正のかけ方は何をしているのかさっぱりです。センサの設計思想がわからないとわかりません。

if var_h > 100.0:
	var_h = 100.0
elif var_h < 0.0:
	var_h = 0.0
print "hum : %6.2f %" % (var_h)

最後に%表示のために0~100までの値に制限して表示を行っています。

setup

def setup():
	osrs_t = 1			#Temperature oversampling x 1
	osrs_p = 1			#Pressure oversampling x 1
	osrs_h = 1			#Humidity oversampling x 1
	mode   = 3			#Normal mode
	t_sb   = 5			#Tstandby 1000ms
	filter = 0			#Filter off
	spi3w_en = 0			#3-wire SPI Disable
変数名 設定項目 意味
osrs_t 気温取得サンプリング数 1 x1
osrs_p 圧力取得サンプリング数 1 x1
osrs_h 湿度取得サンプリング数 1 x1
mode センサ動作モード 3 normal
t_sb スタンバイ時間 5 1000ms
filter IIRfilter 0 off
spi3w_en 3-sire SPI on/off 0 off

この設定ですが,今回私の用途は室内の温度を取得することが前提です。データシートにそのための設定例があるのでそちらに変更したいと思います。

def setup():
	osrs_t = 1			#Temperature oversampling x 1
	osrs_p = 5			#Pressure oversampling x 16
	osrs_h = 2			#Humidity oversampling x 2
	mode   = 3			#Normal mode
	t_sb   = 0			#Tstandby 0.5ms
	filter = 4                      #Filter off
	spi3w_en = 0			#3-wire SPI Disable
変数名 設定項目 意味
osrs_t 気温取得サンプリング数 2 x2
osrs_p 圧力取得サンプリング数 5 x16
osrs_h 湿度取得サンプリング数 1 x1
mode センサ動作モード 3 normal
t_sb スタンバイ時間 0 0.5ms
filter IIRfilter 4 16
spi3w_en 3-sire SPI on/off 0 off
ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode
config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en
ctrl_hum_reg  = osrs_h
writeReg(0xF2,ctrl_hum_reg)
writeReg(0xF4,ctrl_meas_reg)
writeReg(0xF5,config_reg)

次にこれらの設定値を以下のメモリーマップに従って格納しています。
f:id:kagemomiji:20160814192920p:plain
それぞれアドレスに格納する値を設定し、writeReg関数でレジスタに書き込みをを行っています。

修正プログラム

以下に私の修正したbme280読み込み修正プログラムを公開しています。
利用される方はご自由にお使いください。
github.com