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にあると書かれています。
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
ここが補正になっています。しかしこれではどういった補正なのかわからないので実際にこの式を何かしらの意味がある形に変形して数式に起こしていきます。
温度は32bitのsigned型に対して20bitのフォーマットで出力されているのでdig_T1(16bit)を4bit左にビットシフトして20bitフォーマットにして減算し、20bit右にbitシフトさせます。
このTに対して2次近似式で補正値を出力しています。
digT3の16bitを同様に4bit左にビットシフトして20bitにし2次の係数に、同様にdigT2の4bitシフトして20bitにして1次の係数として演算し、最後に8bit右にビットシフトしたものを5で除算して最終出力値としています。
最後に圧力や湿度の補正用の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
こちらも同様に数式に起こしていきたいと思います。
先ほどの温度でのとの関係から
最初に気温から25減算して 10を乗算し8bit左シフト下値がv1となります。ここから25℃を基準に補正をしていることがわかります。
ここでとすると
と表わされます。2次近似式が生成されます。若干意味が通らない式なので何かしらの意図があってこのような値になっているのでしょう。
次にとすると
となります。こちらも温度の2次近似式ではありますが非常に小さい値になっていると考えられます。
if v1 == 0: return 0
v1で除算するため0の場合returnで抜けるようになっています。
pressure = ((1048576 - adc_P) - (v2 / 4096)) * 3125
これを数式に起こすと
となります。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)
ここでとすると
ここでは演算された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の関係から数式に起こすと
となります。ここから温度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)
この処理を数式に起こします。
ここまで起こしてみましたが流石にこの補正のかけ方は何をしているのかさっぱりです。センサの設計思想がわからないとわかりません。
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)
次にこれらの設定値を以下のメモリーマップに従って格納しています。
それぞれアドレスに格納する値を設定し、writeReg関数でレジスタに書き込みをを行っています。
修正プログラム
以下に私の修正したbme280読み込み修正プログラムを公開しています。
利用される方はご自由にお使いください。
github.com