PR

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

組み込みエンジニア
本記事はプロモーションが含まれています。

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

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

本記事はSeeeduino XIAOでSPIライブラリを使用してBME280モジュールからSPI通信を行いデータ取得する方法についてまとめています。BME280 温湿度・気圧センサモジュール(I2C/SPI タイプ)(ストロベリー・リナックス)を使用しています。

下記リンクの内容を引用しながらSeeeduino XIAOで動作確認をしています。

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

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

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

Seeeduino XIAOでSPIを使用する

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

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

SeeeduinoとBME280モジュールの動作確認の回路図
SeeeduinoとBME280モジュールの動作確認の回路図

Seeeduino XIAOとBME280モジュールの配線例を示しています。Seeeduino XIAOの電源はDC3.3V系なので直接接続することができます。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の操作について定義しています。BME280モジュールのマニュアルにCSがLOWアクティブとあるのでCSがONの時にLOWになるようにdigtalWrite()を定義しています。

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モジュールはCSがLOWでアクティブになるので初期状態としてHIGHにしています。SPI通信のビットを最上位ビットから順番に送信する設定とマスタのSPIモードを0にする設定をしています。

begin()関数でSPIの初期化を行います。setBitOrder()関数は送信のビットに関する設定で、デフォルトは最上位ビットから順番に送信(MSBFIRST)です。setDataMode()関数はSPIのモードを指定するのもで、デフォルトはモード0です。setClockDivider()関数は基本クロック4MHzの分周比を指定します。スレーブの応答速度に応じてクロックを調整するのに使用します。

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のボーレートを遅くするほどスレーブ側がデータの準備する余裕ができるため安定した通信ができます。通信に安定性を持たせるため分周する際はsetClockDivider関数()で分周比を設定することができます。

スポンサーリンク

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

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

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

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

SPIの送信と実装例

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

  1. CSでデバイスを選択する
  2. transfer()関数でデータを送信
  3. CSのデバイス選択を解除

1はBME280モジュールはLOWアクティブなのでLOWにするとBME280モジュールが動作開始します。2はtransfer()関数の引数に送信データを指定して送信します。3はデータ送信が終わったのでBME280モジュールのデバイス選択を解除します。

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をLOWにしています。transfer()関数で対象のアドレスを指定し、transfer()関数でlenのサイズ分のデータを送信(書き込み)を行います。データをすべて送信するとBME280モジュールに対するCSをHIGHにして選択を解除しています。

SPIの受信と実装例

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

  1. CSでデバイスを選択する
  2. transfer()関数でデータを送信
  3. transfer()関数でダミーデータを送信してデータを読み込む
  4. CSのデバイス選択を解除

1と2については送信と同様ですが、3はtransfer()関数の戻り値が読み込みデータになるのでダミーデータでクロックをBME280モジュールに供給してデータを読み込みます。データを読み込み終えたらBME280モジュールに対するCSをHIGHにして選択を解除しています。

受信も送信と同様に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をLOWにしています。transfer()関数で対象のアドレスを指定し、データをlenサイズ分読み込むためダミーで0を送信しています。

ダミーで0を送信しているのはBME280モジュールにクロックを供給するためです。BME280モジュールはArduinoのクロックに同期してデータをセットするためダミーで0を送信するとBME280モジュールが応答し、transfer()関数の戻り値となります。

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

データをすべて受信したらBME280モジュールのCSをHIGHにして選択を解除しています。

スポンサーリンク

動作確認

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

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

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

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

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

広告
マイベスト3年連続1位を獲得した実績を持つ実践型のプログラミングスクール

ソースコード全体

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

#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);
      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("[%]");
    }
  }
}
/* 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で学べるソフト開発と標準ライブラリの使い方

PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!

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

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