Seeeduino XIAOのWireを使ってBME280のデータを取得

組み込みエンジニア

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

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

本記事はSeeeduino XIAOでWireライブラリを使用してBME280からI2C通信を行いデータ取得する方法についてまとめています。下記リンクの内容を引用しながらSeeeduino XIAOで動作確認をしています。

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

Seeeduino XIAOで動作確認したことについてリンクをまとめています。

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

Seeeduino XIAOでWire(I2C)を使用する

Arduinoの標準ライブラリである「Wire.h」をインクルードして使用します。Wireライブラリを使用したI2Cの送信と受信の方法を中心に説明しています。

Wire(I2C)の考え方

Seeeduino XIAOのWireを使用する場合は6ピン(DIO5)がSCL、5ピン(DIO4)をI2C通信ポートとして使用します。これらのピンはプルアップする必要がありますが、Wireライブラリの標準速度は100kbpsであるため10kΩでプルアップしていれば十分です。

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

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

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

Wireライブラリのセットアップ

#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()で送信し、4はendTransmission()を発行することで実現できます。Wireライブラリを使ったマスタ受信の手順は以下の通りです。

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

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

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

BME280APIにWire(I2C)を組み込む

BME280のデータの取得はBME280の製作メーカであるBOSCH社が提供しているAPIを使用が準備した関数にWire関数を組み込んで行います。BME280APIの組み込み方法については下記リンク内の「BME280のダウンロードとAPIの実装方法」を参照してください。

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

Seeeduino XIAOのWire(I2C)をBME280APIに組み込むことでデータの送信と受信ができるようになります。組み込む際の例を示します。

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

マスタ送信の例を示します。送信は基本的にWire.write()を使用することで送信することができます。

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()でストップ・コンディションを発行して終了です。

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

マスタ受信の例を示します。受信は対象のアドレスを指定するためにWire.write()を使用し受信サイズを指定してWire.read()を使ってデータを読み込みます。

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)でセットしています。読み込み先のアドレスの指定が完了したので一旦ストップ・コンディションの発行を行います。

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

動作確認

Seeeduino XIAOとBME280の配線
Seeeduino XIAOとBME280の配線

Seeeduino XIAOとBME280の配線例を示しています。Seeeduino XIAOは電源電圧がDC3.3Vなので直接接続することができます。I2Cの入出力にはプルアップ抵抗が必要です。抵抗値は通信速度を早くしたい場合は小さくする必要がありますが、モジュールに流れる電流が増えてしまうため注意が必要です。

シリアルモニタでの動作確認
シリアルモニタでの動作確認

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

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

ソースコード全体

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

#include <Wire.h>
#include <TimerTC3.h>
#include "bme280.h"

#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);
void TimerCnt();

void setup() {
    Wire.begin();
    //Wire.setClock(100000);
    TimerTc3.initialize(1000);
    TimerTc3.attachInterrupt(TimerCnt);
    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;
           
            stream_sensor_data_forced_mode(&bme280main);
            
            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);
        stream_sensor_data_forced_mode(&bme280main);
    }
}
/* 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;
}

上記のスケッチ例のみでは動作しません。関連記事のArduinoのWireライブラリを使ってBME280のデータを取得するのBME280APIのソースを追加し、スケッチ例を上記のものと置き換えると動作します。

関連リンク

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

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

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

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

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

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

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