BME280 搭載、温度・湿度・気圧センサーを SPI で動かしてみた( ESP-WROOM-02 ( ESP8266 )使用)

ESP8266 ( ESP-WROOM-02 )

5.スケッチの入力

それでは Arduino IDE のスケッチはこのようになります。
これは、スイッチサイエンスさんのBME280使い方ページにあるI2C通信用ブログラムを独自にSPI通信用に書き換えたものです。
ライブラリは一切使用しません。Arduino IDE 標準のSPIライブラリも使わずに動作します。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

const uint8_t sclk = 14;
const uint8_t mosi =13; //Master Output Slave Input ESP8266=Master,BME280=slave 
const uint8_t miso =12; //Master Input Slave Output
const uint8_t cs_bme = 16; //CS pin

uint32_t hum_raw, temp_raw, pres_raw;
 int32_t t_fine;

uint16_t dig_T1;
 int16_t dig_T2;
 int16_t dig_T3;
 
uint16_t dig_P1;
 int16_t dig_P2;
 int16_t dig_P3;
 int16_t dig_P4;
 int16_t dig_P5;
 int16_t dig_P6;
 int16_t dig_P7;
 int16_t dig_P8;
 int16_t dig_P9;
 
uint8_t  dig_H1;
 int16_t dig_H2;
uint8_t  dig_H3;
 int16_t dig_H4;
 int16_t dig_H5;
 int8_t  dig_H6;

double SeaLevelPressure_hPa = 1009.4; //標準は1013.25

