しまねソフト研究開発センター(略称 ITOC)にいます、東です。
Grove Beginner Kit for Arduino を使ってみる記事の第4回、今回から I2C 接続のセンサーを題材にします。
このキットに付属している I2C 接続のセンサーは、以下の3種類です。
- 3軸加速度センサー
- 気圧センサー
- 温湿度センサー
その中でも今回は、3軸加速度センサーを題材にします。
このレポートでは、
「メーカーのデータシートを見て、センサICを直接コントロールをすること」
を、方針とします。
もちろん、プログラムを書く上で既存のライブラリ等の実装を参考にすることは良い事です。しかしながらこのレポートは、I2C バスやセンサ IC の扱い方、データシートの読み方を示す事も目的としているため、できるだけ低レイヤーでの説明を行います。
ターゲットは、以下の通り。
- Arduino - 付属の Arduino Uno 互換機
- Ruby - Raspberry Pi + Grove Base HAT for Raspberry Pi
- mruby/c - RBoard
3軸加速度センサー (3-Axis Digital Accelerometer)
外観 | 回路図 |
---|---|
(センサーがGrove端子に接続されてるだけなので省略) |
(メーカーWiki: Grove - 3-Axis Digital Accelerometer (LIS3DHTR) より引用)
搭載されているセンサーICは、STマイクロエレクトロニクスの LIS3DHTR です。
データシートの確認
ICメーカー製品ページ で、データシートと必要に応じてアプリケーションノートなどを確認します。
プログラム作成に必要な情報は以下ぐらいでしょうか。
- X,Y,Z の3軸方向の加速度センサー
- フルスケールは、重力加速度を単位として ±2, 4, 8, 16g に切り替え可能
- 測定値は、10bit を標準とし、8bit や 12bit に切り替え可能。よって、分解能は ±2g,12bit モードの、約 1mg
- 出力は、いずれも 16bit 左詰で、2の補数表現で出力される
- FIFO を持っている
- I2Cアドレスは 0x18 もしくは 0x19 で、内部レジスタを 1バイトで指定する
- レジスタの連続アクセスモードを持っており、その場合は MSB を 1 にして内部レジスタを指定する
- 起動時はパワーダウンモードなので、動作モードの変更が必要
また、SPI接続などもできますが、この Grove モジュールは、I2C接続のみで作られており、アドレスは 0x19 です。
レジスタマップとその機能を確認して、サンプルプログラムの作成は、以下の方針で行います。
- X,Y,Z 方向それぞれの値を、約0.5秒おきに画面表示する
- フルスケール ±2g、12bit モードにする
- FIFOを使わず、プログラムのタイミングでデータを読み込む
任意のタイミングで値を読み込む場合は、Block data update (BDU) の continuous update を外すことが強く推奨されると書いてあるので、それに従います。
ICの初期化
CTRL_REG1
データシートより、コントロールレジスタ CTRL_REG1(20h) の ODRビットでパワーダウンを解除するとともに、測定周波数を設定することが分かります。その他はデフォルト値でよさそうなので、ここでは 100Hzの測定モードにすることにして、0b0101_0111 を指定すれば良いことになります。
CTRL_REG4
同様に、コントロールレジスタ CTRL_REG4(23h) に関し、以下の設定にすることとして 0b1000_1000 を設定します。
- BDU ビットを 1 にして、continuous update モードを外す
- FS ビットを、00 にして、±2g モードにする(これはデフォルト値)
- HR ビットを 1 にして、ハイレゾモード (12bit) にする
データの読み込み
測定が終わり測定値の出力準備が終わったら、ステータスレジスタ (STATUS_REG) の、*DA (data available) ビットが 1になると書いてあります。
今回は XYZ 3軸のデータを取得するので、ZYXDA ビットが 1 になったら、OUT_* レジスタを読んで値を取得します。
サンプルプログラム
Arduino
センサーを、I2C
端子に接続します。
#include <Wire.h>
const int I2C_ADRS = 0x19;
void setup() {
Wire.begin();
Serial.begin(9600);
// IC初期化
Wire.beginTransmission( I2C_ADRS );
Wire.write( 0x20 ); // CTRL_REG1
Wire.write( 0x57 ); // = 0101_0111
Wire.endTransmission();
Wire.beginTransmission( I2C_ADRS );
Wire.write( 0x23 ); // CTRL_REG4
Wire.write( 0x88 ); // = 1000_1000
Wire.endTransmission();
}
void loop() {
int sts;
byte data[6];
// STATUS_REG:XYXDA ビットが 1 になるのを待つ
do {
Wire.beginTransmission( I2C_ADRS );
Wire.write( 0x27 ); // STATUS_REG
Wire.endTransmission( false );
Wire.requestFrom( I2C_ADRS, 1 );
sts = Wire.read();
} while( bitRead( sts, 3) == 0 );
// データを読み込む
Wire.beginTransmission( I2C_ADRS );
Wire.write( 0x28 | 0x80 ); // OUT_X_L .. OUT_Z_H
Wire.endTransmission( false );
Wire.requestFrom( I2C_ADRS, 6 );
for( int i = 0; i < 6; i++ ) {
data[i] = Wire.read();
}
// 重力加速度に換算する
double fs_div = 0x4000; // 2g=0x4000, 4g=0x2000...
double x = (int16_t)((data[1] << 8) | data[0]) / fs_div;
double y = (int16_t)((data[3] << 8) | data[2]) / fs_div;
double z = (int16_t)((data[5] << 8) | data[4]) / fs_div;
// 表示
Serial.print("X:");
Serial.print(x, 3);
Serial.print(" Y:");
Serial.print(y, 3);
Serial.print(" Z:");
Serial.println(z, 3);
delay( 500 );
}
Arduino は、C++ ベース(だと聞いている)ので、class を使おうと思いましたが、Arduino 公式 Language Reference の範囲内で書くべきかと思いなおし、クラスは使いませんでした。
センサーからの読み込み値から、測定値 (重力加速度 g) に計算するため、定数 0x4000 で割り算をしています。これは、signed 16bit のフルスケールを 2g とみなすため、FS(0x8000) / 2 = 0x4000(=16384) を使っています。
ところが、Seeed 社の作ったライブラリを見ると 16000 で割り算をしており、0x4000(=16384) とは多少違いがあります。何故なのかデータシートを見ても分かりませんでした。それどころか、アプリケーションノート(AN3308) では、(換算が必要ですが) 16000、16384 両方の数値が使われていて、どちらが正しいのか分かりませんでした。
Raspberry Pi (CRuby)
センサーを、I2C
端子に接続します。
require "mruby/i2c"
class LIS3DHTR
I2C_ADRS = 0x19
#
# init instance
#
def initialize( i2c_bus, address = I2C_ADRS )
@bus = i2c_bus
@address = address
end
#
# init sensor
#
def init()
# CTRL_REG1(20): Normal mode 100Hz, enable XYZ-axis
@bus.write( @address, 0x20, 0b0101_0111 )
# CTRL_REG4(23): Block data update disabled, fs=2g, HighResolution
@bus.write( @address, 0x23, 0b1000_1000 )
end
#
# meas
#
def meas()
while true
data = @bus.read( @address, 1, 0x27 ) # STATUS_REG(27)
break if data.getbyte(0)[3] == 1 # check ZYXDA bit enable
end
# read OUT_X_L(28) - OUT_Z_H(2D) continuously
data = @bus.read( @address, 6, 0x28 | 0x80 ).bytes
fs_div = 0x4000 # 2g=0x4000, 4g=0x2000...
# 16000, 7282, 3968, 1280 ?
x = to_int16( data[1], data[0] ).to_f / fs_div
y = to_int16( data[3], data[2] ).to_f / fs_div
z = to_int16( data[5], data[4] ).to_f / fs_div
return {:x=>x, :y=>y, :z=>z}
end
#
# convert 2 byte to int16 value
#
def to_int16( b1, b2 )
return (b1 << 8 | b2) - ((b1 & 0x80) << 9)
end
end
sensor = LIS3DHTR.new( I2C.new )
sensor.init
while true
data = sensor.meas
printf("X:%5.3f Y:%5.3f Z:%5.3f\n", data[:x], data[:y], data[:z] )
sleep 0.5
end
こちらはクラスを使います。init メソッドで初期化を、meas メソッドで測定値の取得を行います。0b
を使って2進数が直接記述でき、しかも _
で区切りを入れられて見やすくできるので、便利ですね。
RBoard (mruby/c)
RaspberryPiと同じプログラムから、require をコメントアウトしたプログラムがそのまま動きます。
おわりに
当レポート執筆のための調査中に、MEMS加速度センサーの性能比較を行っている方の記事を発見しました。良記事です。
地震観測を目的とした加速度センサ5種の性能比較
今回は、Grove モジュールのコントロールのみに焦点をあてた記事で、測定周期も規定せず、とりあえず使ってみるという事を重視したものです。実用的な用途では、測定周期の指定やデータ抜けを防ぐための FIFO の利用などが必要になると思います。
データシートを読んでその機能と使い方が理解できるようになっていれば、既存のライブラリを使うだけではわからなかったセンサICの機能、性能を100%生かせますから、もっとたくさんの事ができるようになると思いますし、トラブルシュート時も、より早く原因にたどりつけることと思います。
次回は、同じく I2C を使った気圧センサーを試してみたいと思います。