ESP32-WROOM-32EのSPIでBME280の情報を取得

組み込みエンジニア

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

Arduino環境ではSPIライブラリを使うと簡単にSPI通信が実現できます。BME280は温湿度・気圧センサーでありI2C/SPIの双方の通信に対応しておりSPIで情報の取得ができます。SPIでセンサー情報を取得する方法をまとめました。

ESP32-WROOM-32Eの開発環境はArduino IDEを使用しています。またESP32-WROOM-32E開発ボード(秋月電子)及びAE-BME280(秋月電子)を使用しています。

BME280の製作メーカーが提供しているBME280用のAPIを組み込んで動作させる方法とライブラリマネージャからインストールした「Adafruit BME280」を使用する方法を紹介しています。

ESP32-WROOM-32EでSPIを使用する

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

BME280のAPIを使用する方法

Arduino環境にBME280APIを追加する方法については下記記事にまとめています。

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

以下の説明はBME280APIが作業フォルダに準備されていることを前提にしてSPIライブラリを組み込む方法を中心に説明しています。

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

ESP32-WROOM-32EはSPIはVSPIとHSPIが実装されています。ソフトウェアSPIを使用する場合は任意のピンで動作させることができます。特に指定しない場合はVSPIを使用するように設定されます。

信号名ピン番号:シルク説明
VSPI_SCK30ピン:18SPIデバイスへのクロックを出力する
VSPI_MISO31ピン:19マスタが入力でスレーブが出力
VSPI_MOSI37ピン:23マスタが出力でスレーブが入力
VSPI_SS29ピン:5SPIデバイス(スレーブ)の選択
SPIの配線の組み合わせ(VSPI)
信号名ピン番号:シルク説明
HSPI_SCK12ピン:14SPIデバイスへのクロックを出力する
HSPI_MISO13ピン:12マスタが入力でスレーブが出力
HSPI_MOSI15ピン:13マスタが出力でスレーブが入力
HSPI_SS23ピン:15SPIデバイス(スレーブ)の選択
SPIの配線の組み合わせ(HSPI)

ピン番号はESP32-WROOM-32Eの左上から反時計回りにピンを数えた場合の番号です。シルクはピンの横に印刷された番号で主にGPIOの番号になります。

VSPI_SSやHSPI_SS(スレーブセレクト)は任意のDOを指定しても問題ありません。SPI通信を行う最初にスレーブ(BME280)に対してデバイスが選択されたことを通知するために使用します。

BME280はESP32-WROOM-32EのSCKのクロックの供給によってデータの返信をします。クロックが供給されなければデータを返信することができません。

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

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

#include <SPI.h>

#define SPI_SS 5 //SSのDOに使用する番号
SPIClass *spi = NULL; //SPIクラスのポインタを準備

最初に「SPI.h」をインクルードする必要があります。SPI_SSはスレーブセレクトに使用するDOの番号を定義しています。SPIクラスのポインタを宣言しているのはVSPIとHSPIを切り替える際に使用するためです。VSPIに固定する場合は必要ありません。

void setup() {

  pinMode(SPI_SS,OUTPUT);
  digitalWrite(SPI_SS, HIGH); //HにしてSS選択を解除
  spi = new SPIClass(HSPI); //HSPIを選択する場合
  spi->begin();
//SPI.begin(SPI_SCLK,SPI_MISO,SPI_MOSI,SPI_SS); ソフトウェアSPIの場合の例
}
//使用例
spi->transfer(reg_addr); //ポインタに対するアクセスなので->となる

HSPIを使用するためにはSPIクラスにおいてHSPIを指定する必要があります。SPIクラスのポインタで宣言してspiにNewでSPIクラスのインスタンス(実体)を作ってアドレスを渡しています。spiがSPIの実体として動くため以降のソースではspiを使用します。

BME280はアクティブL(CSがLレベルの時動作)なので初期状態としてHレベルにしています。ソフトウェアSPIはGPIOをSPI通信を模擬して動作させるもので任意のGPIOピンを指定して動作させることができますが、速度をあまり高速にできません。使用する場合はbegin()の引数でピン番号を指定します。

SPI通信のモードと開始

初期条件から変更しない場合はbeginTransaction()及びSPISettings()による設定は必要ありません。初期条件は通信速度が1MHzでありSPIモード0でMSBFISTでデータを送信するようになっています。

spi->beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始

SPI通信の設定はSPISetting()で行います。第1引数にSPI通信の速度を設定します。第2引数にデータのビット構成を指定します。最上ビットから送る場合はMSBFIRST、最下位ビットから送る場合はLSBFIRSTを指定します。第3引数にSPIモードを指定します。