//*********************セットアップ********************************
void setup() {
  Serial.begin(115200);
  
  uint8_t t_sb = 5; //stanby 1000ms
  uint8_t filter = 0; //filter O = off
  uint8_t spi3or4 = 0; //SPI 3wire or 4wire, 0=4wire, 1=3wire
  uint8_t osrs_t = 4; //OverSampling Temperature x4
  uint8_t osrs_p = 4; //OverSampling Pressure x4
  uint8_t osrs_h = 4; //OverSampling Humidity x4
  uint8_t Mode = 3; //Normal mode

  uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | Mode;
  uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3or4;
  uint8_t ctrl_hum_reg  = osrs_h;
  
  pinMode(sclk, OUTPUT);
  pinMode(miso, INPUT);
  pinMode(mosi, OUTPUT);
  pinMode(cs_bme, OUTPUT);

  digitalWrite(cs_bme, HIGH);
 
  writeReg(0xF2,ctrl_hum_reg);
  writeReg(0xF4,ctrl_meas_reg);
  writeReg(0xF5,config_reg);
  delay(1000);
  
  readTrim();
  Serial.println();
}
//******************メインループ************************************
void loop() {
  double temp_act = 0.0, press_act = 0.0, hum_act = 0.0, altitude_act = 0.0;
  int32_t temp_cal;
  uint32_t press_cal,hum_cal;
  
  readData();

  temp_cal = calibration_T(temp_raw);
  press_cal = calibration_P(pres_raw);
  hum_cal = calibration_H(hum_raw);
  temp_act = (double)temp_cal / 100.0;
  press_act = (double)press_cal / 100.0;
  hum_act = (double)hum_cal / 1024.0;
  altitude_act = ReadAltitude(SeaLevelPressure_hPa, press_act);
  
  Serial.println("-----------------------");
  Serial.print("Temperature = "); Serial.print(temp_act); Serial.println(" *C");
  Serial.print("Humidity = "); Serial.print(hum_act); Serial.println(" %");
  Serial.print("Pressure = "); Serial.print(press_act); Serial.println(" hPa");
  Serial.print("Altitude = "); Serial.print(altitude_act); Serial.println(" m");

  delay(2000);
}
//*****************初期値読み込み**************************************
void readTrim(void) {
    dig_T1 = read16bit(0x88);
    dig_T2 = (int16_t)read16bit(0x8A);
    dig_T3 = (int16_t)read16bit(0x8C);

    dig_P1 = read16bit(0x8E);
    dig_P2 = (int16_t)read16bit(0x90);
    dig_P3 = (int16_t)read16bit(0x92);
    dig_P4 = (int16_t)read16bit(0x94);
    dig_P5 = (int16_t)read16bit(0x96);
    dig_P6 = (int16_t)read16bit(0x98);
    dig_P7 = (int16_t)read16bit(0x9A);
    dig_P8 = (int16_t)read16bit(0x9C);
    dig_P9 = (int16_t)read16bit(0x9E);

    dig_H1 = read8bit(0xA1);
    dig_H2 = (int16_t)read16bit(0xE1);
    dig_H3 = read8bit(0xE3);
    dig_H4 = (int16_t)((read8bit(0xE4) << 4) | (read8bit(0xE5) & 0x0F));
    dig_H5 = (int16_t)((read8bit(0xE6) << 4) | (read8bit(0xE5) >> 4));
    dig_H6 = (int8_t)read8bit(0xE7);
}
//***************BME280へ初期レジスタ書き込み関数****************************
void writeReg(uint8_t reg_address, uint8_t data) {
  digitalWrite(cs_bme, LOW);
  SpiWrite(reg_address & B01111111); // write, bit 7 low
  SpiWrite(data);
  digitalWrite(cs_bme, HIGH);
}
//***************BME280からの温度、湿度、気圧データ読み込み関数****************************
void readData() {

  uint32_t data[8];
  uint8_t i;

  digitalWrite(cs_bme, LOW);
  SpiWrite(0xF7 | B10000000); //0xF7 pressure msb read, bit 7 high
  for(i=0; i<8; i++){
    data[i] = SpiRead();
  }
  digitalWrite(cs_bme, HIGH);
  pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4); //0xF7, msb+lsb+xlsb=19bit
  temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4); //0xFA, msb+lsb+xlsb=19bit
  hum_raw  = (data[6] << 8) | data[7];  //0xFD, msb+lsb=19bit(16:0)
}
//***************温度キャリブレーション関数****************************
int32_t calibration_T(int32_t adc_T) {
    int32_t var1, var2, T;
    var1 = ((((adc_T >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((adc_T >> 4) - ((int32_t)dig_T1)) * ((adc_T>>4) - ((int32_t)dig_T1))) >> 12) * ((int32_t)dig_T3)) >> 14;
    
    t_fine = var1 + var2;
    T = (t_fine * 5 + 128) >> 8;
    return T; 
}
//***************気圧キャリブレーション関数****************************
uint32_t calibration_P(int32_t adc_P) {
    int32_t var1, var2;
    uint32_t P;
    var1 = (((int32_t)t_fine)>>1) - (int32_t)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11) * ((int32_t)dig_P6);
    var2 = var2 + ((var1*((int32_t)dig_P5))<<1);
    var2 = (var2>>2)+(((int32_t)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2)*(var1>>2)) >> 13)) >>3) + ((((int32_t)dig_P2) * var1)>>1))>>18;
    var1 = ((((32768+var1))*((int32_t)dig_P1))>>15);
    if (var1 == 0) {
        return 0;
    }    
    P = (((uint32_t)(((int32_t)1048576)-adc_P)-(var2>>12)))*3125;
    if(P<0x80000000) {
       P = (P << 1) / ((uint32_t) var1);   
    }else{
        P = (P / (uint32_t)var1) * 2;    
    }
    var1 = (((int32_t)dig_P9) * ((int32_t)(((P>>3) * (P>>3))>>13)))>>12;
    var2 = (((int32_t)(P>>2)) * ((int32_t)dig_P8))>>13;
    P = (uint32_t)((int32_t)P + ((var1 + var2 + dig_P7) >> 4));
    return P;
}
//***************湿度キャリブレーション関数****************************
uint32_t calibration_H(int32_t adc_H) {
    int32_t v_x1;
    
    v_x1 = (t_fine - ((int32_t)76800));
    v_x1 = (((((adc_H << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1)) + 
              ((int32_t)16384)) >> 15) * (((((((v_x1 * ((int32_t)dig_H6)) >> 10) * 
              (((v_x1 * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) * 
              ((int32_t) dig_H2) + 8192) >> 14));
   v_x1 = (v_x1 - (((((v_x1 >> 15) * (v_x1 >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
   v_x1 = (v_x1 < 0 ? 0 : v_x1);
   v_x1 = (v_x1 > 419430400 ? 419430400 : v_x1);
   return (uint32_t)(v_x1 >> 12);   
}
//***************標高計算関数****************************************************
double ReadAltitude(double SeaLevel_Pres, double pressure) {
  double altitude = 44330.0 * (1.0 - pow(pressure / SeaLevel_Pres, (1.0/5.255)));  
  return altitude;
}
//***************BME280から16bitデータ読み込み関数****************************
uint16_t read16bit(uint8_t reg) {
  uint16_t d1, d2;
  uint16_t data;
  digitalWrite(cs_bme, LOW);
  SpiWrite(reg | B10000000); // read, bit 7 high
  d1 = SpiRead();
  d2 = SpiRead();
  data = (d2 << 8) | d1;
  digitalWrite(cs_bme, HIGH);
  return data;
}
//***************BME280から8bitデータ読み込み関数****************************
uint8_t read8bit(uint8_t reg) {
  uint8_t data;
  digitalWrite(cs_bme, LOW);
  SpiWrite(reg | B10000000); // read, bit 7 high
  data = SpiRead();
  digitalWrite(cs_bme, HIGH);
  return data;
}
//***************BME280へSPI信号データ送信関数****************************
void SpiWrite(uint8_t data) {
  for (int i=7; i>=0; i--) {
    digitalWrite(sclk, LOW);
    digitalWrite(mosi, data & (1<<i));
    digitalWrite(sclk, HIGH);
  }
}
//***************BME280からのSPI信号データ読み込み関数****************************
uint8_t SpiRead() {
  uint8_t r_data = 0;
  for (int i=7; i>=0; i--) {
    r_data <<= 1;
    digitalWrite(sclk, LOW);
    digitalWrite(mosi, LOW);
    digitalWrite(sclk, HIGH);
    if(digitalRead(miso)){
      r_data |= 1;
    }
  }
  return r_data;
}

●1-4行:
ESPr Developer ( ESP-WROOM-02 ( ESP8266 ))側のGPIOピンを設定してます。

●6-28行:
温度、湿度、気圧センサーのキャリブレーション(校正)用の変数宣言です。これは、BME280のデータシートに型宣言方法が書いてあります。
下図の様にデータシートのTable 16 にあります。

符号有り(signed)と符号なし(unsigned)変数があるので注意してください。
short はArduinoでは16bitになります。私の場合はshort型やchar型宣言は何ビットか分からない場合があるので、極力 uint16_t という宣言にしてます。

●30行:
標高計算のための海面(海抜0m)気圧変数です。
後で述べますが、海面気圧というのは頻繁に変動しますので、気象庁のアメダス表形式ページを参照して、近辺の海面気圧を入力します。

●36-42行:
ここはスイッチサイエンスさんのBME280使い方ページを参照して、値を決めます。
filter はオフ以外でもあまり変化なかったのでオフで良いと思いました。
オーバーサンプリングはさらに高い周波数でサンプリングするかどうかで精度が変わりますが、その分計算に時間がかかります。オーバーサンプリングが1よりか4くらいにすると、手持ちの温度湿度計に近い値が出たのでそうしました。

●44-46行:
この変数設定はBME280のデータシートに設定方法があります。

36-42行で初期化した値をビットシフトして、それぞれ8bitに収めます。

●48-53行;
GPIOピンをそれぞれ設定しておきます。
53行で予めCS ( SS) ピンをHIGHにしておかないとうまく動作しませんでした。
BME280 にデータを書き込む場合はこのCSピンをLOWにします。

●55-57行:
44-46行で設定したフィルターやオーバーサンプリングなどの値をBME280へSPI通信で書き込みます。書込みは111-116行の関数を呼び出します。

●60行:
ここで88-109行の関数を呼び出して、BME280のフラッシュメモリにある補正データを呼び出して、9-28行の変数を初期化します。

●64-86行:
ここがメインループです。
69行で118-132行の関数を呼び出して、温度、湿度、気圧のセンサー値を一括して呼び出します。
呼び出したセンサー値のtemp_raw, pres_raw, hum_raw を134-185行のキャリブレーション関数にそれぞれ代入して、実際に理解できる数値に置き換えます。
それをシリアルモニターに表示させます。
ちなみに、Serial.printf関数はdouble型やfloat型は表示してくれませんので、このような形でプログラミングするしかありませんでした。

●88-109行:
BME280のメモリに入っている初期構成データを読み出します。それぞれ符号有りの型と符号なしの型があるので、左辺等辺で型を合わせる必要があります。
これの抽出方法はデータシートのTable16にあります。

●111-116行:
SPI通信でBME280にデータを書き込む関数です。
書き込む場合はCS ( SS )ピンをLOWにして、BME280のレジスタアドレスを書き込むのですが、書き込みの場合はレジスタアドレスの7bit(8番目のビット)をゼロにする必要があります。
それはデータシートの以下の部分に記載されています。

というわけで、113行にあるように01111111を&演算してやれば7bitをゼロにすることができます。
レジスタアドレスを書き込んだら、即該当データを送ります。
送り終わったらCSピンをHIGHにします。

●118-132行:
BME280から一括して温度・湿度・気圧のセンサー値を読み込む関数です。
読み込む前にSPI通信で読み取りたいレジスタアドレスを書き込んでからその後SDOからセンサー値が送出されます。
124行目では0xF7というレジスタアドレスを読み取りモードでBME280に書き込みます。読み取りモードは7bitを1にします。
そのレジスタを書き込むと、それ以降のセンサー値がBME280のSDOから順次送出されます。
つまり、press_msb, press_lsb, …..hum_msb, hum_lsb という感じでセンサー値を8バイト連続でSDOから送信してきます。それをESP-WROOM-02側で読み取るわけです。
ですから、書き込みレジスタはその最初のアドレス(0xF7)で良いということです。
送られてくる順番はこの表をみると分かります。

●133-180行:
これは得られたセンサー値を計算式に入れることによって人間が理解できる温度、湿度、気圧の値に変換できることになります。
この式はBME280のデータシートの最後の方に下図の様に丸ごとそのままそっくり掲載されていますので、それをそのままArduino スケッチに当てはめただけです。

●182-185行:
気圧から標高を計算する関数です。
これはネットのどこかのページにありましたが、Adafruitライブラリにもありました。
SeaLevel_Presは海面気圧(海抜0m気圧)をhPa単位で入力して、pressure値はBME280で測定した値を入力すれば標高が出てきます。
しかし、あまりこの式では正確ではありません。
海面気圧の求め方は後述します。

●187-197行:
BME280から16bit分のデータを読み取る関数です。
読み取りモードですからレジスタを書き込む際には7bitを1にします。
194行目が注意ですが、後で読み込んだバイトが上位ビットになるので注意です。

●199-206行:
BME280から8bit分のデータを読み取る関数です。

●208-214行:
レジスタアドレスをSPI通信で送信する関数です。
spi.hライブラリを使わないでもこうすることによって送信できます。

●216-228行:
ESP-WROOM-02 ( ESP8266 )のmosiピンからデータを読み取って、バイトに変換する関数です。

6.アメダス表形式を使って海面気圧(海抜0m気圧、又は海面更正気圧)を入力する。

では、気圧から標高を計算するために、30行目のSeaLevelPressure_hPaに値を入力してみましょう。

ついこの前に知ったのですが、以下のことが正確でなかったらスミマセン。
海面気圧とは正確には海面更正気圧というらしいです。
つまり、標高が明らかになっている場所で測った気圧や温度の値からある計算式を使えば、標高0mの気圧の近似値が求められるらしいです。
例えば、こちらのサイトには標高と気温と気圧を入力すると海面気圧が表示されます。

ただ、それだと、BME280の気圧値が正確でないと、入力しても意味無いですよね。
そこで、気象庁のアメダス表形式ページでは、実はご自分の住んでいる地域近辺の海面気圧が分かるんです。
(※気象庁のホームページは2021/2/24にリニューアルしました)

例えば、東京を例にとって、アメダス表形式を表示してみます。
まず、気象庁「アメダス表形式ページ」を開くと下図のように表示されるので、マウスホイールか左下の拡大ボタンでターゲットの地域を拡大していきます。


(この図は気象庁「アメダス表形式ページ」を加工して作成したものです)

そして、例えば東京をクリックすると、下図のようにポップアップが出るので、その中の下図の様な箇所をクリックします。


(この図は気象庁「アメダス表形式ページ」を加工して作成したものです)

すると、下図の様な表が出てきます。
(気圧を測っているところは気象台のある地域と測候所がある地域だけです。)


(この図は気象庁「アメダス表形式ページ」を加工して作成したものです)

東京の場合はこのように「現地気圧」と「海面気圧」が表示されているので、東京のど真ん中に住んでいる人は現地気圧をチェックすれば、BME280の気圧校正につかえそうですね。
私の住んでいる田舎では残念ながら「海面気圧」さえ表示されませんでしたが、、、。
「現地気圧」は2021/2/24に気象庁ホームページがリニューアルしてから表示されるようになったみたいです。

以前は「気圧」としか表示されておらず、それは実は海面気圧だったので、よく混乱しました。
気圧としか記載が無い場合は、海面気圧なので注意してください。
アメダス表形式の一番下に注意書きがありました。


(この図は気象庁「アメダス表形式ページ」を加工して作成したものです)

なぜ海面気圧なのかというと、どうやら天気図を書く時に、海からの等圧線を描くとなると、すべて同じ標高で考えねばならないからのようです。
なるほど・・・。
もう忘れてしまいましたが、昔、中学校の理科か何かで習ったような習ってないような・・・。

ということで、ご自分の住んでいる地域に近い気象台の海面気圧をhPa単位で入力してください。
海面気圧と、気圧センサBME280の気圧値が分かれば、標高が計算できます。

7.気圧からの標高を計算することについて

前章で気象庁のアメダス表形式ページから取得したその地点の海面気圧(海面更正気圧)と、気圧センサで取得した現地気圧、そして現地気温がわかれば、その地点のおおよその標高が計算できます。

海面気圧:p0
現地気圧:p
現地気温:t

とすると、下図の様な計算式になるようです。
(参考サイト:CASIO 高精度計算サイト

これをC言語で書くと以下のようになります。

double altitude; //標高
altitude = ((pow((p0 / p), (1.0 / 5.257)) - 1.0) * (t + 273.15)) / 0.0065;

この計算式を使って、実際に、気象庁のアメダス表形式ページから東京の海面気圧と現地気圧を取得して、Arduinoスケッチで以下のように入力してみます。

//東京の標高計算
void setup() {
  Serial.begin(115200);
  Serial.println();

  double p0 = 1023.2; //海面気圧
  double p = 1020.2; //現地気圧
  double t = 7.2; //現地気温
  double altitude; //標高

  altitude = ((pow((p0 / p), (1.0 / 5.257)) - 1.0) * (t + 273.15)) / 0.0065;
  Serial.println(altitude);
  //東京の測候所の標高は25m
}

void loop() {
}

すると、以下のようになりました。

標高が24.10m という結果でした。
気象庁のアメダス表形式ページの地点の標高は25mでした。
東京の地点ではなく、その測候所の標高というところに注意してください。

やはり誤差はありますが、なかなか良い結果です。
4章のスケッチ(プログラムソースコード)をこれに変えても良いですね。
いずれにしても、おおよその値ということは把握しておいた方が良いです。

ところで、自分の地点の性格な標高を知りたい場合は、国土地理院の以下のサイトが良いです。

地理院地図Webサイト

例えば、東京駅の標高を調べるには、下図のようにマウススクロール等で拡大していき、ウィンドウの中心の十字印を調べたいターゲット地点に合わせます。
すると、ウィンドウの下部に下図のように標高が表示されると思います。


ここでは、東京駅の標高は3.3mと表示されました。
やはり、先ほど述べたように、気象庁のアメダス表形式ページの東京の標高表示は測候所の標高というところに注意ですね。

公なサイトはなかなか便利なものがあるんですね~。

8.コンパイル書き込みおよび実行する

では、いよいよ、コンパイル書き込みして、実行させてみます。
起動当初のシリアルモニターはこんな感じで表示されました。

別の温度・湿度計の値はこんな感じです。

立ち上げ当初は湿度に誤差がありますね。3~4%くらいの誤差です。

しかし、立ち上げて15分ほど経過するとシリアルモニターはこうなります。

温度・湿度計の値はこうなってます。

どうでしょうか。ほとんど誤差が無くなってます。オーバーサンプリングを4にするとかなり正確ですね。1だと湿度が2%くらい誤差がありました。

ちなみに、私の住んでいるところの標高は66mほどのところです。
近くの気象台の海面気圧を入力しても10m以上の誤差があります。これはなかなか正確とはいかないですね。仕方のないところです。
標高をより正しく表示し続けるためには、当然のことなが常に変化する海面気圧を毎回入力してコンパイルし直さなければいけません。
Webから自動取得して海面気圧を校正していくこともできなくはないのですが、これを持って登山するわけではないので、そこまでしなくてもいいのかなぁと思ってます。

ちなみに、話は変わりますが、私の使っている温度・湿度計は薄くて軽くて意外と正確でとても重宝しています。

自立することもできるのでなかなか使い勝手が良いのでおすすめです。

EMPEX の TD-8173 というものです。Amazon.co.jpではこんな感じで売ってます。

EMPEX (エンペックス) デジカード(デジタル温・湿度計) TD-8173
EMPEX (エンペックス)
¥2,980(2024/04/25 15:35時点)

以上です。

いかがでしょうか。
BME280 を調べているうちに気象庁や気圧についてまである程度学習できて個人的に面白かったです。

さて、次回はこのデータをフルカラー有機ELディスプレイに表示させたり、グラフ表示させたりしてみようと思います。
さぁ、そろそろオリンピック間近ですね。いろいろ楽しみです。

ではまた・・・。

コメント

  1. かいじ より:

    この取得したデータ(温度、湿度、気圧、高度)をsdに保存したいのですが、どうすればいいですか? 

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