【電子工作】BME280(温度・気圧・湿度センサー)をSPI通信で利用する

温度・湿度・気圧センサーが1つになったBME280を使ってみようと思います。
実装はPython、SPI通信は4線式、GPIOライブラリは [pigpio](http://abyz.me.uk/rpi/pigpio/python.html) を利用します。

ちなみに、秋月電子で 1,380円(税込み)でした。
[	BME280 使用　温湿度・気圧センサモジュールキット](https://akizukidenshi.com/catalog/g/gK-09421/)

# ■ BME280の基本情報

[BME280 データシート](https://www.mouser.jp/datasheet/2/783/bst_bme280_ds002-2238172.pdf)

## 仕様

- 電源電圧: 1.71V ~ 3.6V
- 通信方式: I2C、SPI(4線式)、SPI(3線式)
- 測定レンジと精度
    - 温度: -40 ~ 85 ℃  (±1℃)
    - 湿度: 0 ~ 100% (±3%)
    - 気圧: 300 ~ 1100hPa (±1hPa)
 - 分解能
     - 温度: 0.01℃
     - 湿度: 0.008%
     - 気圧: 0.18Pa

## ピン

1. VDD: ラズパイの3.3V端子に接続
電源入力
1. GND: ラズパイのGND端子に接続
GND
1. CSB: ラズパイのCE0端子に接続
チップ選択。ラズパイがこの端子をLOWにすると通信開始。
1. SDI: ラズパイのMOSI端子に接続
ラズパイからBME280へのデータ入力
1. SDO: ラズパイのMISO端子に接続
BME280からラズパイへのデータ出力
1. SCK: ラズパイのSCLK端子に接続
クロック入力。ラズパイとBME280の通信タイミング合わせ。

# ■ SPIでのデータの送受信仕様

SPIモードは モード00(CPOL=0, CPHA=0)と、モード11(CPOL=1, CPHA=1) のどちらかを利用することができます。
※ 今回はモード11を利用します。
※ [SPIモードについて](https://qiita.com/ktamido/items/937712efa5c2640eb472#-spi%E3%83%A2%E3%83%BC%E3%83%89%E3%81%8C%E3%82%88%E3%81%8F%E5%88%86%E3%81%8B%E3%82%89%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%A7%E8%AA%BF%E3%81%B9%E3%81%A6%E3%81%BF%E3%81%9F)

モード11なので、アイドル時のクロック(SCK)はHIGH、クロック(SCK)の立ち上がりでデータをサンプリング、立ち下がりでシフトです。1バイト送信して、1バイト受信する感じですね。


![スクリーンショット 2023-09-14 23.57.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/7be4da21-3382-ee06-774a-2367e2472b58.png)



# ■ レジスタ

BME280ではレジスターを指定して設定データを書き込んだり、測定データを読み取ったりします。


![スクリーンショット 2023-09-16 14.34.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/ee654b2c-ba8d-0a0d-6b12-614ffffae6ca.png)



# ■ データの書き込み/読み出し

## データの書き込み

データの書き込みはCSBをLOWに設定し コントロールバイトとデータバイトのペアを送信します。
コントロールバイトには書き込み先のレジスタアドレス(bit7を0にしたもの)を指定します。
※ 例えば `ctrl_meas` に設定を書き込む場合、レジスタアドレスが `0xF4`なので、bit7を `0` に変換した `0x74` がコントロールバイトとなります。
データバイトには書き込む値を指定します。
トランザクションはCSBをHIGHに設定することで終了します。

![スクリーンショット 2023-09-17 15.34.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/9534dd34-d925-0466-9b38-1d3ea5e702f2.png)



## データの読み出し

データの読み出しはCSBをLOWに設定し コントロールバイトを送信します
コントロールバイトには読み出し対象のレジスタアドレス(bit7を1にしたもの)を指定します。
コントロールバイトを書き込むと、レジスタアドレスが自動的にインクリメントされ、一連のデータ(複数バイト)を受信できます。
※ 例えば `temp_msb, temp_lsb, temp_xlsb` の値を読み出したければ、`0xFA, 0x00, 0x00, 0x00` のようにレジスタを指定するコントロールバイトに続いて、取得したいバイト数分のデータを送信します。すると、 `0x00, temp_msb, temp_lsb, temp_xlsb` のように各レジスタの値を受信できます。
トランザクションはCSBをHIGHに設定することで終了します。


![スクリーンショット 2023-09-17 16.08.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/2605e27f-77b6-282b-cb8a-22815eb6e794.png)


# ■ 設定

BME280では測定前にデバイスの設定を行う必要があります。
設定に利用するレジスタは `config` `ctrl_meas` `ctrl_hum` の3種類で、測定前にこれらのレジスタに設定を書き込みます。
レジスタの設定値を下記に示します。

![スクリーンショット 2023-09-16 12.10.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/2a5c7cea-f568-deb1-3b73-9091f53949f6.png)


## 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) のオーバーサンプリング設定。
測定期間内のサンプリング周波数を増やすパラメータだと思われます。オーバーサンプリング値を高く設定することで、測定ノイズを減らすことができます。

※ オーバーサンプリング値に比例して測定時間が長くなるので、単純にサンプリング回数を指定するパラメータかもしれません。 (データシートに詳しい記載がないので不明)
※ 測定時間の計算は[データシート](https://www.mouser.jp/datasheet/2/783/bst_bme280_ds002-2238172.pdf) の 「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には過去の測定値と今回の測定値の平均を取った値を測定値とするためのフィルターが用意されています。
フィルタではいわゆる、移動平均的な処理を行っており、この設定値が高いほど過去の測定値の重みが大きくなります。

フィルタの計算式


![スクリーンショット 2023-09-16 13.49.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/475236fd-05b3-7bdc-1da6-77b6742b8a35.png)



- `000` フィルターを利用しない
- `001` フィルタ係数=2
- `010` フィルタ係数=4
- `011` フィルタ係数=8
- `100, others` フィルタ係数=16


## 測定フロー

測定フローは各種設定値を参照しながら、下記のフローで実行されるようです。


![スクリーンショット 2023-09-16 11.40.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92383/e2f54671-426d-83e5-4d87-b7c2b7a12366.png)



# ■ 実装

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`)
- `ctrl_meas` ( `0xF4` )
    - osrs_t: オーバーサンプリング x 2 ( `010` )
    - osrs_p: オーバーサンプリング x 16 ( `101` )
    - mode: ノーマルモード (`11`)
- `ctrl_hum` ( `0xF2` )
    - osrs_h: オーバーサンプリング x 1 ( `001` )

※ [データシート](https://www.mouser.jp/datasheet/2/783/bst_bme280_ds002-2238172.pdf) の「3.5 Recommended modes of operation」に各種設定の推奨値が記載されているので参考にしてみてください。

### 2. 測定値取得

温度 ( `0xFA - 0xFC` )、気圧 ( `0xF7 - 0xF9` )、湿度( `0xFD - 0xFE` ) をレジスタから読み出します。
※ 読みだしたデータはこの時点では温度、気圧、湿度として利用できる形式ではありません。


### 3. キャリブレーション

`calib26 - calib41` ( `0xE1 - 0xF0` )、 `calib00 - calib25` ( `0x88 - 0xA1` )に格納されている校正データを利用して、読みだしたデータを校正します。
校正を行うことで、初めて温度・気圧・湿度として意味のある値になります。

※ 校正データの読み出しは[データシート](https://www.mouser.jp/datasheet/2/783/bst_bme280_ds002-2238172.pdf)の「4.2.2 Trimming parameter readout」を参照
※ 校正の方法は[データシート](https://www.mouser.jp/datasheet/2/783/bst_bme280_ds002-2238172.pdf)の「4.2.3 Compensation formulas」を参照。

## 実装

```python
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
```