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

組み込みエンジニア

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

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

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

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

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

ArduinoでSPIを使用する

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

ArduinoとBME280(SPI)の配線

Arduino UNOとBME280の配線図(SPI)
Arduino UNOとBME280の配線図(SPI)

ArduinoとBME280の配線例を示しています。BME280はDC3V系の電源で動作するモジュールであるためArduinoの5Vを直接印加することができません。そのためArduinoからBME280に向かう信号については抵抗で分圧してBME280に供給しています。

一方BME280からArduinoに向かう信号はArduinoが3Vで受けたとしてもHレベルとして認識できるためそのまま接続しています。BME280を選択するCSは任意のDOを使用することができます。今回は9ピンをDO出力設定しSPIのデバイス選択とします。

信号名説明
13ピン:SCKSPIデバイスへのクロックを出力する
12ピン:MISOマスタが入力でスレーブが出力
11ピン:MOSIマスタが出力でスレーブが入力
上記以外の任意のDOピン:CSSPIデバイス(スレーブ)の選択
SPIの配線の組み合わせ

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

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

ライブラリの使い方

#include <SPI.h>

#define SPI_CS_ON digitalWrite(spi_cs,LOW)
#define SPI_CS_OFF digitalWrite(spi_cs,HIGH)

const uint8_t spi_cs = 9;

「SPI.h」をインクルードしCSに使用するDOの操作について定義しています。digtalWrite()をそのままソース内に記述してもよいのですが、CSの制御はSPIのシーケンスの一部なのでソース上区別しやすいためSPI_CS_ONとSPI_CS_OFFを定義してDOを操作しています。

void setup() {
 
    pinMode(spi_cs, OUTPUT); //CSとして使用するDOを設定
    SPI_CS_OFF; //Hレベルにする(アクティブLなのでHを初期値とする)
    SPI.begin();
    SPI.setBitOrder(MSBFIRST); //最上位ビットから順番に送信
    SPI.setDataMode(SPI_MODE0); //SPIモードを0にする
  //SPI.setClockDivider(SPI_CLOCK_DIV4); //クロックを4分周(基本は4MHz)
}

BME280はアクティブL(CSがLレベルの時動作)なので初期状態としてHレベルにしています。SPI通信のビットを最上位ビットから順番に送信する設定とマスタのSPIモードを3にする設定をしています。SPIには4つのモードがありCPOLビットとCPHAビットの組み合わせによって動作が決まります。

CPOLビットはマスタがアイドル状態(何もせず待機している)ときはのクロックの極性を決めるものです。0にするとクロックがLレベルでアイドルとなり1にするとクロックがHレベルでアイドルとなります。

CPHAビットはクロックの位相を選択するものです。0にするデータをクロックの立上がりでサンプリング(データを確定)し立下りでデータをシフト(次のビットの値をセット)します。1にするとデータをクロックの立下りでサンプリングし立上がりでデータをシフトします。

ModeCPOL:CPHA説明
00:0クロックはアイドル時Lレベル
クロックの立上がりでデータをサンプリングし立上りでシフトする
10:1クロックはアイドル時Lレベル
クロックの立下りでデータをサンプリングし立上がりでシフトする
21:0クロックはアイドル時Hレベル
クロックの立上りでデータをサンプリングし立下りでシフトとする
31:1クロックはアイドル時Hレベル
クロックの立下りでデータをサンプリングし立上りでシフトする
SPIモードのまとめ

基本的に制約がない限りモードを気にせずに使用できますが、スレーブの仕様によってデータの準備のタイミングなどの影響を受けるためスレーブ側の通信仕様を確認しておくことが必要です。今回の例ではモード0とモード3でデータが取得できましたが、モード1とモード2においてはデータを取得できていません。

SPIのボーレートを遅くするほどスレーブ側がデータの準備する余裕ができるため安定した通信ができます。通信に安定性を持たせるため分周する際はSPI.setClockDivider(分周値)によってボーレートを設定することができます。

設定しない場合は初期条件(ライブラリのソースコードを確認すると4MHzになっている)となります。

SPIによるデータの送信と受信

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

int8_t user_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
    int8_t rslt = 0;
  
    SPI_CS_ON; //CSをLレベル
    SPI.transfer(reg_addr); //対象アドレスを送信(書き込み)
    for(uint8_t i=0; i < len; i++){
        SPI.transfer(*reg_data); //対象データを送信(書き込み)
        ++reg_data;
    }
    SPI_CS_OFF; //CSをHレベル
    return rslt; //APIに戻り値を返す必要がある
}

最初にBME280(スレーブ)に対してデバイスが選択されていることを通知するためにCSをLレベルにしています。SPI.transfer(reg_addr)で対象のアドレスを指定し、lenサイズ分だけSPI.transfer(*reg_data)によってデータを送信(書き込み)しています。書き込むアドレスとサイズやデータはAPIが引数として指定しています。

データをすべて送信した後はBME280に対するCSをHレベルにしてデバイスの選択を解除しています。

int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
    int8_t rslt = 0;
  
    SPI_CS_ON; //CSをLレベル
    SPI.transfer(reg_addr); //対象アドレスを送信(書き込み)
    for( uint8_t i=0;i < len; i++ ){
      *reg_data = SPI.transfer(0x00); //ダミーで0を送信
      ++reg_data;
    }
    SPI_CS_OFF; //CSをHレベル
    return rslt; //APIに戻り値を返す必要がある
}

BME280にデバイス選択の通知のためにCSをLレベルにします。SPI.transfer(reg_addr)で対象のアドレスを指定し、lenサイズ分だけデータを取得するためにダミーで0を送信しています。

ダミーで0を送信しているのはスレーブに対してクロックを供給するためです。スレーブはマスタのクロックに同期して返信データをセットするためダミーで0を送信すると同時にスレーブが応答し、そのデータをreg_dataに格納しています。

スレーブに対してクロックを供給することが目的なのでダミーデータは任意の1バイトデータで問題ありません。SPIは基本的に送信と受信が同時に処理される仕様であることがポイントです。

データをすべて送信した後はBME280に対するCSをHレベルにしてデバイスの選択を解除しています。

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-spiに変更しています。

BME280APIの実装の準備

BME280APIは温湿度や気圧の計算を行う特殊な計算やフィルタに関しての設定が簡単にできるように構成されていますが、SPIの送受信などは使用するマイコンに合わせて組み込む必要があります。プロジェクト最初に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_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
  6. int8_t user_spi_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 = 0;
    bme280main.intf = bme280_intf::BME280_SPI_INTF;
    bme280main.read = user_spi_read;
    bme280main.write = user_spi_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;
    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;

    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のステータスがリターンするように組み込む必要があります。実装例はSPIによるデータの送信と受信で示しています。

動作確認

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

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

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

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

ソースコード全体

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

#include <SPI.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

#define SPI_CS_ON digitalWrite(spi_cs,LOW)
#define SPI_CS_OFF digitalWrite(spi_cs,HIGH)

const uint8_t spi_cs = 9;

//application use
bme280_dev bme280main;
bme280_data sensor_data;
int8_t cnt10ms;
int16_t timsensor = TIME_OFF;

/*** Local function prototypes */
void Bme280Init();
int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
int8_t user_spi_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() {

    pinMode(spi_cs, OUTPUT); //CSとして使用するDOを設定
    SPI_CS_OFF; //Hレベルにする(アクティブLなのでHを初期値とする)
    SPI.begin();
    SPI.setBitOrder(MSBFIRST); //最上位ビットから順番に送信
    SPI.setDataMode(SPI_MODE0); //SPIモードを0にする
    //SPI.setClockDivider(SPI_CLOCK_DIV4); //クロックを4分周(基本は4MHz)

    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_spi_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
  int8_t rslt = 0;
  
    SPI_CS_ON; //CSをLレベル
    SPI.transfer(reg_addr); //対象アドレスを送信(書き込み)
    for(uint8_t i=0; i < len; i++){
        SPI.transfer(*reg_data); //対象データを送信(書き込み)
        ++reg_data;
    }
    SPI_CS_OFF; //CSをHレベル
    return rslt;
}
/* Bme280 api use function add */
int8_t user_spi_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
    int8_t rslt = 0;
  
    SPI_CS_ON; //CSをLレベル
    SPI.transfer(reg_addr); //対象アドレスを送信(書き込み)
    for( uint8_t i=0;i < len; i++ ){
      *reg_data = SPI.transfer(0xFF); //ダミーで0を送信
      ++reg_data;
     
    }
    SPI_CS_OFF; //CSをHレベル
    return rslt;
}
/* Bme280 sensor iniialize */
void Bme280Init(){
    bme280main.dev_id = 0;
    bme280main.intf = bme280_intf::BME280_SPI_INTF;
    bme280main.read = user_spi_read;
    bme280main.write = user_spi_write;
    bme280main.delay_ms = user_delay_ms;

    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;
    //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;
    
    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をコピーしました