beginTransaction()はSPISettings()の戻り値を指定することで設定した条件でSPI通信を開始します。

SPIの4つのモード(参考)

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モードのまとめ

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

SPIによるデータ送信の例

BME280APIのデータ送信関数にArduinoの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->beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    digitalWrite(SPI_SS, LOW);

    spi->transfer(reg_addr);

    for(uint16_t i = 0; i < len; i++){
        spi->transfer(*reg_data);
        ++reg_data;
    }
    digitalWrite(SPI_SS, HIGH);
    spi->endTransaction();
    
    return rslt;
}

最初にSPIスレーブとの通信条件を設定します。通信条件が初期値から変更しない場合は特に必要ありません。通信条件を設定するとSPI通信開始のためSSをLレベルにしてデバイスが選択されていることを通知します。

transfer(reg_addr)で対象のアドレスを指定し、lenサイズ分だけtransfer(*reg_data)によってデータを送信(書き込み)しています。書き込むアドレスとサイズやデータはBME280APIが引数として指定しています。

データを送信が完了するとSSをHレベルしてデバイスの選択を解除しendTransaction()によって通信を終了します。

SPIによるデータ受信の例

BME280APIのデータ受信関数にArduinoの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->beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    digitalWrite(SPI_SS, LOW);
    spi->transfer(reg_addr);

    for(uint16_t i = 0; i < len; i++){
        *reg_data = spi->transfer(0x00);
        ++reg_data;
    }
    digitalWrite(SPI_SS, HIGH);
    spi->endTransaction();

    return rslt;
}

基本的な手順は送信と同じですがデータの取得部分が.transfer(reg_addr)で対象のアドレスを指定し、lenサイズ分だけデータを取得するためにダミーで0を送信しています。

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

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

データを受信が完了するとSSをHレベルしてデバイスの選択を解除しendTransaction()によって通信を終了します。

Adafruit BME280 ライブラリを使用する

Arduino IDEのライブラリマネージャの検索欄にbme280を入力するとBME280ライブラリの候補が表示されます。候補の中からAdafruit BME280 Libraryをインストールします。

Adafruit BME280 Libraryをインストールする際に他の追加ライブラリをインストールするか選択するメッセージが表示されることがあります。

Adafruit BME280 Libraryは他のライブラリと関連性があるため単体では使用できないためインストールを勧められています。Install allを選択してその他のライブラリも追加します。

#include <Adafruit_BME280.h>
#include <SPI.h>
#define BME_SS 5

//変数宣言
Adafruit_BME280 bme280(BME_SS); //測定情報などを格納する変数を宣言
//初期化
bme280.begin();
//使用例
bme280data[0] = bme280.readTemperature(); //温度情報を取得
bme280data[1] = bme280.readPressure() / 100.0F; //気圧情報を取得
bme280data[2] = bme280.readHumidity(); //湿度情報を取得

Adafruit BME280 Libraryをインクルードします。setup()関数内でbme28.begin()でライブラリの初期化を行います。BME280をSPIで使用する場合は変数でAdafruit_BMEの型で宣言した変数に引数としてSS番号を指定します。

Adafruit_BMEライブラリではVSPIを使用するように初期化されます。begin()において引数にSPIライブラリのように各種ピン番号を指定することでソフトウェアSPIによる動作も可能です。

温度情報の取得はreadTemperature()、気圧情報の取得はreadPressure()、湿度情報の取得はreadHumidity()を使用します。

使用例のようにデータを取得した場所で関数をコールするだけで簡単に測定値が取得できるため手短に動作確認したい場合にお勧めのライブラリです。

動作確認

動作確認用の回路図(SPI通信)
動作確認用の回路図(SPI通信)

ESP32-WROOM-32EとBME280の配線例を示しています。初期化時に指定しなければVSPIになります。VSPIは黒線の配線になりますが、HSPIを選択した場合は青線の配線となります。

回路図の番号はESP32-WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

VSPI_SS及びHSPI_SSは任意のDOを選択しても良いのですが可能な限りSSピンに配線するようにしておくと確認がしやすくなります。

電源ONしてから1秒間隔でBME280の測定とデータの取得を行います。シリアルモニタの表示を確認するとデータが取得できていることが分かりました。

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

温度(Temperature)は部屋に置いている温度計によると24.5℃であり、気圧(Pressure)はスマホを見ると998hPa=998mBarで湿度(Humidity)についてはスマホのデータでは66%でした。設置する環境にも左右されますがSPI通信でデータが取得できていることが確認できたため良好な結果です。

ソースコード全体

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

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

#define SPI_MISO   26
#define SPI_MOSI   27
#define SPI_SCLK   25
#define SPI_SS 5

