こんにちは、ENGかぴです。
Arduino環境ではI2C通信を行うために標準ライブラリとしてWireライブラリがあります。BME280の製作メーカが提供しているAPIを使用して温度・気圧・湿度の計算を行いI2C通信でセンサー情報を取得する方法をまとめました。
BME280用のライブラリが有志によって公開されていますがWireライブラリの使用方法や組み込み方法を説明するため、BME280の製作メーカであるBOSCH社が提供しているAPIを組み込んで実装しています。
BME280 温湿度・気圧センサモジュール(I2C/SPI タイプ)(ストロベリー・リナックス)を使用しています。Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
BME280のダウンロードとAPIの実装方法
BME280のデータシートを確認すると温度・湿度・気圧に関して計算式や補正値による考え方が記載されていますが、BOSCH社が提供しているAPIを使用することが推奨されています。
データシートに記載されている計算式でも問題ないのですが、提供されているAPIによるものと比較するとAPIによるソースのほうが作りこまれているためAPIを使用したほうが良いでしょう。以降ではBME280のドライバーをBME280APIと表記します。
BME280APIをBOSCH社のHPからダウンロード
BOSCH社のHPからBME280ドライバーをダウンロードします。下記リンクをクリックするとダウンロードのページに遷移します。
BoschSensortec-BME280_driver(Bosch社のBME280のAPI)
「Code」をクリックすると「Download ZIP」を選択するとファイルをまとめてダウンロードすることができます。ダウンロードが完了したら展開します。
BME280のAPIソフトをArduinoファイル内に配置する
Arduino IDEのプロジェクトファイル表示欄横の▼マークから新規タブを押すと新規ファイル追加することができますが、ここではプロジェクトのフォルダ内にファイルを直接追加する方法を説明しています。
Arduinoのプロジェクトファイル(inoファイル)があるフォルダにダウンロードした「BME280_driver-master」の中から以下の3つをコピーして追加します。
- bme280.c
- bme280.h
- bme280_defs.h
上記の例ではArduinoファイルのプロジェクト名をbme280-i2cに変更しています。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
BME280APIの実装の準備
BME280APIは温湿度や気圧の計算を行う特殊な計算やフィルタに関しての設定が簡単にできるように構成されていますが、Wire(I2C)の送受信の処理はマイコンに合わせて組み込む必要があります。プロジェクト最初にbme280のAPIをコールするためにbme280.hをインクルードします。
#include "bme280.h"
BME280APIに使用する変数とユーザーが準備する関数の実装
BME280APIを使用するためには2つの変数を準備する必要があります。
bme280_dev bme280main;
bme280_data sensor_data;
bme280_devはAPIを使用するための情報を格納する変数です。この変数に必要な情報を記述することでAPIとリンクすることができます。bme280_dataはAPIが計算した温度、湿度、気圧に関するデータを格納する変数です。
ユーザーが準備する関数についてはダウンロードしたファイルの中になる「README.md」をテキストで開くと内容が確認できます。ポイントとなる関数は以下の通りになります。
- bme280_init(&dev)
- int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
- int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev)
- void user_delay_ms(uint32_t period)
- int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
- int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
1はBME280APIの初期化に使用しますが、コールする前にデバイスID(スレーブアドレス)、read/writeに使用する関数のアドレス、遅延用の関数を登録する必要があります。またI2C通信を使用するためintfにBME280_I2C_INTFをセットします。Bme280Init()という関数を自作してその中で変数の登録とbme280_init()をコールしています。
void Bme280Init(){
bme280main.dev_id = BME280_I2C_ADDR_PRIM;
bme280main.intf = bme280_intf::BME280_I2C_INTF;
bme280main.read = user_i2c_read;
bme280main.write = user_i2c_write;
bme280main.delay_ms = user_delay_ms;
bme280_init(&bme280main);
stream_sensor_data_forced_mode(&bme280main);
}
2は1回センサーの情報を取得するとスリープする機能です。3は繰り返してセンサーの値を計算する機能です。本記事では消費電力を抑えるため2のforce modeを実装して使用します。2と4の関数の実装例は以下の通りです。
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev){
int8_t rslt;
uint8_t settings_sel;
uint32_t req_delay;
dev->settings.osr_h = BME280_OVERSAMPLING_1X;
dev->settings.osr_p = BME280_OVERSAMPLING_16X;
dev->settings.osr_t = BME280_OVERSAMPLING_2X;
dev->settings.filter = BME280_FILTER_COEFF_16;
settings_sel = BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL;
rslt = bme280_set_sensor_settings(settings_sel, dev);
req_delay = bme280_cal_meas_delay(&dev->settings);
//while (1) {
rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, dev);
dev->delay_ms(req_delay);
rslt = bme280_get_sensor_data(BME280_ALL, &sensor_data, dev);
//}
return rslt;
}
基本的に「READ.md」の内容はそのままでよいのですが、不要な部分をコメントアウトしている部分があります。while(1)をコメントアウトしないとループから抜け出せなくなります。3を使用する際も同様にwhile(1)をコメントアウトしておいた方が良いでしょう。
void user_delay_ms(uint32_t period){
delay(period);
}
4の関数はmsのウェイトを持たせるために引数が指定されているのでdelay()関数を追加しています。
5と6の関数についてはデータの読み書きに使用しますが、準備されている引数を利用しながら組み込む必要があります。また戻り値にAPIのステータスがリターンするように組み込む必要があります。実装例はWire(I2C)の送信と実装例とWire(I2C)の受信の手順と実装例で示しています。
ArduinoでWire(I2C)を使用する
Arduinoの標準ライブラリであるWireライブラリの使い方を説明します。Wire(I2C)の送信と受信を中心に説明しています。BME280APIの実装の仕方については「BME280のダウンロードとAPIの実装方法」で説明しています。
ArduinoとBME280(I2C)の配線
ArduinoとBME280モジュールの配線例を示しています。BME280はDC3V系の電源で動作するモジュールであるためArduinoの5Vを直接印加することができません。そのためArduinoからBME280モジュールに向かう信号についてはレベル変換IC(FXMA2102)を使用しています。
Wire(I2C)はプルアップ抵抗が必要です。ArduinoとFXMA2102間とFXMA2102とBME280モジュールの間それぞれにプルアップ抵抗を実装します。プルアップ抵抗が大きすぎると配線長によってはクロックが鈍ってしまい通信できないことがあるので注意が必要です。
BME280モジュールはArduinoのSCLのクロックの供給によってデータの返信をします。(クロック同期式という)クロックが供給されなければデータをArduinoに返信することができません。
I2C通信は基本的にマスタ(クロックを出す側)がスレーブ(クロックを受けて動作する側)に対してクロックを供給する関係になります。
センサーモジュールにクロック発振器がついている場合はクロックを発生させられるためマスタにもスレーブにもなることができますが、基本的にはArduinoがマスタでBME280モジュールなどのセンサーモジュールがスレーブの関係になります。
Wireライブラリの初期化
#include <Wire.h>
void setup() {
Wire.begin(); //begin()内にアドレスを入れるとスレーブになる
//Wire.setClock(100000); //クロック周波数を設定する場合にセット初期は100kHz
}
Wire(I2C)を使用する場合は初めに「Wire.h」をインクルードします。begin()関数の引数の有無によってマスタで動作するかスレーブで動作するかが決まります。引数に指定した値でスレーブ動作を開始します。マスタとして使用するため引数は無しとします。
クロックは設定しなければ初期条件(ライブラリのソースコードを確認すると100kHzになっている)となります。引数として指定した値に近似した値がクロック周波数になりますが誤差が大きくなると通信エラーの原因になるため注意が必要です。
I2Cによる信号はプルアップ抵抗にも影響するためクロックを早く設定しすぎると波形がなまってしまうこともあるので特に意識せず100kHzで十分だと思います。
Wire(I2C)の送信と実装例
Wireライブラリを使った送信の手順は以下の通りです。
- beginTransmission()関数で初期化とスレーブアドレスをセット
- write()関数で書き込み対象のアドレスをセット
- write()関数で書き込むデータをセット
- endTransmission()関数で送信
1~3まではデータの準備です。4のendTransmission()関数はスタート・コンディションの発行やコントロール・バイト(7ビットのスレーブアドレスを左詰めにして最下位ビットにWriteフラグ(0)をセット)の処理を行いデータを送信した(書き込んだ)後ストップ・コンディションの発行を行います。以下に実装例を示します。
int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット
for( uint16_t i=0; i < len; i++ ){
Wire.write(*reg_data); //lenサイズ分だけデータを書き込む
++reg_data;
}
Wire.endTransmission(); //ストップ・コンディションの発行
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt; //APIに通知するため戻り値が必要
}
BME280APIが宣言している関数であるuser_i2c_write()にWireを組み込みます。最初にbeginTransmission()関数で初期化とスレーブアドレスをセットします。
endTransmission()関数でアドレスを送信しスレーブが存在するか確認を行います。スレーブが存在する場合は戻り値が0になるためデータの書き込み処理に移ります。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加してAPIに異常を通知します。
beginTransmission()関数で初期化とスレーブアドレスをセットします。次に書き込む対象のレジスタのアドレスをwrite()関数でセットします。続けてlenのサイズに応じてwrite()関数でデータをセットします。
最後にendTransmission()関数でスタート・コンディションからストップ・コンディションまでを含めたデータを送信します。
Wire(I2C)の受信の手順と実装例
Wireライブラリを使った受信の手順は以下の通りです。
- beginTransmission()関数で初期化とスレーブアドレスをセット
- write()関数で書き込み対象のアドレスをセット
- endTransmission()関数で送信
- requestFrom()関数で対象のアドレスのデータを取得する
- read()関数で取得したデータを読み込む
1~3まではスレーブアドレスに対して書き込み対象のアドレスを指定します。4のrequestFrom()関数はスタート・コンディションの発行やコントロール・バイト(7ビットのスレーブアドレスを左詰めにして最下位ビットにReadフラグ(1)をセット)の処理を行い指定した数のデータを取得し、ストップ・コンディションの発行を行います。
5は4で取得したデータを読み込みます。送信と異なる点はrequestFrom()でストップ・コンディションを発行するためendTransmission()の発行は必要ありません。以下に実装例を示します。
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
Wire.requestFrom(dev_id, len); //受信開始(スタート(ストップ)・コンディションの発行)
for( uint16_t i=0; i < len; i++ ){
*reg_data = Wire.read(); //len分だけデータをリードする
++reg_data;
}
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
BME280APIが宣言している関数であるuser_i2c_read()にWireを組み込みます。最初にbeginTransmission()関数で初期化とスレーブアドレスをセットします。
endTransmission()関数でアドレスを送信しスレーブが存在するか確認を行います。スレーブが存在する場合は戻り値が0になるためデータの書き込み処理に移ります。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加してAPIに異常を通知します。
beginTransmission()関数で初期化とスレーブアドレスをセットします。次に書き込む対象のレジスタのアドレスをwrite()関数でセットします。endTransmission()関数でスレーブのレジスタを選択した状態にします。
requestFrom()関数でスレーブから指定した数のデータを取得します。read()関数で取得したデータを読み込みます。
BME280ライブラリを追加して使用する(参考)
BME280APIを組み込んで動作させる方法を説明しましたが、Arduino IDEでBME280ライブラリを追加する方法もあります。BME280ライブラリの中でAdafruit BME280ライブラリを使用する方法を参考として説明します。
Arduino IDEのライブラリマネージャの検索欄にbme280を入力するとBME280ライブラリの候補が表示されます。候補の中からAdafruit BME280 Libraryをインストールします。
Adafruit BME280 Libraryをインストールする際に他の追加ライブラリをインストールするか選択するメッセージが表示されることがあります。
Adafruit BME280 Libraryは他のライブラリと関連性があるため単体では使用できないことがあるためインストールを勧められています。Install allを選択してその他のライブラリも追加することをお勧めします。
#include <Adafruit_BME280.h>
Adafruit_BME280 bme280;
float bme280data[3];
void setup() {
Serial.begin(115200);
bme280.begin(0x76);
}
void loop() {
Bme280Get();
delay(1000);
}
/* BME280のデータ取得 */
void Bme280Get(void){
Serial.print("Temperature = ");
bme280data[0] = bme280.readTemperature();
Serial.print(bme280data[0]);
Serial.println(" °C");
Serial.print("Pressure = ");
bme280data[1] = bme280.readPressure() / 100.0F;
Serial.print(bme280data[1]);
Serial.println(" hPa");
Serial.print("Humidity = ");
bme280data[2] = bme280.readHumidity();
Serial.print(bme280data[2]);
Serial.println(" %");
Serial.println();
}
Adafruit BME280 Libraryをインクルードします。Adafruit_BME280クラスの変数を宣言してインスタンス化します。例ではbme280でインスタンス化しています。
begin()関数でライブラリの初期化を行います。BME280をI2Cで使用する場合は引数にスレーブを指定します。引数にアドレスを指定しない場合はデフォルト値である0x77がスレーブになります。例では0x76を指定しています。
温度情報の取得はreadTemperature()関数、気圧情報の取得はreadPressure()関数、湿度情報の取得はreadHumidity()関数を使用します。
動作確認
ArduinoのシリアルモニターにBME280モジュールから取得したデータを換算した値を表示しています。
温度(Temperature)は部屋に置いている温度計によると25.5℃でした。気圧(Pressure)と湿度(Humidity)はスマホ(Android)で確認すると動作確認の時点で気圧が1009hPa=1009mBar、湿度(Humidity)が43%でした。
少し誤差がある気がしますが、室内のデータであることも考慮するとそれなりに計測できていると思います。
ArduinoのI2Cを使用してセンサーの情報が取得できるようになることで他のI2C通信が可能なセンサーのデータも取得できるため応用範囲が広がりそうです。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <Wire.h>
#include "bme280.h"
#define FORCED_MODE //省エネモードを使用するとき定義する
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define GET_SENSOR_MAX 500
//application use
bme280_dev bme280main;
bme280_data sensor_data;
bool bmeinitflg = false;
int8_t cnt10ms;
int16_t timsensor = TIME_OFF;
uint32_t beforetimCnt = millis();
/*** Local function prototypes */
void Bme280Init();
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
void user_delay_ms(uint32_t period);
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev);
int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev);
void setup() {
Wire.begin();
//Wire.setClock(100000);
Serial.begin(115200);
Bme280Init();
timsensor = GET_SENSOR_MAX;
}
void loop() {
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timsensor > TIME_UP ){
timsensor--;
}
if( timsensor == TIME_UP ){
timsensor = GET_SENSOR_MAX;
#ifdef FORCED_MODE
stream_sensor_data_forced_mode(&bme280main);
#else
//stream_sensor_data_normal_mode(&bme280main);
#endif
Serial.print("Temperature:");
Serial.print((double)sensor_data.temperature);
Serial.print("[℃]");
Serial.print(" Pressure:");
Serial.print((double)sensor_data.pressure/100);
Serial.print("[hPa]");
Serial.print(" Humidity:");
Serial.print((double)sensor_data.humidity);
Serial.println("[%]");
}
}
}
/* Bme280 api use function add */
int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット
for( uint16_t i=0; i < len; i++ ){
Wire.write(*reg_data); //lenサイズ分だけデータを書き込む
++reg_data;
}
Wire.endTransmission(); //ストップ・コンディションの発行
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
/* Bme280 api use function add */
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
Wire.requestFrom(dev_id, len); //受信開始(スタート(ストップ)・コンディションの発行)
for( uint16_t i=0; i < len; i++ ){
*reg_data = Wire.read(); //len分だけデータをリードする
++reg_data;
}
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
/* Bme280 sensor iniialize */
void Bme280Init(){
bme280main.dev_id = BME280_I2C_ADDR_PRIM;
bme280main.intf = bme280_intf::BME280_I2C_INTF;
bme280main.read = user_i2c_read;
bme280main.write = user_i2c_write;
bme280main.delay_ms = user_delay_ms;
Wire.beginTransmission(bme280main.dev_id);
byte error = Wire.endTransmission();
if( error == 0){
bme280_init(&bme280main);
#ifdef FORCED_MODE
stream_sensor_data_forced_mode(&bme280main);
#else
//stream_sensor_data_normal_mode(&bme280main);
#endif
}
}
/* Bme280 api use function add */
void user_delay_ms(uint32_t period){
delay(period);
}
/* Bme280 api use function add */
int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev){
int8_t rslt;
uint8_t settings_sel;
uint32_t req_delay;
dev->settings.osr_h = BME280_OVERSAMPLING_1X;
dev->settings.osr_p = BME280_OVERSAMPLING_16X;
dev->settings.osr_t = BME280_OVERSAMPLING_2X;
dev->settings.filter = BME280_FILTER_COEFF_16;
settings_sel = BME280_OSR_PRESS_SEL | BME280_OSR_TEMP_SEL | BME280_OSR_HUM_SEL | BME280_FILTER_SEL;
rslt = bme280_set_sensor_settings(settings_sel, dev);
req_delay = bme280_cal_meas_delay(&dev->settings);
//while (1) {
rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, dev);
dev->delay_ms(req_delay);
rslt = bme280_get_sensor_data(BME280_ALL, &sensor_data, dev);
//}
return rslt;
}
/* Bme280 api use function add */
int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev){
int8_t rslt;
uint8_t settings_sel;
struct bme280_data comp_data;
dev->settings.osr_h = BME280_OVERSAMPLING_1X;
dev->settings.osr_p = BME280_OVERSAMPLING_16X;
dev->settings.osr_t = BME280_OVERSAMPLING_2X;
dev->settings.filter = BME280_FILTER_COEFF_16;
dev->settings.standby_time = BME280_STANDBY_TIME_62_5_MS;
settings_sel = BME280_OSR_PRESS_SEL;
settings_sel |= BME280_OSR_TEMP_SEL;
settings_sel |= BME280_OSR_HUM_SEL;
settings_sel |= BME280_STANDBY_SEL;
settings_sel |= BME280_FILTER_SEL;
rslt = bme280_set_sensor_settings(settings_sel, dev);
rslt = bme280_set_sensor_mode(BME280_NORMAL_MODE, dev);
//while (1) {
dev->delay_ms(70);
rslt = bme280_get_sensor_data(BME280_ALL, &sensor_data, dev);
//}
return rslt;
}
BME280のダウンロードとAPIの実装方法で説明しているBME280APIをArduinoのプロジェクトファイルに追加することで動作します。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。