ArduinoのWireライブラリを使ってBME280のデータを取得する

組み込みエンジニア

こんにちは、ENGかぴです。

Arduino環境ではI2C通信を行うために標準ライブラリとしてWireライブラリがあります。BME280の製作メーカが提供しているAPIを使用して温度・気圧・湿度の計算を行いI2C通信でセンサー情報を取得する方法をまとめました。

BME280用のライブラリがArudinoの有志によって公開されていますがBME280の製作メーカであるBOSCH社が提供しているAPIを組み込んで実装する方法を説明しています。

Arduino UNOを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

ArduinoでWire(I2C)を使用する

ArduinoのIDEの初期に搭載されているライブラリである「Wire.h」をインクルードして使用します。I2Cの送信と受信を中心に説明しています。BME280APIの実装の仕方については「BME280のダウンロードとAPIの実装方法」で説明しています。

ArduinoとBME280(I2C)の配線

ArduinoとBME280(I2C)の配線例
ArduinoとBME280(I2C)の配線例

ArduinoとBME280の配線例を示しています。BME280はDC3V系の電源で動作するモジュールであるためArduinoの5Vを直接印加することができません。そのためArduinoからBME280に向かう信号についてはレベル変換IC(FXMA2102)を使用しています。

プルアップ抵抗で電圧を調整したりトランジスタ(FET)でレベル変換しても良いのですが、レベル変換ICを使用した方が手間もかからず安全に変換できます。

BME280はArduinoのSCLのクロックの供給によってデータの返信をします。(クロック同期式という)クロックが供給されなければデータをArduinoに返信することができません。

I2C通信は基本的にマスタ(クロックを出す側)がスレーブ(クロックを受けて動作する側)に対してクロックを供給する関係になります。

センサーモジュールにクロック発振器がついている場合はクロックを発生させられるためマスタにもスレーブにもなることができますが、基本的にはArduinoがマスタでBME280などのセンサーモジュールがスレーブの関係になります。

ライブラリの使い方

#include <Wire.h>

void setup() {

    Wire.begin(); //begin()内にアドレスを入れるとスレーブになる
    //Wire.setClock(100000); //クロック周波数を設定する場合にセット初期は100kHz
}

I2Cを使用する場合は初めに「Wire.h」をインクルードします。初期化関数内でWire.begin()の引数の有無によってマスタで動作するかスレーブで動作するかが決まります。引数に値を入れるとその引数がスレーブのアドレスとして動作します。マスタとして使用するため引数は無しとします。

クロックは設定しなければ初期条件(ライブラリのソースコードを確認すると100kHzになっている)となります。引数として指定した値に近似した値がクロック周波数になりますが誤差が大きくなると通信エラーの原因になるため注意が必要です。

I2Cによる信号はプルアップする抵抗値にも影響するためクロックを早く設定しすぎると波形がなまってしまうこともあるので特に意識せず100kHzで十分だと思います。

I2Cマスタの送信と受信

Wireライブラリを使ったマスタ送信の手順は以下の通りです。

  1. スタート・コンディションを発行
  2. コントロール・バイトを送信(Writeフラグ)
  3. データを送信
  4. ストップ・コンディションを発行

1と2についてはbeginTransmission()を発行することで実現できます。コントロール・バイトは7ビットのスレーブアドレスを左詰めにして最下位ビットにWriteフラグ(0)をセットします。beginTransmisson()で設定されるため特に意識する必要はありません。

続けて3の任意のデータをwrite()もしくはsend()で送信し、4はendTransmission()を発行することで実現できます。

Wireライブラリを使ったマスタ受信の手順は以下の通りです。

  1. スタート・コンディションの発行
  2. コントロール・バイトを送信(Readフラグ)
  3. データを受信
  4. ストップ・コンディションを発行

1と2についてはrequestFrom()を発行することで実現できます。コントロール・バイトは送信時と同様ですが最下位ビットにReadフラグ(1)をセットします。requestFrom()で設定されるため特に意識する必要はありません。

続けて3の任意のデータをread()もしくはreceive()で受信します。4はrequestFrom()で指定したデータ数になるとストップ・コンディションを発行するためendTransmission()の発行は必要ありません。

I2Cによるデータの送信と受信の例

ArduinoのI2CをBME280APIに組み込むことでBME280とI2C通信を使ってデータの送信と受信ができるようになります。組み込む場合の例を示します。

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に通知するため戻り値が必要
}

マスタ送信の例を示しています。最初にスレーブが存在するか確認を行い、正常であればerrorが0になるため内部の処理を行います。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加してAPIに異常を通知します。

正常な場合はWire.beginTransmission(dev_id)でスレーブに対してスタート・コンディションを発行し、書き込む対象のアドレスをWire.write(reg_addr)でセットしています。続けてlenのサイズ分だけデータを書き込んでいます。

すべてのデータが書き終わったらWire.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;
}

マスタ受信の例を示しています。最初にスレーブが存在するか確認を行い、正常であれば受信の処理を開始します。

Wire.beginTransmission(dev_id)でスレーブに対してスタート・コンディションを発行し、書き込む対象のアドレスをWire.write(reg_addr)でセットしています。読み込み先のアドレスの指定が完了したので一旦ストップ・コンディションの発行を行います。APIの実装例にはアドレスの指定はWriteで行うことがコメントで示されています。

続けて読み込むデータをセットするためにWire.requestFrom(dev_id, len)を発行します。これを発行すると同時にスタート・コンディションの発行されlenサイズ分だけリードするとストップ・コンディションが発行されます。

自動でストップ・コンディションが発行されるためWire.endTransmission()の発行は必要ありません。

BME280のダウンロードとAPIの実装方法