#define BASE_CNT 10
#define TIM_FORCE 100
#define TIM_GET 10
#define TIME_UP 0
#define TIME_OFF -1
#define HSPI_USE //HSPIを使用する場合コメントアウトを外す

#ifdef HSPI_USE
SPIClass *spi = NULL;
#endif
bme280_dev bme280main;
bme280_data sensor_data;
uint32_t beforetimCnt = millis();
uint32_t beforetimdat;
int16_t timforcedat = TIME_OFF;
int16_t timgetdat = TIME_OFF;

void Bme280Init(void);
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 mainApp(void);
bool startSensorCapture(void);

void setup() {

    pinMode(SPI_SS,OUTPUT);
    #ifdef HSPI_USE
        spi = new SPIClass(HSPI);
        spi->begin();
    #else
        SPI.begin();
        //SPI.begin(SPI_SCLK,SPI_MISO,SPI_MOSI,SPI_SS); ソフトウェアSPIの場合の例
    #endif
    Serial.begin(115200);
    Bme280Init();
    timforcedat = TIM_FORCE;
}

void loop() {

    mainApp();
}
/* メインアプリ */
void mainApp(void){

    if ( millis() - beforetimCnt > BASE_CNT ){
        beforetimCnt = millis();

        if( timforcedat > TIME_UP ){
          --timforcedat;
        }
        if( timgetdat > TIME_UP ){
          --timgetdat;
        }
    }

    if( timforcedat == TIME_UP ){
        timforcedat = TIM_FORCE;
        stream_sensor_data_forced_mode(&bme280main); //測定開始
        timgetdat = TIM_GET;
    }
    if( timgetdat == TIME_UP ){
        timgetdat = TIME_OFF;
        startSensorCapture(); //測定値を取得
    }
}
/* Bme280 sensor iniialize */
void Bme280Init(void){
    int8_t no;

    bme280main.dev_id = 0;
    bme280main.intf = BME280_SPI_INTF;
    bme280main.read = user_spi_read;
    bme280main.write = user_spi_write;
    bme280main.delay_ms = user_delay_ms;

    no = bme280_init(&bme280main);
    if(no ==0){
        Serial.println("init_ok");
        stream_sensor_data_forced_mode(&bme280main);
    }
    else{
        Serial.println("init_ng");
    }
}
/* 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;

    #ifdef HSPI_USE
        spi->beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    #else
        SPI.beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    #endif
    digitalWrite(SPI_SS, LOW);
    #ifdef HSPI_USE
        spi->transfer(reg_addr);
    #else
        SPI.transfer(reg_addr);
    #endif
    for(uint16_t i = 0; i < len; i++){
        #ifdef HSPI_USE
            *reg_data = spi->transfer(0x00);
        #else
            *reg_data = SPI.transfer(0x00);
        #endif
        ++reg_data;
    }
    digitalWrite(SPI_SS, HIGH);
    #ifdef HSPI_USE
        spi->endTransaction();
    #else
        SPI.endTransaction();
    #endif

    return rslt;
}
/* 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;

    #ifdef HSPI_USE
        spi->beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    #else
        SPI.beginTransaction(SPISettings(1000000,MSBFIRST,SPI_MODE0)); //設定して開始
    #endif
    digitalWrite(SPI_SS, LOW);
    #ifdef HSPI_USE
        spi->transfer(reg_addr);
    #else
        SPI.transfer(reg_addr);
    #endif
    for(uint16_t i = 0; i < len; i++){
        #ifdef HSPI_USE
            spi->transfer(*reg_data);
        #else
            SPI.transfer(*reg_data);
        #endif
        ++reg_data;
    }
    digitalWrite(SPI_SS, HIGH);
    #ifdef HSPI_USE
        spi->endTransaction();
    #else
        SPI.endTransaction();
    #endif
    
    return rslt;
}
/* 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);
    rslt = bme280_set_sensor_mode(BME280_FORCED_MODE, dev);
    //dev->delay_ms(req_delay);
    return rslt;
}
/* sensor data read */
bool startSensorCapture(void){
    bool ret = true;
    int8_t i=0;
    
    if(bme280_get_sensor_data(BME280_ALL,&sensor_data, &bme280main) == 0 ){
        Serial.print("temp:"); 
        Serial.print(sensor_data.temperature);Serial.print("[℃],");
        Serial.print("press:"); 
        Serial.print(sensor_data.pressure/100);Serial.print("[hPa],");
        Serial.print("humid:"); 
        Serial.print(sensor_data.humidity);Serial.print("[%]");
        Serial.println();
    }
    else{
        Serial.println("SPI_DATA_NG");
        ret = false;
    }

    return ret;
}

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

関連リンク

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

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

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

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

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

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

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