しまねソフト研究開発センター(略称 ITOC)にいます、東です。
Grove Beginner Kit for Arduino を使ってみる記事の第5回、今回は I2C 接続の気圧センサーを題材にします。
このレポートでは、
「メーカーのデータシートを見て、センサICを直接コントロールをすること」
を、方針とします。
もちろん、プログラムを書く上で既存のライブラリ等の実装を参考にすることは良い事です。しかしながらこのレポートは、I2C バスやセンサ IC の扱い方、データシートの読み方を示す事も目的としているため、できるだけ低レイヤーでの説明を行います。
ターゲットは、以下の通り。
- Arduino - 付属の Arduino Uno 互換機
- Ruby - Raspberry Pi + Grove Base HAT for Raspberry Pi
- mruby/c - RBoard
気圧センサー (Barometer Sensor (BMP280))
外観 | 回路図 |
---|---|
(センサーがGrove端子に接続されてるだけなので省略) |
(メーカーWiki: Grove - Barometer Sensor (BMP280) より引用)
搭載されているセンサーICは、ボッシュの BMP280 です。
このキットに付属しているモジュールは、メーカーページに掲載されているモジュールとは、サイズが違いますが、回路は同じようです。
データシートの確認
ICメーカー製品ページ で、データシートを確認します。
この IC は、気圧計の後段にデジタルフィルターが入っており、フィルター定数もユースケースごとに最適値が公開されていて、とても親切ですね。
データシートで、プログラム作成に必要な情報は以下ぐらいでしょうか。
* 電源投入時は停止モードなので、連続測定(Normalと呼称)か、1回測定(Forcedと呼称)モードへ遷移させる必要がある
* その際に、各種フィルタ定数も設定する
* 気圧とともに気温も取得できる
* 各データは 20bit長であり、圧力単位(Pa), 温度(Degree) への変換は、提供されるライブラリを使うかデータシートに記載のサンプルコードによって行える
* I2Cアドレスは、0x77 (このGroveモジュールの仕様)
サンプルプログラムの作成は、以下の方針で行います。
- 気圧とともに温度も取得する
- 1回測定(Forced)モードを使い、プログラム側のタイミングでデータを取得する
- Use case "Indoor navigation" で示される、最高解像度で作ってみる
ICの初期化
データシートのレジスタマップとその詳細説明より、config
, ctrl_meas
2つのレジスタを設定するだけで良いことが分かります。
config(F5) レジスタ
今回、config
レジスタで設定すべきは、filter ビットです。このパラメータは、Filter selection 表の IIR filter coeff
にあたります。しかしこの 3bit をどう設定すれば表に記載された OFF, 4, 16 に設定できるのか明記してありません。今回はメーカーがライブラリを公開しているので、そちらを参照すると以下の通りでした。
/*! @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) レジスタ
ctrl_meas
レジスタは、同じくフィルタ定数と併せて測定モードの指示ビットを持っています。こちらは別表にきちんと設定値とビットパターンの表がありますので迷うことなく設定値が分かります。
また、測定開始の指示をするビットも持っているため、フィルタ定数ビットはプログラム側の変数に保存しておいた方がよさそうです。
トリミングデータ
このセンサICでは、測定データの row 値から、実際の値 (Pa, degree) へ換算するために、IC固有のトリミングデータを必要とします。この値は NVRAM に書かれており、ということは測定中に変わることはないので、初期化時にあらかじめ読んでおきます。
データの読み込み
測定中/測定完了の状態は、status
レジスタの measuring
ビットに反映されます。
測定開始トリガーをかけた後、このビットが 0 になってから測定データが反映される press_*
, temp_*
レジスタを読み込みます。
その後、 raw値から実際の値に変換する処理が必要ですが、このデータシートでは、数式ではなくC言語によるコードで、
- 固定小数点で、64bit int が使える場合
- 固定小数点で、32bit int のみの場合
- 浮動小数点を使う場合
の、3種類で例示してあります。
サンプルプログラム
Arduino
センサーを、I2C
端子に接続します。
私が今使っている開発環境「ArduinoIDE 2.3.2」では、uint64_t
と書いても、コンパイルエラーにはならないですが、公式ドキュメントでは 32bit までしか記載が無いので、変換処理は、32bit 固定小数点版を使う事にします。
#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では不要なキャストなどを消して、適切に成形したコードになります。
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 の標準ファームウェアではこれは定義されていないので、浮動小数点版を使ってみました。
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時間分のデータでグラフを描いてみた例です。
おわりに
今回は、気圧センサーを使ってみました。今回使ったセンサーは、データシートに変換のためのCプログラムが例示してありましたが、他社の気圧センサーでは変換式が書いてあるだけの事もあります。
MEMSの進歩により、こんなに小さなセンサーでもかなり分解能が高く、数メートルの上昇/下降が検出できるレベルです。しかし、こういった分解能付近で使おうとすると、以外とノイズに悩まされたりします。以前にセンサーメーカーが展示会で、高い分解能を誇るデモンストレーションを用意していたのに、恐らく空調の関係でデモが正しく動かないという残念な例を見たことがあります。今回のセンサーは、フィルターが内蔵されておりパラメータの調整もできるので、ある程度はセンサー単体で対処できるかもしれませんね。
次回は、温湿度センサーを試してみたいと思います。