BME280のデータシートを確認すると温度・湿度・気圧に関して計算式や補正値による考え方が記載されていますが、BOSCH社が提供しているAPIを使用することが推奨されています。

データシートに記載されている計算式でも問題ないのですが、提供されているAPIによるものと比較するとAPIによるソースのほうが作りこまれているためAPIを使用したほうが良いでしょう。以降ではBME280のドライバーをBME280APIと表記します。

BME280APIをBOSCH社のHPからダウンロード

BME280のドライバーのダウンロード
BME280のドライバーのダウンロード

BOSCH社のHPからBME280ドライバーをダウンロードします。下記リンクをクリックするとダウンロードのページに遷移します。

BoschSensortec-BME280_driver(Bosch社のBME280のAPI)

「Code」をクリックすると「Download ZIP」を選択するとファイルをまとめてダウンロードすることができます。ダウンロードが完了したら展開します。

BME280のAPIソフトをArduinoファイル内に配置する

BME280のドライバーファイルを組み込む
BME280のドライバーファイルを組み込む

Arduinoのプロジェクトファイル(inoファイル)があるフォルダにダウンロードした「BME280_driver-master」の中から以下の3つをコピーして追加します。

  1. bme280.c
  2. bme280.h
  3. bme280_defs.h

上記の例ではArduinoファイルのプロジェクト名をbme280-i2cに変更しています。

BME280APIの実装の準備

BME280APIは温湿度や気圧の計算を行う特殊な計算やフィルタに関しての設定が簡単にできるように構成されていますが、I2Cの送受信などは使用するマイコンに合わせて組み込む必要があります。プロジェクト最初にbme280のAPIをコールするためにbme280.hをインクルードします。

#include "bme280.h"

また計算結果をFloatを使わずにuint32_t形式で表示するために「bme280_defs.h」の以下をコメントアウトします。Floatで表示したい場合はコメントアウトの必要はありません。integerが32bit対応とする場合は#define BME280_32BIT_ENABLEを追加します。

#ifndef BME280_64BIT_ENABLE /* Check if 64-bit integer (using BME280_64BIT_ENABLE) is enabled */
#ifndef BME280_32BIT_ENABLE /* Check if 32-bit integer (using BME280_32BIT_ENABLE) is enabled */
#define BME280_32BIT_ENABLE //integerが32bit対応なら追加
#ifndef BME280_FLOAT_ENABLE /* If any of the integer data types not enabled then enable BME280_FLOAT_ENABLE */
//#define BME280_FLOAT_ENABLE
#endif
#endif
#endif

BME280APIに使用する変数とユーザーが準備する関数の実装

BME280APIを使用するためには2つの変数を準備する必要があります。

bme280_dev bme280main;
bme280_data sensor_data;

bme280_devはAPIを使用するための情報を格納する変数です。この変数に必要な情報を記述することでAPIとリンクすることができます。bme280_dataはAPIが計算した温度、湿度、気圧に関するデータを格納する変数です。

ユーザーが準備する関数についてはダウンロードしたファイルの中になる「README.md」をテキストで開くと内容が確認できます。ポイントとなる関数は以下の通りになります。

  1. bme280_init(&dev)
  2. int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
  3. int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev)
  4. void user_delay_ms(uint32_t period)
  5. int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
  6. int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)

1はAPIとしてコールすると使用できますが、コールする前にユーザー側でIDやread/writeに使用する関数のアドレスや遅延用の関数を登録しておく必要があります。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()をコメントアウトしておいた方が良いでしょう。

void user_delay_ms(uint32_t period)
{
    delay(period);
}

4の関数はmsのウェイトを持たせるために引数が指定されているのでdelay()関数を追加しています。

5と6の関数についてはセンサーのデータを通信する際に使用するものですが、引数が指定されているの引数を利用しながら組み込みAPIのステータスがリターンするように組み込む必要があります。実装例はI2Cによるデータの送信と受信で示しています。

動作確認

ArduinoとBME280(I2C)の動作確認
ArduinoとBME280(I2C)の動作確認

ArduinoのシリアルモニターにBME280から取得したデータを換算した値を表示しています。

温度(Temperature)は部屋に置いている温度計によると25.5℃であり、気圧(Pressure)はスマホを見ると1009hPa=1009mBarで湿度(Humidity)についてはスマホのデータでは43%でした。少し誤差がある気がしますが、室内のデータであることも考慮するとそれなりに計測できていると思います。

ArduinoのI2Cを使用してセンサーの情報が取得できるようになることで他のI2C通信が可能なセンサーのデータも取得できるため応用範囲が広がりそうです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include <Wire.h>
#include <MsTimer2.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;

/*** 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 TimerCnt();

void setup() {
    Wire.begin();
    //Wire.setClock(100000); //初期で100kHz
    MsTimer2::set(1,TimerCnt); //1msごとに関数へ遷移
    MsTimer2::start();
    Serial.begin(115200);
    Bme280Init();
    timsensor = GET_SENSOR_MAX;
}

void loop() {

    if( cnt10ms >= BASE_CNT ){ //10msごとにここに遷移する
        cnt10ms -=BASE_CNT;
 
        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/100);
            Serial.print("[℃]");
            Serial.print("  Pressure:");
            Serial.print((double)sensor_data.pressure/10000);
            Serial.print("[hPa]");
            Serial.print("  Humidity:");
            Serial.print((double)sensor_data.humidity/1024);
            Serial.println("[%]");
        }
    }
}
/* callback function add */
void TimerCnt(){
    ++cnt10ms;
}
/* 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;
        }
        //Wire.endTransmission();
    }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;

    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;
}

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

TECH::CAMPプログラミング教養【無料体験会】

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました