こんにちは、ENGかぴです。
Arduino環境ではSPI通信を行うために標準ライブラリとしてSPIライブラリがあります。BME280の製作メーカが提供しているAPIを使用して温度・気圧・湿度の計算を行いSPI通信でセンサー情報を取得する方法をまとめました。
BME280用のライブラリが有志によって公開されていますがSPIライブラリの使用方法や組み込み方法を説明するため、BME280の製作メーカであるBOSCH社が提供しているAPIを組み込んで実装しています。
BME280 温湿度・気圧センサモジュール(I2C/SPI タイプ)(ストロベリー・リナックス)を使用しています。Arduino UNO(以下Arduinoとします。)を対象とします。Arduinoのライブラリを使用して動作確認したことをまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
BME280のダウンロードとAPIの実装方法
BME280のデータシートを確認すると温度・湿度・気圧に関して計算式や補正値による考え方が記載されていますが、BOSCH社が提供しているAPIを使用することが推奨されています。
データシートに記載されている計算式でも問題ないのですが、提供されているAPIによるものと比較するとAPIによるソースのほうが作りこまれているためAPIを使用したほうが良いでしょう。以降ではBME280のドライバーをBME280APIと表記します。
BME280APIをBOSCH社のHPからダウンロード
BOSCH社のHPからBME280ドライバーをダウンロードします。下記リンクをクリックするとダウンロードのページに遷移します。
BoschSensortec-BME280_driver(Bosch社のBME280のAPI)
「Code」をクリックすると「Download ZIP」を選択するとファイルをまとめてダウンロードすることができます。ダウンロードが完了したら展開します。
BME280のAPIソフトをArduinoファイル内に配置する
Arduino IDEのプロジェクトファイル表示欄横の▼マークから新規タブを押すと新規ファイル追加することができますが、ここではプロジェクトのフォルダ内にファイルを直接追加する方法を説明しています。
Arduinoのプロジェクトファイル(inoファイル)があるフォルダにダウンロードした「BME280_driver-master」の中から以下の3つをコピーして追加します。
- bme280.c
- bme280.h
- bme280_defs.h
上記の例ではArduinoファイルのプロジェクト名をbme280-spiに変更しています。
BME280APIの実装の準備
BME280APIは温湿度や気圧の計算を行う特殊な計算やフィルタに関しての設定が簡単にできるように構成されていますが、SPIの送受信などは使用するマイコンに合わせて組み込む必要があります。プロジェクト最初にbme280のAPIをコールするためにbme280.hをインクルードします。
#include "bme280.h"
BME280APIに使用する変数とユーザーが準備する関数の実装
BME280APIを使用するためには2つの変数を準備する必要があります。
bme280_dev bme280main;
bme280_data sensor_data;
bme280_devはAPIを使用するための情報を格納する変数です。この変数に必要な情報を記述することでAPIとリンクすることができます。bme280_dataはAPIが計算した温度、湿度、気圧に関するデータを格納する変数です。
ユーザーが準備する関数についてはダウンロードしたファイルの中になる「README.md」をテキストで開くと内容が確認できます。ポイントとなる関数は以下の通りになります。
- bme280_init(&dev)
- int8_t stream_sensor_data_forced_mode(struct bme280_dev *dev)
- int8_t stream_sensor_data_normal_mode(struct bme280_dev *dev)
- void user_delay_ms(uint32_t period)
- 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)
1はBME280APIを初期化に使用しますが、コールする前にデバイスID(スレーブアドレス)、read/writeに使用する関数のアドレス、遅延用の関数を登録する必要があります。またSPI通信を使用するためintfにBME280_SPI_INTFをセットします。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(1)をコメントアウトしておいた方が良いでしょう。
void user_delay_ms(uint32_t period){
delay(period);
}
4の関数はmsのウェイトを持たせるために引数が指定されているのでdelay()関数を追加しています。
5と6の関数についてはセンサーの読み書きに使用しますが、準備されている引数を利用しながら組み込む必要があります。また戻り値にAPIのステータスがリターンするように組み込む必要があります。実装例はSPIの送信と実装例及びSPIの受信と実装例で示しています。
ArduinoでSPIを使用する
Arduinoの標準ライブラリであるSPIライブラリの使い方を説明します。SPIの送信と受信を中心に説明しています。BME280APIの実装の仕方については「BME280のダウンロードとAPIの実装方法」で説明しています。
SPIの送信と受信についてBME280APIの組み込み方法を例して説明します。
ArduinoとBME280(SPI)の配線
ArduinoとBME280モジュールの配線例を示しています。BME280モジュールはDC3V系の電源で動作するモジュールであるためArduinoの5Vを直接印加することができません。そのためArduinoからBME280モジュールに向かう信号については抵抗で分圧してBME280モジュールに供給しています。
一方BME280モジュールからArduinoに向かう信号はArduinoが3Vで受けたとしてもHレベルとして認識できるためそのまま接続しています。BME280モジュールを選択するCSは任意のDOを使用することができます。今回は9ピンをDO出力設定しSPIのデバイス選択とします。
信号名 | 説明 |
---|---|
13ピン:SCK | SPIデバイスへのクロックを出力する |
12ピン:MISO | マスタが入力でスレーブが出力 |
11ピン:MOSI | マスタが出力でスレーブが入力 |
上記以外の任意のDOピン:CS | SPIデバイス(スレーブ)の選択 |
BME280モジュールはArduinoのSCKのクロックの供給によってデータの返信をします。(クロック同期式という)クロックが供給されなければデータをArduinoに返信することができません。
SPI通信は基本的にマスター(クロックを出す側)がスレーブ(クロックを受けて動作する側)に対してクロックを供給する関係になります。
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の操作について定義しています。BME280モジュールのマニュアル(ストロベリー・リナックス社の資料)にCSがLOWアクティブとあるのでCSがONの時にLOWになるようにdigtalWrite()を定義しています。
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モジュールはCSがLOWでアクティブになるので初期状態をHIGHにしています。SPI通信のビットを最上位ビットから順番に送信する設定とマスタのSPIモードを0にする設定をしています。
begin()関数でSPIの初期化を行います。setBitOrder()関数は送信のビットを指定するもので、デフォルトは最上位ビットから順番に送信(MSBFIRST)です。setDataMode()関数はSPIのモードを指定するもので、デフォルトはモード0です。setClockDivider()関数は基本クロック4MHzの分周比を指定します。スレーブの応答速度に応じてクロックを調整するのに使用します。
SPIのモードの考え方
SPIには4つのモードがありCPOLビットとCPHAビットの組み合わせによって動作が決まります。
CPOLビットはマスタがアイドル状態(何もせず待機している)ときはのクロックの極性を決めるものです。0にするとクロックがLレベルでアイドルとなり1にするとクロックがHレベルでアイドルとなります。
CPHAビットはクロックの位相を選択するものです。0にするデータをクロックの立上がりでサンプリング(データを確定)し立下りでデータをシフト(次のビットの値をセット)します。1にするとデータをクロックの立下りでサンプリングし立上がりでデータをシフトします。
Mode | CPOL:CPHA | 説明 |
---|---|---|
0 | 0:0 | クロックはアイドル時Lレベル クロックの立上がりでデータをサンプリングし立上りでシフトする |
1 | 0:1 | クロックはアイドル時Lレベル クロックの立下りでデータをサンプリングし立上がりでシフトする |
2 | 1:0 | クロックはアイドル時Hレベル クロックの立上りでデータをサンプリングし立下りでシフトとする |
3 | 1:1 | クロックはアイドル時Hレベル クロックの立下りでデータをサンプリングし立上りでシフトする |
基本的に制約がない限りモードを気にせずに使用できますが、スレーブの仕様によってデータの準備のタイミングなどの影響を受けるためスレーブ側の通信仕様を確認しておくことが必要です。今回の例ではモード0とモード3でデータが取得できましたが、モード1とモード2においてはデータを取得できていません。
SPIのボーレートを遅くするほどスレーブ側がデータの準備する余裕ができるため安定した通信ができます。通信に安定性を持たせるため分周する際はsetClockDivider関数()で分周比を設定することができます。
SPIの送信と実装例
SPIライブラリを使った送信の手順は以下の通りです。
- CSでデバイスを選択する
- transfer()関数でデータを送信
- 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ライブラリを使った受信の手順は以下の通りです。
- CSでデバイスを選択する
- transfer()関数でデータを送信
- transfer()関数でダミーデータを送信してデータを読み込む
- CSのデバイス選択を解除
1と2については送信と同様ですが、3はtransfer()関数の戻り値が読み込みデータになるのでダミーデータでクロックをBME280モジュールに供給してデータを読み込みます。データを読み込み終えたらBME280モジュールに対するCSをHIGHにして選択を解除しています。
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モジュールが返信したデータを戻り値にセットします。
BME280モジュールにクロックを供給することが目的なのでダミーデータは任意の1バイトデータで問題ありません。SPIは基本的に送信と受信が同時に処理される仕様であることがポイントです。
データをすべて受信したらBME280モジュールのCSをHIGHにして選択を解除しています。
BME280ライブラリを追加して使用する(参考)
BME280APIを組み込んで動作させる方法を説明しましたが、Arduino IDEでBME280ライブラリを追加する方法もあります。BME280ライブラリの中でAdafruit BME280 ライブラリを使用する方法を参考として説明します。
Arduino IDEのライブラリマネージャの検索欄にbme280を入力するとBME280ライブラリの候補が表示されます。候補の中からAdafruit BME280 Libraryをインストールします。
Adafruit BME280 Libraryをインストールする際に他の追加ライブラリをインストールするか選択するメッセージが表示されることがあります。
Adafruit BME280 Libraryは他のライブラリと関連性があるため単体では使用できないことがあるためインストールを勧められています。Install allを選択してその他のライブラリも追加することをお勧めします。
#include <Adafruit_BME280.h>
#define SPI_CS 9
Adafruit_BME280 bme280(SPI_CS);
float bme280data[3];
void setup() {
Serial.begin(115200);
bme280.begin();
}
void loop() {
Bme280Get();
delay(1000);
}
/* BME280のデータ取得 */
void Bme280Get(void){
Serial.print("Temperature = ");
bme280data[0] = bme280.readTemperature();
Serial.print(bme280data[0]);
Serial.println(" °C");
Serial.print("Pressure = ");
bme280data[1] = bme280.readPressure() / 100.0F;
Serial.print(bme280data[1]);
Serial.println(" hPa");
Serial.print("Humidity = ");
bme280data[2] = bme280.readHumidity();
Serial.print(bme280data[2]);
Serial.println(" %");
Serial.println();
}
Adafruit BME280 Libraryをインクルードします。SPIで使用する場合はAdafruit_BME280クラスの型でインスタンス化した変数の引数にCSに使用するピンを指定します。setup()関数内でbegin()関数でライブラリの初期化を行います。
温度情報の取得はreadTemperature()関数、気圧情報の取得はreadPressure()関数、湿度情報の取得はreadHumidity()関数を使用します。
動作確認
Arduino IDEのシリアルモニターにBME280モジュールから取得したデータを換算した値を表示しています。
温度(Temperature)は部屋に置いている温度計によると25.5℃でした。気圧(Pressure)と湿度(Humidity)はスマホ(Android)で確認すると動作確認の時点で気圧が1009hPa=1009mBar、湿度(Humidity)が85%でした。
少し誤差がある気がしますが、室内のデータであることも考慮するとそれなりに計測できていると思います。
ArduinoのSPIを使用してセンサーの情報が取得できるようになることで他のSPI通信が可能なセンサーのデータも取得できるため応用範囲が広がりそうです。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <SPI.h>
#include "bme280.h"
#define FORCED_MODE //省エネモードを使用するとき定義する
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define GET_SENSOR_MAX 100
#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;
uint32_t beforetimCnt = millis();
/*** 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 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)
Serial.begin(115200);
Bme280Init();
timsensor = GET_SENSOR_MAX;
}
void loop() {
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
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);
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("[%]");
}
}
}
/* 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;
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;
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, &comp_data, dev);
//}
return rslt;
}
BME280のダウンロードとAPIの実装方法で説明しているBME280APIをArduinoのプロジェクトファイルに追加することで動作します。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。