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

組み込みエンジニア

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

Seeeduino XIAOはArduino環境でソフト開発ができ、SPIライブラリが使用できます。BME280の製作メーカが提供しているAPIを使用して温度・気圧・湿度の計算を行いSPI通信でセンサー情報を取得する方法をまとめました。

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

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

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

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

Seeeduino XIAOでSPIを使用する

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

SPIを使用する場合のピン配置

Seeeduino XIAOではSPIを使用する場合は以下の通りのピン設定となります。スレーブ選択のためのチップセレクトについては任意のDOを指定します。

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

本記事ではDO0をCS(チップセレクト)に設定してスレーブ(BME280)に対してデバイスが選択されたことを通知します。動作確認の回路図参照

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

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

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

#include <SPI.h>

#define PIN_DO1 0
#define SPI_CS_ON digitalWrite(PIN_DO1,LOW)
#define SPI_CS_OFF digitalWrite(PIN_DO1,HIGH)

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

void setup() {
 
    pinMode(PIN_DO1, 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にするとデータをクロックの立下りでサンプリングし立上がりでデータをシフトします。

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

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

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

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

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

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

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

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

SPIによるデータ送信の例

SPI送信の例を示します。送信はSPI.transfer()を使用することで送信することができます。

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レベルにしてデバイスの選択を解除しています。

SPIによるデータ受信の例

SPI受信の例を示します。受信も送信と同様にSPI.transfer()を使用しますがアドレス指定した後データを読み込む際の用途が異なります。

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レベルにしてデバイスの選択を解除しています。

動作確認

SeeeduinoとBME280の動作確認の回路図
SeeeduinoとBME280の動作確認の回路図

Seeeduino XIAOとBME280の配線例を示しています。Seeeduino XIAOの電源はDC3.3V系なので直接接続することができます。SPIでは対象となるデバイスが選択されているかを通信相手のモジュールに伝える必要があります。

BME280のデータをSPIで取得した場合の結果
BME280のデータをSPIで取得した場合の結果

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

温度(Temperature)は部屋に置いている温度計によると21.5℃であり、気圧(Pressure)はスマホを見ると1019hPa=1019mBarで湿度(Humidity)についてはスマホのデータでは66%でした。

室内でエアコン(暖房)をつけているため外気に比べて乾燥している状況を考えてみても少し誤差がある気がしますが、室内のデータであることも考慮するとそれなりに計測できていると思います。

Seeeduino XIAOのSPIを使用してセンサーの情報が取得できるようになることで他のSPI通信が可能なセンサーのデータも取得できるため応用範囲が広がりそうです。最近センサーはDC3.3Vのものが多いので直接接続できるのも動作確認しやすく良い点だと思います。

ソースコード全体

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

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

#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define GET_SENSOR_MAX 500

#define PIN_DO1 0
#define SPI_CS_ON digitalWrite(PIN_DO1,LOW)
#define SPI_CS_OFF digitalWrite(PIN_DO1,HIGH)

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

void setup() {

    pinMode(PIN_DO1, 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)
    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_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);
    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のSPIライブラリを使ってBME280のデータを取得する で紹介しているBME280APIのソースを追加し、スケッチ例を上記のものと置き換えると動作します。

関連リンク

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

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

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

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

テックジム-将来のためにプログラミングを学ぶ

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

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