温度・湿度・気圧センサーが1つになったBME280を使ってみようと思います。
実装はPython、SPI通信は4線式、GPIOライブラリは pigpio を利用します。
ちなみに、秋月電子で 1,380円(税込み)でした。
BME280 使用 温湿度・気圧センサモジュールキット
■ BME280の基本情報
仕様
- 電源電圧: 1.71V ~ 3.6V
- 通信方式: I2C、SPI(4線式)、SPI(3線式)
- 測定レンジと精度
- 温度: -40 ~ 85 ℃ (±1℃)
- 湿度: 0 ~ 100% (±3%)
- 気圧: 300 ~ 1100hPa (±1hPa)
- 分解能
- 温度: 0.01℃
- 湿度: 0.008%
- 気圧: 0.18Pa
ピン
- VDD: ラズパイの3.3V端子に接続
電源入力 - GND: ラズパイのGND端子に接続
GND - CSB: ラズパイのCE0端子に接続
チップ選択。ラズパイがこの端子をLOWにすると通信開始。 - SDI: ラズパイのMOSI端子に接続
ラズパイからBME280へのデータ入力 - SDO: ラズパイのMISO端子に接続
BME280からラズパイへのデータ出力 - SCK: ラズパイのSCLK端子に接続
クロック入力。ラズパイとBME280の通信タイミング合わせ。
■ SPIでのデータの送受信仕様
SPIモードは モード00(CPOL=0, CPHA=0)と、モード11(CPOL=1, CPHA=1) のどちらかを利用することができます。
※ 今回はモード11を利用します。
※ SPIモードについて
モード11なので、アイドル時のクロック(SCK)はHIGH、クロック(SCK)の立ち上がりでデータをサンプリング、立ち下がりでシフトです。1バイト送信して、1バイト受信する感じですね。
■ レジスタ
BME280ではレジスターを指定して設定データを書き込んだり、測定データを読み取ったりします。
■ データの書き込み/読み出し
データの書き込み
データの書き込みはCSBをLOWに設定し コントロールバイトとデータバイトのペアを送信します。
コントロールバイトには書き込み先のレジスタアドレス(bit7を0にしたもの)を指定します。
※ 例えば ctrl_meas
に設定を書き込む場合、レジスタアドレスが 0xF4
なので、bit7を 0
に変換した 0x74
がコントロールバイトとなります。
データバイトには書き込む値を指定します。
トランザクションはCSBをHIGHに設定することで終了します。
データの読み出し
データの読み出しはCSBをLOWに設定し コントロールバイトを送信します
コントロールバイトには読み出し対象のレジスタアドレス(bit7を1にしたもの)を指定します。
コントロールバイトを書き込むと、レジスタアドレスが自動的にインクリメントされ、一連のデータ(複数バイト)を受信できます。
※ 例えば temp_msb, temp_lsb, temp_xlsb
の値を読み出したければ、0xFA, 0x00, 0x00, 0x00
のようにレジスタを指定するコントロールバイトに続いて、取得したいバイト数分のデータを送信します。すると、 0x00, temp_msb, temp_lsb, temp_xlsb
のように各レジスタの値を受信できます。
トランザクションはCSBをHIGHに設定することで終了します。
■ 設定
BME280では測定前にデバイスの設定を行う必要があります。
設定に利用するレジスタは config
ctrl_meas
ctrl_hum
の3種類で、測定前にこれらのレジスタに設定を書き込みます。
レジスタの設定値を下記に示します。
spi3w_en ( config[0]
)
3線式のSPIを利用するか、4線式のSPIを利用するかを選択。
-
0
4線式のSPIを利用 -
1
3線式のSPIを利用
mode ( ctrl_meas[1:0]
)
測定モードの選択を行います。
-
00
スリープモード
起動時、リセット時のデフォルトモード。測定は行わない。 -
01,10
フォースモード
モードが設定されたときに1回だけ測定を行い、その後はスリープモードに移行。 -
11
ノーマルモード
測定と待機を繰り返すモード。待機時間は後述のt_sb
で 0.5 ~ 1000ms の範囲で指定可能。
t_sb ( config[7:5]
)
modeでノーマルモードを選択した場合の測定待機時間の設定
-
000
0.5ms -
001
62.5ms -
010
125ms -
011
250ms -
100
500ms -
101
1000ms -
110
10ms -
111
20ms
osrs_t( ctrl_meas[7:5]
) , osrs_p( ctrl_meas[4:2]
) , osrs_h( ctrl_hum[2:0]
)
温度(osrs_t)、気圧(osrs_p)、湿度(osrs_h) のオーバーサンプリング設定。
測定期間内のサンプリング周波数を増やすパラメータだと思われます。オーバーサンプリング値を高く設定することで、測定ノイズを減らすことができます。
※ オーバーサンプリング値に比例して測定時間が長くなるので、単純にサンプリング回数を指定するパラメータかもしれません。 (データシートに詳しい記載がないので不明)
※ 測定時間の計算はデータシート の 「9.1 Measurement time」を参照
-
000
測定しない -
001
オーバーサンプリング x 1 -
010
オーバーサンプリング x 2 -
011
オーバーサンプリング x 4 -
100
オーバーサンプリング x 8 -
101,others
オーバーサンプリング x 16
filter ( config[4:2]
)
IIRフィルタの係数設定。
一時的な測定値の変化やばらつきといった、ノイズの影響を減らすため、BME280には過去の測定値と今回の測定値の平均を取った値を測定値とするためのフィルターが用意されています。
フィルタではいわゆる、移動平均的な処理を行っており、この設定値が高いほど過去の測定値の重みが大きくなります。
フィルタの計算式
-
000
フィルターを利用しない -
001
フィルタ係数=2 -
010
フィルタ係数=4 -
011
フィルタ係数=8 -
100, others
フィルタ係数=16
測定フロー
測定フローは各種設定値を参照しながら、下記のフローで実行されるようです。
■ 実装
BME280からデータを取得するには、設定、測定値取得、キャリブレーションの3手順が必要です。
1. 設定
config
( 0xF5
)、 ctrl_meas
( 0xF4
)、 ctrl_hum
( 0xF2
) レジスターに設定を書き込みます。
今回利用する設定はこちら。
-
config
(0xF5
)- t_sb: 0.5ms (
000
) - filter: IIRフィルタ係数=16 (
101
) - spi3w_en: 4線式SPI (
0
)
- t_sb: 0.5ms (
-
ctrl_meas
(0xF4
)- osrs_t: オーバーサンプリング x 2 (
010
) - osrs_p: オーバーサンプリング x 16 (
101
) - mode: ノーマルモード (
11
)
- osrs_t: オーバーサンプリング x 2 (
-
ctrl_hum
(0xF2
)- osrs_h: オーバーサンプリング x 1 (
001
)
- osrs_h: オーバーサンプリング x 1 (
※ データシート の「3.5 Recommended modes of operation」に各種設定の推奨値が記載されているので参考にしてみてください。
2. 測定値取得
温度 ( 0xFA - 0xFC
)、気圧 ( 0xF7 - 0xF9
)、湿度( 0xFD - 0xFE
) をレジスタから読み出します。
※ 読みだしたデータはこの時点では温度、気圧、湿度として利用できる形式ではありません。
3. キャリブレーション
calib26 - calib41
( 0xE1 - 0xF0
)、 calib00 - calib25
( 0x88 - 0xA1
)に格納されている校正データを利用して、読みだしたデータを校正します。
校正を行うことで、初めて温度・気圧・湿度として意味のある値になります。
※ 校正データの読み出しはデータシートの「4.2.2 Trimming parameter readout」を参照
※ 校正の方法はデータシートの「4.2.3 Compensation formulas」を参照。
実装
import time
from typing import Union, List, Tuple
import pigpio
from pprint import pprint
from collections import OrderedDict
import enum
def int_to_binary(n: int, bits: int = 8) -> str:
return ''.join([str(n >> i & 1 ) for i in reversed(range(0, bits))])
def bytes_to_binary(data: Union[bytearray,bytes]) -> List[str]:
return [int_to_binary(byte) for byte in data]
def write_register(pi, spi_handler, register_addr: int, data: int):
"""
レジスターに設定を書き込む
Args:
register_addr: レジスタアドレス
data: 書き込むデータ (1バイト)
"""
# 書込み時のregister指定は最上位ビットを0にする
write_data = (register_addr & 0b01111111) << 8 | data
write_data = write_data.to_bytes(2, "big")
cnt, read_data = pi.spi_xfer(spi_handler, write_data)
#print(f"cnt={cnt}, read_data={bytes_to_binary(read_data)}")
def read_register(pi, spi_handler, register_addr: int, num_bytes: int) -> bytes:
"""
レジスターから指定したバイト数読み取る
Args:
register_addr: レジスタアドレス
num_bytes: 読み出すバイト数
"""
# 読込み時のregister指定は最上位ビットを1にする
write_data = (register_addr | 0b10000000) << (8 * num_bytes)
write_data = write_data.to_bytes(num_bytes + 1, "big")
cnt, read_data = pi.spi_xfer(spi_handler, write_data)
if cnt != (num_bytes + 1):
raise Exception(f"ReadError: cnt={cnt} (expected={num_bytes+1})")
return read_data[1:]
def read_calibration_data(pi, spi_handler):
"""
キャリブレーション用のデータを取得する
データシートの「4.2.2 Trimming parameter readout」を参照
"""
cal_1 = read_register(pi, spi_handler, 0x88, 24)
cal_2 = read_register(pi, spi_handler, 0xA1, 1)
cal_3 = read_register(pi, spi_handler, 0xE1, 7)
cal_data = OrderedDict([
# --- --- --- 0x88 ~ 0x9F --- --- ---
("dig_T1", int.from_bytes(cal_1[0:2] , byteorder="little", signed=False)),
("dig_T2", int.from_bytes(cal_1[2:4] , byteorder="little", signed=True)),
("dig_T3", int.from_bytes(cal_1[4:6] , byteorder="little", signed=True)),
("dig_P1", int.from_bytes(cal_1[6:8] , byteorder="little", signed=False)),
("dig_P2", int.from_bytes(cal_1[8:10] , byteorder="little", signed=True)),
("dig_P3", int.from_bytes(cal_1[10:12], byteorder="little", signed=True)),
("dig_P4", int.from_bytes(cal_1[12:14], byteorder="little", signed=True)),
("dig_P5", int.from_bytes(cal_1[14:16], byteorder="little", signed=True)),
("dig_P6", int.from_bytes(cal_1[16:18], byteorder="little", signed=True)),
("dig_P7", int.from_bytes(cal_1[18:20], byteorder="little", signed=True)),
("dig_P8", int.from_bytes(cal_1[20:22], byteorder="little", signed=True)),
("dig_P9", int.from_bytes(cal_1[22:24], byteorder="little", signed=True)),
# --- --- --- 0xA1 --- --- ---
("dig_H1", int.from_bytes(cal_2, byteorder="little", signed=False)),
# --- --- --- 0xE1 ~ 0xE7 --- --- ---
("dig_H2", int.from_bytes(cal_3[0:2], byteorder="little", signed=True)),
("dig_H3", int.from_bytes(cal_3[2:3], byteorder="little", signed=False)),
("dig_H4", cal_3[3] << 4 | (0b00001111 & cal_3[4])), # 読み出し方が他と異なるので注意
("dig_H5", cal_3[5] << 4 | (0b00001111 & (cal_3[4] >> 4))), # 読み出し方が他と異なるので注意
("dig_H6", int.from_bytes(cal_3[7:8], byteorder="little", signed=True)),
])
return cal_data
def read_temp(pi, spi_handler, cal_data: OrderedDict) -> Tuple[int, float]:
"""
温度を読み取る
"""
temp_register = 0xFA
read_bytes = read_register(pi, spi_handler, temp_register, 3)
# 温度は20ビットフォーマットで受信され、正値で32ビット符号付き整数で保存
temp_raw = int.from_bytes(read_bytes, byteorder="big") >> 4
# 以下キャリブレーション (データシートの「4.2.3 Compensation formulas」を参照)
var1 = (((temp_raw >> 3) - (cal_data["dig_T1"] << 1)) * cal_data["dig_T2"]) >> 11
var2 = (((((temp_raw >> 4) - cal_data["dig_T1"]) * ((temp_raw >> 4) - cal_data["dig_T1"])) >> 12) * (cal_data["dig_T3"])) >> 14
t_fine = var1 + var2
temp = ((t_fine * 5 + 128) >> 8) / 100 # DegC
return (t_fine, temp)
def read_pressure(pi, spi_handler, cal_data: OrderedDict, t_fine: int) -> float:
"""
気圧を読み取る
"""
read_bytes = read_register(pi, spi_handler, 0xF7, 3)
# 気圧は20ビットフォーマットで受信され、正値で32ビット符号付き整数で保存
pressure_raw = int.from_bytes(read_bytes, byteorder="big") >> 4
# 以下キャリブレーション (データシートの「4.2.3 Compensation formulas」を参照)
var1 = t_fine - 128000
var2 = var1 * var1 * cal_data["dig_P6"]
var2 = var2 + ((var1 * cal_data["dig_P5"]) << 17)
var2 = var2 + ((cal_data["dig_P4"]) << 35)
var1 = ((var1 * var1 * cal_data["dig_P3"]) >> 8) + ((var1 * cal_data["dig_P2"]) << 12)
var1 = ((1 << 47) + var1) * (cal_data["dig_P1"]) >> 33
if (var1 == 0):
return 0 # avoid exception caused by division by zero
p = 1048576 - pressure_raw
p = (((p << 31) - var2) * 3125) // var1
var1 = ((cal_data["dig_P9"]) * (p >> 13) * (p >> 13)) >> 25
var2 = ((cal_data["dig_P8"]) * p) >> 19
p = ((p + var1 + var2) >> 8) + ((cal_data["dig_P7"]) << 4)
return p / 256 / 100 # hPa
def read_humidity(pi, spi_handler, cal_data: OrderedDict, t_fine: int) -> float:
"""
湿度を読み取る
"""
read_bytes = read_register(pi, spi_handler, 0xFD, 2)
# 湿度は16ビットフォーマットで受信され、32ビット符号付き整数で保存
humidity_raw = int.from_bytes(read_bytes, byteorder="big")
# 以下キャリブレーション (データシートの「4.2.3 Compensation formulas」を参照)
v_x1_u32r = t_fine - 76800
v_x1_u32r = (
(
(((humidity_raw << 14) - ((cal_data["dig_H4"]) << 20) - ((cal_data["dig_H5"]) * v_x1_u32r)) + (16384)) >> 15
) * (
((((((v_x1_u32r * (cal_data["dig_H6"])) >> 10) * (((v_x1_u32r * (cal_data["dig_H3"])) >> 11) + 32768)) >> 10) + 2097152) * (cal_data["dig_H2"]) + 8192) >> 14
)
)
v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * (cal_data["dig_H1"])) >> 4))
v_x1_u32r = 0 if (v_x1_u32r < 0) else v_x1_u32r
v_x1_u32r = 419430400 if (v_x1_u32r > 419430400) else v_x1_u32r
return (v_x1_u32r >> 12) / 1024 # %RH
def main(pi, spi_handler):
# 動作設定
config_reg = 0x5F
t_sb = 0b000 # 測定待機時間 0.5ms
filter = 0b101 # IIRフィルター係数 16
spi3w_en = 0b0 # 4線式SPI
reg_data = (t_sb << 5) | (filter << 2) | spi3w_en
write_register(pi, spi_handler, config_reg, reg_data)
# 温度・気圧測定の設定
ctrl_meas_reg = 0xF4
osrs_t = 0b010 # 温度 オーバーサンプリングx2
osrs_p = 0b101 # 気圧 オーバーサンプリングx16
mode = 0b11 # ノーマルモード
reg_data = (osrs_t << 5) | (osrs_p << 2) | mode
write_register(pi, spi_handler, ctrl_meas_reg, reg_data)
# 湿度測定の設定
ctrl_hum_reg = 0xF2
osrs_h = 0b001 # 湿度 オーバーサンプリングx1
reg_data = osrs_h
write_register(pi, spi_handler, ctrl_hum_reg, reg_data)
# キャリブレーションデータの読み出し
cal_data = read_calibration_data(pi, spi_handler)
while True:
# 温度取得
t_fine, temp = read_temp(pi, spi_handler, cal_data)
print(f"温度: {temp} DegC")
# 気圧取得
press = read_pressure(pi, spi_handler, cal_data, t_fine)
print(f"気圧: {press} hPa")
# 湿度取得
hum = read_humidity(pi, spi_handler, cal_data, t_fine)
print(f"湿度: {hum} %RH")
print()
time.sleep(1)
if __name__ == "__main__":
pi = pigpio.pi()
if not pi.connected:
raise Exception("pigpio connection faild...")
# オプション (http://abyz.me.uk/rpi/pigpio/python.html#spi_open)
# 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
# b b b b b b R T n n n n W A u2 u1 u0 p2 p1 p0 m m
# mm: SPIモード
# A: メインSPI(0), AuxSPI(1) どちらを利用するか選択
# W: 3線のSPIを利用するなら(1)、4線なら(0) (メインSPIでしか利用できない)
# あとは使いどころあるのかよくわからん、、、
spi_mode = 0b11 # SPIモード11を設定。アイドル時のクロックはHIGH(CPOL=1)、クロックがLOWになるときにデータをサンプリング(CPHA=1)
spi_option = 0b0 | spi_mode
spi_clock_speed = 1_000_000 # 1MHz
spi_channel = 0
spi_handler = pi.spi_open(spi_channel, spi_clock_speed, spi_option)
try:
main(pi, spi_handler)
finally:
pi.spi_close(spi_handler)
pi.stop()
できた!!
温度: 24.95 DegC
気圧: 1012.3351953125 hPa
湿度: 55.8447265625 %RH
温度: 24.94 DegC
気圧: 1012.3591015625 hPa
湿度: 55.9775390625 %RH
温度: 24.95 DegC
気圧: 1012.3660546875 hPa
湿度: 56.04296875 %RH
温度: 24.95 DegC
気圧: 1012.3418359375 hPa
湿度: 56.021484375 %RH