GroveBeginnerKit を、C++(Arduino)とRuby(ラズパイ,Rboard) で使う　I2C - 気圧センサー(BMP280) 編

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

[Grove Beginner Kit for Arduino を使ってみる記事](https://qiita.com/HirohitoHigashi/items/dc43ea72d572b21a6823)の第5回、今回は I<sup>2</sup>C 接続の気圧センサーを題材にします。

このレポートでは、

<b>「メーカーのデータシートを見て、センサICを直接コントロールをすること」</b>

を、方針とします。
もちろん、プログラムを書く上で既存のライブラリ等の実装を参考にすることは良い事です。しかしながらこのレポートは、I2C バスやセンサ IC の扱い方、データシートの読み方を示す事も目的としているため、できるだけ低レイヤーでの説明を行います。

ターゲットは、以下の通り。
* Arduino　- 付属の Arduino Uno 互換機
* Ruby - Raspberry Pi + [Grove Base HAT for Raspberry Pi](https://www.seeedstudio.com/Grove-Base-Hat-for-Raspberry-Pi.html)
* mruby/c - [RBoard](https://www.sjc-inc.co.jp/service/rboard)


# 気圧センサー (Barometer Sensor (BMP280))

| 外観 | 回路図 |
|:-:|:-:|
| ![grove_AirPressure.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/b315554d-d93b-6829-8975-02475d7ecf0c.jpeg) | （センサーがGrove端子に接続されてるだけなので省略） |

（[メーカーWiki: Grove - Barometer Sensor (BMP280)](https://wiki.seeedstudio.com/Grove-Barometer_Sensor-BMP280/) より引用）

搭載されているセンサーICは、ボッシュの BMP280 です。
このキットに付属しているモジュールは、メーカーページに掲載されているモジュールとは、サイズが違いますが、回路は同じようです。


# データシートの確認

[ICメーカー製品ページ](https://www.bosch-sensortec.com/products/environmental-sensors/pressure-sensors/bmp280/) で、データシートを確認します。
この IC は、気圧計の後段にデジタルフィルターが入っており、フィルター定数もユースケースごとに最適値が公開されていて、とても親切ですね。
![grove_AirPressure_FilterSelection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/dcae893d-49ac-5293-ba82-fe8fa9087aa2.png)

データシートで、プログラム作成に必要な情報は以下ぐらいでしょうか。

 *　電源投入時は停止モードなので、連続測定（Normalと呼称）か、1回測定（Forcedと呼称）モードへ遷移させる必要がある
 *　その際に、各種フィルタ定数も設定する
 *　気圧とともに気温も取得できる
 *　各データは 20bit長であり、圧力単位(Pa), 温度(Degree) への変換は、提供されるライブラリを使うかデータシートに記載のサンプルコードによって行える
 *　I2Cアドレスは、0x77 （このGroveモジュールの仕様）

サンプルプログラムの作成は、以下の方針で行います。

 * 気圧とともに温度も取得する
 * 1回測定(Forced)モードを使い、プログラム側のタイミングでデータを取得する
 * Use case "Indoor navigation" で示される、最高解像度で作ってみる

## ICの初期化

データシートのレジスタマップとその詳細説明より、`config`, `ctrl_meas` 2つのレジスタを設定するだけで良いことが分かります。

### config(F5) レジスタ

![grove_AirPressure_Reg_config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/4957ae09-11a1-a9f4-6110-78fd294f5ca1.png)

今回、`config` レジスタで設定すべきは、filter ビットです。このパラメータは、Filter selection 表の `IIR filter coeff` にあたります。しかしこの　3bit をどう設定すれば表に記載された OFF, 4, 16 に設定できるのか明記してありません。今回はメーカーがライブラリを公開しているので、そちらを参照すると以下の通りでした。
```c
/*! @name Filter coefficient macros */
#define BMP2_FILTER_OFF                               UINT8_C(0x00)
#define BMP2_FILTER_COEFF_2                           UINT8_C(0x01)
#define BMP2_FILTER_COEFF_4                           UINT8_C(0x02)
#define BMP2_FILTER_COEFF_8                           UINT8_C(0x03)
#define BMP2_FILTER_COEFF_16                          UINT8_C(0x04)
```

### ctrl_meas(F4) レジスタ

![grove_AirPressure_Reg_ctrl_meas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/beda573d-f776-65b8-57ec-a621ad699664.png)

`ctrl_meas` レジスタは、同じくフィルタ定数と併せて測定モードの指示ビットを持っています。こちらは別表にきちんと設定値とビットパターンの表がありますので迷うことなく設定値が分かります。
また、測定開始の指示をするビットも持っているため、フィルタ定数ビットはプログラム側の変数に保存しておいた方がよさそうです。

### トリミングデータ

このセンサICでは、測定データの row 値から、実際の値 (Pa, degree) へ換算するために、IC固有のトリミングデータを必要とします。この値は NVRAM に書かれており、ということは測定中に変わることはないので、初期化時にあらかじめ読んでおきます。


## データの読み込み

測定中/測定完了の状態は、`status` レジスタの `measuring` ビットに反映されます。
![grove_AirPressure_Reg_status.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/7326b59d-33a6-9d0c-6bd0-026654555d69.png)

測定開始トリガーをかけた後、このビットが 0 になってから測定データが反映される `press_*`, `temp_*` レジスタを読み込みます。

その後、 raw値から実際の値に変換する処理が必要ですが、このデータシートでは、数式ではなくC言語によるコードで、
 * 固定小数点で、64bit int が使える場合
 * 固定小数点で、32bit int のみの場合
 * 浮動小数点を使う場合

の、3種類で例示してあります。
 


# サンプルプログラム

## Arduino

センサーを、`I2C` 端子に接続します。
私が今使っている開発環境「ArduinoIDE 2.3.2」では、`uint64_t` と書いても、コンパイルエラーにはならないですが、公式ドキュメントでは 32bit　までしか記載が無いので、変換処理は、32bit 固定小数点版を使う事にします。

```c++:BMP280_AirPressure.ino
#include <Wire.h>

typedef int32_t  BMP280_S32_t;
typedef uint32_t BMP280_U32_t;

const int I2C_ADRS = 0x77;
int ctrl_meas;
uint16_t dig_T1, dig_P1;
int16_t dig_T2, dig_T3, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;

void setup() {
  Wire.begin();
  Serial.begin(9600);

  // Software reset
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xe0 );
  Wire.write( 0xb6 );
  Wire.endTransmission();

  // F5:config filter:100 -> 000_100_00
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xf5 );
  Wire.write( 0x10 );
  Wire.endTransmission();
 
  // F4:ctrl_meas
  ctrl_meas = 0x54;
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xf4 );
  Wire.write( ctrl_meas );
  Wire.endTransmission();

  // Read the timming parameter
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0x88 );
  Wire.endTransmission( false );
  Wire.requestFrom( I2C_ADRS, 24 );
  byte data[24];
  for( int i = 0; i < 24; i++ ) {
    data[i] = Wire.read();
  }

  dig_T1 = (uint16_t)(data[ 1] << 8 | data[ 0]);
  dig_T2 = (int16_t)( data[ 3] << 8 | data[ 2]);
  dig_T3 = (int16_t)( data[ 5] << 8 | data[ 4]);                                
  dig_P1 = (uint16_t)(data[ 7] << 8 | data[ 6]);                                 
  dig_P2 = (int16_t)( data[ 9] << 8 | data[ 8]);                                
  dig_P3 = (int16_t)( data[11] << 8 | data[10]);                                
  dig_P4 = (int16_t)( data[13] << 8 | data[12]);                                
  dig_P5 = (int16_t)( data[15] << 8 | data[14]);                                
  dig_P6 = (int16_t)( data[17] << 8 | data[16]);                                
  dig_P7 = (int16_t)( data[19] << 8 | data[18]);                                
  dig_P8 = (int16_t)( data[21] << 8 | data[20]);                                
  dig_P9 = (int16_t)( data[23] << 8 | data[22]);                                
}

void loop() {
  // Start measurement
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xf4 );
  Wire.write( ctrl_meas | 0x01 );
  Wire.endTransmission();

  // wait for measurement done.
  int sts;
  do {
    Wire.beginTransmission( I2C_ADRS );
    Wire.write( 0xf3 );
    Wire.endTransmission( false );
    Wire.requestFrom( I2C_ADRS, 1 );
    sts = Wire.read();
  } while( (sts & 0x08) != 0 );

  // read data
  Wire.beginTransmission( I2C_ADRS );
  Wire.write( 0xf7 );
  Wire.endTransmission( false );
  Wire.requestFrom( I2C_ADRS, 6 );
  byte data[6];
  for( int i = 0; i < 6; i++ ) {
    data[i] = Wire.read();
  }
  BMP280_S32_t adc_P = ((BMP280_S32_t)data[0] << 12) | ((BMP280_S32_t)data[1] << 4) | (data[2] >> 4);
  BMP280_S32_t adc_T = ((BMP280_S32_t)data[3] << 12) | ((BMP280_S32_t)data[4] << 4) | (data[5] >> 4);

  // Data compensation in 32 bit fixed point.
  //  (quote from datasheet Appendix.1)
  //
  // Convert temperature in DegC, resolution is 0.01 DegC. Output value of “5123” equals 51.23 DegC.
  BMP280_S32_t t_fine;
  BMP280_S32_t var1, var2, T;
  var1 = ((((adc_T>>3) - ((BMP280_S32_t)dig_T1<<1))) * ((BMP280_S32_t)dig_T2)) >> 11;
  var2 = (((((adc_T>>4) - ((BMP280_S32_t)dig_T1)) * ((adc_T>>4) - ((BMP280_S32_t)dig_T1))) >> 12) *
          ((BMP280_S32_t)dig_T3)) >> 14;
  t_fine = var1 + var2;
  T = (t_fine * 5 + 128) >> 8;

  // Convert pressure in Pa as unsigned 32 bit integer. Output value of “96386” equals 96386 Pa = 963.86 hPa
  BMP280_U32_t p;
  var1 = (((BMP280_S32_t)t_fine)>>1) - (BMP280_S32_t)64000;
  var2 = (((var1>>2) * (var1>>2)) >> 11 ) * ((BMP280_S32_t)dig_P6);
  var2 = var2 + ((var1*((BMP280_S32_t)dig_P5))<<1);
  var2 = (var2>>2)+(((BMP280_S32_t)dig_P4)<<16);
  var1 = (((dig_P3 * (((var1>>2) * (var1>>2)) >> 13 )) >> 3) + ((((BMP280_S32_t)dig_P2) * var1)>>1))>>18;
  var1 =((((32768+var1))*((BMP280_S32_t)dig_P1))>>15);
  if (var1 == 0)
  {
    return; // avoid exception caused by division by zero
  }
  p = (((BMP280_U32_t)(((BMP280_S32_t)1048576)-adc_P)-(var2>>12)))*3125;
  if (p < 0x80000000)
  {
    p = (p << 1) / ((BMP280_U32_t)var1);
  }
  else
  {
    p = (p / (BMP280_U32_t)var1) * 2;
  }
  var1 = (((BMP280_S32_t)dig_P9) * ((BMP280_S32_t)(((p>>3) * (p>>3))>>13)))>>12;
  var2 = (((BMP280_S32_t)(p>>2)) * ((BMP280_S32_t)dig_P8))>>13;
  p = (BMP280_U32_t)((BMP280_S32_t)p + ((var1 + var2 + dig_P7) >> 4));

  // Display
  Serial.print( (double)p / 100, 2 );
  Serial.print(" hPa  ");
  Serial.print( (double)T / 100, 2 );
  Serial.println("degree");

  delay( 1000 );
}
```


## Raspberry Pi (CRuby)

センサーを、`I2C` 端子に接続します。
CRubyの整数はビット数の制限が無いので、64bit int 版のC言語サンプルコードを参考にしてみます。C言語によるサンプルコードをコピー＆ペーストし、Rubyでは不要なキャストなどを消して、適切に成形したコードになります。

```ruby:BMP280_AirPressure_int64.rb
require "mruby/i2c"

class BMP280
  I2C_ADRS = 0x77

  #
  # init instance
  #
  def initialize( i2c_bus, address = I2C_ADRS )
    @bus = i2c_bus
    @address = address
  end

  #
  # init sensor
  #
  def init()
    @bus.write( @address, 0xe0, 0xb6 )  # reset

    # filter: 100  16
    @bus.write( @address, 0xf5, 0b000_100_00 )  # F5:config

    # osrs_t: 010  x2
    # osrs_p: 101  x16
    @ctrl_meas = 0b010_101_00
    @bus.write( @address, 0xf4, @ctrl_meas )    # F4:ctrl_meas

    # read the trimming parameter
    data = @bus.read( @address, 24, 0x88 ).bytes
    @dig_T1 = to_uint16( data[ 1], data[ 0] )
    @dig_T2 = to_int16(  data[ 3], data[ 2] )
    @dig_T3 = to_int16(  data[ 5], data[ 4] )
    @dig_P1 = to_uint16( data[ 7], data[ 6] )
    @dig_P2 = to_int16(  data[ 9], data[ 8] )
    @dig_P3 = to_int16(  data[11], data[10] )
    @dig_P4 = to_int16(  data[13], data[12] )
    @dig_P5 = to_int16(  data[15], data[14] )
    @dig_P6 = to_int16(  data[17], data[16] )
    @dig_P7 = to_int16(  data[19], data[18] )
    @dig_P8 = to_int16(  data[21], data[20] )
    @dig_P9 = to_int16(  data[23], data[22] )
  end

  #
  # meas (整数演算版 int64)
  #
  def meas()
    # start measurement
    @bus.write( @address, 0xf4, @ctrl_meas | 0b01 )  # F4:ctrl_meas

    # wait for measurement done.
    while true
      sts = @bus.read( @address, 1, 0xf3 )
      break if sts.getbyte(0)[3] == 0
    end

    # read data
    data = @bus.read( @address, 6, 0xf7 ).bytes
    adc_P = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
    adc_T = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)

    # convert temperature
    var1 = (((adc_T >> 3) - (@dig_T1 << 1)) * @dig_T2) >> 11
    var2 = (( (((adc_T >> 4) - @dig_T1) * ((adc_T >> 4) - @dig_T1)) >> 12) * @dig_T3) >> 14
    t_fine = var1 + var2
    t = (t_fine * 5 + 128) >> 8

    # convert pressure
    var1 = t_fine - 128000
    var2 = var1 * var1 * @dig_P6
    var2 = var2 + ((var1 * @dig_P5) << 17)
    var2 = var2 + (@dig_P4 << 35)
    var1 = ((var1 * var1 * @dig_P3) >> 8) + ((var1 * @dig_P2) << 12)
    var1 = ((1 << 47) + var1) * @dig_P1 >> 33
    if var1 == 0
      return { :pressure=>0.0, :temperature=>t.to_f / 100 }
    end

    prs = 1048576 - adc_P
    prs = (((prs << 31) - var2) * 3125) / var1
    var1 = (@dig_P9 * (prs >> 13) * (prs >> 13)) >> 25
    var2 = (@dig_P8 * prs) >> 19
    prs = ((prs + var1 + var2) >> 8) + (@dig_P7 << 4)

    # 最後に float にして返す
    return { :pressure=>prs.to_f / 256, :temperature=>t.to_f / 100 }
  end


  # convert 2 bytes to a int16 value
  def to_int16( b1, b2 )
    return (b1 << 8 | b2) - ((b1 & 0x80) << 9)
  end

  # convert 2 bytes to a uint16 value
  def to_uint16( b1, b2 )
    return (b1 << 8 | b2)
  end
end

sensor = BMP280.new( I2C.new )
sensor.init
while true
  data = sensor.meas
  printf("%.3f hPa  %.2f degree\n", data[:pressure]/100, data[:temperature] )

  sleep 1
end
```


## RBoard (mruby/c)

センサーを、`I2C` 端子に接続します。
mruby/c は、int64 が使えるかはコンパイル時に定数 MRBC_INT64　が定義されているかによります。RBoard の標準ファームウェアではこれは定義されていないので、浮動小数点版を使ってみました。

```ruby:BMP280_AirPressure_float.rb
class BMP280
  I2C_ADRS = 0x77

  #
  # init instance
  #
  def initialize( i2c_bus, address = I2C_ADRS )
    @bus = i2c_bus
    @address = address
  end

  #
  # init sensor
  #
  def init()
    @bus.write( @address, 0xe0, 0xb6 )  # reset

    # filter: 100  16
    @bus.write( @address, 0xf5, 0b000_100_00 )  # F5:config

    # osrs_t: 010  x2
    # osrs_p: 101  x16
    @ctrl_meas = 0b010_101_00
    @bus.write( @address, 0xf4, @ctrl_meas )    # F4:ctrl_meas

    # read the trimming parameter
    data = @bus.read( @address, 24, 0x88 ).bytes
    @dig_T1 = to_uint16( data[ 1], data[ 0] )
    @dig_T2 = to_int16(  data[ 3], data[ 2] )
    @dig_T3 = to_int16(  data[ 5], data[ 4] )
    @dig_P1 = to_uint16( data[ 7], data[ 6] )
    @dig_P2 = to_int16(  data[ 9], data[ 8] )
    @dig_P3 = to_int16(  data[11], data[10] )
    @dig_P4 = to_int16(  data[13], data[12] )
    @dig_P5 = to_int16(  data[15], data[14] )
    @dig_P6 = to_int16(  data[17], data[16] )
    @dig_P7 = to_int16(  data[19], data[18] )
    @dig_P8 = to_int16(  data[21], data[20] )
    @dig_P9 = to_int16(  data[23], data[22] )
  end

  #
  # meas (浮動小数点演算版)
  #
  def meas()
    # start measurement
    @bus.write( @address, 0xf4, @ctrl_meas | 0b01 )  # F4:ctrl_meas

    # wait for measurement done.
    while true
      sts = @bus.read( @address, 1, 0xf3 )
      break if sts.getbyte(0)[3] == 0
    end

    # read data
    data = @bus.read( @address, 6, 0xf7 ).bytes
    adc_P = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
    adc_T = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)

    # convert temperature
    var1 = (adc_T / 16384.0 - @dig_T1 / 1024.0) * @dig_T2
    var2 = (adc_T /131072.0 - @dig_T1 / 8192.0) * (adc_T / 131072.0 - @dig_T1 / 8192.0) * @dig_T3
    t_fine = var1 + var2
    t = (var1 + var2) / 5120.0

    # convert pressure
    var1 = t_fine / 2 - 64000.0
    var2 = var1 * var1 * @dig_P6 / 32768.0
    var2 = var2 + var1 * @dig_P5 * 2.0
    var2 = var2 / 4 + @dig_P4 * 65536.0
    var1 = (@dig_P3 * var1 * var1 / 524288.0 + @dig_P2 * var1) / 524288.0
    var1 = (1 + var1 / 32768.0) * @dig_P1

    if var1 == 0.0
      return { :pressure=>0.0, :temperature=>t }
    end

    prs = 1048576.0 - adc_P
    prs = (prs - var2 / 4096.0) * 6250.0 / var1
    var1 = @dig_P9 * prs * prs / 2147483648.0
    var2 = prs * @dig_P8 / 32768.0
    prs = prs + (var1 + var2 + @dig_P7 ) / 16.0

    return { :pressure=>prs, :temperature=>t }
  end


  # convert 2 bytes to a int16 value
  def to_int16( b1, b2 )
    return (b1 << 8 | b2) - ((b1 & 0x80) << 9)
  end

  # convert 2 bytes to a uint16 value
  def to_uint16( b1, b2 )
    return (b1 << 8 | b2)
  end
end

sensor = BMP280.new( I2C.new )
sensor.init
while true
  data = sensor.meas
  printf("%.3f hPa  %.2f degree\n", data[:pressure]/100, data[:temperature] )

  sleep 1
end
```

int64 版と 浮動小数点版の計算誤差を確認しましたが、観測した範囲では 5桁以上の確度で一致するようでしたので、どちらを使っても良さそうです。

# 結果

RaspberryPi 版の出力 24時間分のデータでグラフを描いてみた例です。
![grove_AirPressure_Graph1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/9c433c13-87dd-a07e-7168-c0f8c3bf0f0b.png)


# おわりに

今回は、気圧センサーを使ってみました。今回使ったセンサーは、データシートに変換のためのCプログラムが例示してありましたが、他社の気圧センサーでは変換式が書いてあるだけの事もあります。


MEMSの進歩により、こんなに小さなセンサーでもかなり分解能が高く、数メートルの上昇/下降が検出できるレベルです。しかし、こういった分解能付近で使おうとすると、以外とノイズに悩まされたりします。以前にセンサーメーカーが展示会で、高い分解能を誇るデモンストレーションを用意していたのに、恐らく空調の関係でデモが正しく動かないという残念な例を見たことがあります。今回のセンサーは、フィルターが内蔵されておりパラメータの調整もできるので、ある程度はセンサー単体で対処できるかもしれませんね。

次回は、温湿度センサーを試してみたいと思います。