こんにちは、ENGかぴです。
Seeeduino XIAOはArduinoと互換性があるためWireライブラリが使用できます。BME280の製作メーカが提供しているAPIを使用して温度・気圧・湿度の計算を行いI2C通信でセンサー情報を取得する方法をまとめました。
本記事はSeeeduino XIAOでWireライブラリを使用してBME280モジュールからI2C通信を行いデータ取得する方法についてまとめています。下記リンクの内容を引用しながらSeeeduino XIAOで動作確認をしています。
ArduinoのWireライブラリを使ってBME280のデータを取得する
BME280 温湿度・気圧センサモジュール(I2C/SPI タイプ)(ストロベリー・リナックス)を使用しています。Seeeduino XIAOで動作確認したことについてリンクをまとめています。
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
BME280APIにWire(I2C)を組み込む
BME280のデータの取得はBME280の製作メーカであるBOSCH社が提供しているAPIを使用が準備した関数にWire関数を組み込んで行います。BME280APIの組み込み方法については下記リンク内の「BME280のダウンロードとAPIの実装方法」を参照してください。
ArduinoのWireライブラリを使ってBME280のデータを取得する
Seeeduino XIAOのWire(I2C)をBME280APIに組み込むことでデータの送信と受信ができるようになります。以下ではBME280APIが宣言している関数であるuser_i2c_write()とuser_i2c_read()にWireライブラリを組み込む方法を中心に説明します。
Wire(I2C)を実装
Arduinoの標準ライブラリである「Wire.h」をインクルードして使用します。Wireライブラリを使用したI2Cの送信と受信の方法を中心に説明しています。
Wire(I2C)の考え方
Seeeduino XIAOのWireを使用する場合は6ピン(DIO5)がSCL、5ピン(DIO4)をI2C通信ポートとして使用します。これらのピンはプルアップする必要がありますが、Wireライブラリの標準速度は100kbpsであるため10kΩでプルアップしていれば十分です。
BME280モジュールはSeeeduino XIAOのSCLのクロックの供給によってデータの返信をします。(クロック同期式という)クロックが供給されなければデータをSeeeduino XIAOに返信することができません。
I2C通信は基本的にマスタ(クロックを出す側)がスレーブ(クロックを受けて動作する側)に対してクロックを供給する関係になります。
センサーモジュールにクロック発振器がついている場合はクロックを発生させられるためマスタにもスレーブにもなることができますが、基本的にはSeeeduino XIAOがマスタでセンサーモジュールがスレーブになります。
Wireライブラリの初期化
#include <Wire.h>
void setup() {
Wire.begin(); //begin()内にアドレスを入れるとスレーブになる
//Wire.setClock(100000); //クロック周波数を設定する場合にセット初期は100kHz
}
I2Cを使用する場合は「Wire.h」をインクルードします。begin()関数の引数の有無によってマスタで動作するかスレーブで動作するかが決まります。引数に指定した値でスレーブ動作を開始します。マスタとして使用するため引数は無しとします。
クロックは設定しなければ初期条件(ライブラリのソースコードを確認すると100kHzになっている)となります。引数として指定した値に近似した値がクロック周波数になりますが誤差が大きくなると通信エラーの原因になるため注意が必要です。
I2Cによる信号はプルアップする抵抗値にも影響するためクロックを早く設定しすぎると波形がなまってしまうこともあるので特に意識せず100kHzで十分だと思います。
Wire(I2C)の送信と実装例
Wireライブラリを使った送信の手順は以下の通りです。
- beginTransmission()関数でスレーブアドレスをセット
- write()関数で書き込み対象のアドレスをセット
- write()関数で書き込むデータをセット
- endTransmission()関数で送信
1~3まではデータの準備です。4のendTransmission()関数はスタート・コンディションの発行やコントロール・バイト(7ビットのスレーブアドレスを左詰めにして最下位ビットにWriteフラグ(0)をセット)の処理を行いデータを書き込んだ後ストップ・コンディションの発行を行います。以下に実装例を示します。
int8_t user_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット
for( uint16_t i=0; i < len; i++ ){
Wire.write(*reg_data); //lenサイズ分だけデータを書き込む
++reg_data;
}
Wire.endTransmission(); //ストップ・コンディションの発行
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt; //APIに通知するため戻り値が必要
}
BME280APIが宣言している関数であるuser_i2c_write()にWireを組み込みます。最初にbeginTransmission()関数でスレーブアドレスをセットしendTransmission()でスレーブが存在するか確認を行います。
スレーブが存在する場合は戻り値が0になるためデータの書き込み処理に移ります。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加してAPIに異常を通知します。
beginTransmission()関数でスレーブアドレスをセットします。次に書き込む対象のアドレスをwrite()関数でセットします。続けてlenのサイズに応じてwrite()関数でデータをセットします。
最後にendTransmission()関数でスタート・コンディションからストップ・コンディションまでを含めたデータを送信します。
Wire(I2C)の受信の手順と実装例
Wireライブラリを使った受信の手順は以下の通りです。
- beginTransmission()関数でスレーブアドレスをセット
- write()関数で書き込み対象のアドレスをセット
- endTransmission()関数で送信
- requestFrom()関数で対象のアドレスのデータを取得する
- read()関数で取得したデータを読み込む
1~3まではスレーブアドレスに対して書き込み対象のアドレスを指定します。4のendTransmission()関数はスタート・コンディションの発行やコントロール・バイト(7ビットのスレーブアドレスを左詰めにして最下位ビットにReadフラグ(1)をセット)の処理を行い指定した数のデータを取得し、ストップ・コンディションの発行を行います。
5は4で取得したデータを読み込みます。送信と異なる点はrequestFrom()でストップ・コンディションを発行するためendTransmission()の発行は必要ありません。以下に実装例を示します。
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len){
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
Wire.requestFrom(dev_id, len); //受信開始(スタート(ストップ)・コンディションの発行)
for( uint16_t i=0; i < len; i++ ){
*reg_data = Wire.read(); //len分だけデータをリードする
++reg_data;
}
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
BME280APIが宣言している関数であるuser_i2c_read()にWireを組み込みます。最初にbeginTransmission()関数でスレーブアドレスをセットしendTransmission()でスレーブが存在するか確認を行います。
スレーブが存在する場合は戻り値が0になるためデータの書き込み処理に移ります。スレーブが存在しない場合もしくはエラーを監視したい場合はNGの場合の処理を追加してAPIに異常を通知します。
beginTransmission()関数でスレーブアドレスをセットします。次に書き込む対象のアドレスをwrite()関数でセットします。endTransmission()関数でスレーブを書き込み対象までを選択した状態にします。
requestFrom()でスレーブから指定した数のデータを取得します。read()関数で取得したデータを読み込みます。
広告
動作確認
Seeeduino XIAOとBME280モジュールの配線例を示しています。Seeeduino XIAOは電源電圧がDC3.3Vなので直接接続することができます。
Wire(I2C)はプルアップ抵抗が必要です。Seeeduino XIAOとBME280モジュールの間それぞれにプルアップ抵抗を実装します。プルアップ抵抗が大きすぎると配線長によってはクロックが鈍ってしまい通信できないことがあるので注意が必要です。
温度(Temperature)は部屋に置いている温度計によると23.2℃であり、気圧(Pressure)はスマホを見ると1019hPa=1019mBarで湿度(Humidity)についてはスマホのデータでは49%でした。少し誤差がある気がしますが、室内のデータであることも考慮するとそれなりに計測できていると思います。
Seeeduino XIAOのI2Cを使用してセンサーの情報が取得できるようになることで他のI2C通信が可能なセンサーのデータも取得できるため応用範囲が広がりそうです。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include <Wire.h>
#include <TimerTC3.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
//application use
bme280_dev bme280main;
bme280_data sensor_data;
bool bmeinitflg = false;
int8_t cnt10ms;
int16_t timsensor = TIME_OFF;
/*** Local function prototypes */
void Bme280Init();
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len);
int8_t user_i2c_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() {
Wire.begin();
//Wire.setClock(100000);
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;
#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_i2c_write(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット
for( uint16_t i=0; i < len; i++ ){
Wire.write(*reg_data); //lenサイズ分だけデータを書き込む
++reg_data;
}
Wire.endTransmission(); //ストップ・コンディションの発行
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
/* Bme280 api use function add */
int8_t user_i2c_read(uint8_t dev_id, uint8_t reg_addr, uint8_t *reg_data, uint16_t len)
{
int8_t rslt = 0;
Wire.beginTransmission(dev_id); //スレーブが存在するか確認
byte error = Wire.endTransmission();
if( error == 0){
Wire.beginTransmission(dev_id); //スタート・コンディションの発行
Wire.write(reg_addr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
Wire.requestFrom(dev_id, len); //受信開始(スタート(ストップ)・コンディションの発行)
for( uint16_t i=0; i < len; i++ ){
*reg_data = Wire.read(); //len分だけデータをリードする
++reg_data;
}
//Wire.endTransmission();
}else{
//NGの場合(エラーを管理したい場合はここで処理する)
}
return rslt;
}
/* Bme280 sensor iniialize */
void Bme280Init(){
bme280main.dev_id = BME280_I2C_ADDR_PRIM;
bme280main.intf = bme280_intf::BME280_I2C_INTF;
bme280main.read = user_i2c_read;
bme280main.write = user_i2c_write;
bme280main.delay_ms = user_delay_ms;
Wire.beginTransmission(bme280main.dev_id);
byte error = Wire.endTransmission();
if( error == 0){
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;
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, &sensor_data, dev);
//}
return rslt;
}
上記のスケッチ例のみでは動作しません。関連記事のArduinoのWireライブラリを使ってBME280のデータを取得するで紹介している方法でBME280APIのファイルをArduinoプロジェクトファイルに追加することで動作します。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
最後まで、読んでいただきありがとうございました。