第35回 温湿度・気圧センサ(BME280) 〜データ取得プログラム〜

センサ(BME280)のデータシートを元に、温湿度・気圧データの取得プロクグラムを完成させます。

測定データ取得の流れ

温湿度・気圧センサBME280の概要の記事で説明しましたが、もう一度測定データ取得の流れを確認します。なお、以前の記事で同じような図を説明しましたが、SPI通信手順の関係から、一部の処理手順をまとめてあります。

Pic practice 35 data process

この図のように電源投入後、(1)動作設定を行い、(2)補正データを取得します。この処理は電源投入後1回だけ行います。

次に測定の度に、(3)測定指示を行い、(4)測定データを取得して、(5)データ補正をします。

この後、この処理ごとにプログラムを解説します。なお、データ取得のプログラムはほとんどがデータシートの通りに作成することになります。そのため、プログラムの作成よりデータシートを読み解くことが重要、というか結構大変な作業なので、心が折れそうになった時でも前に進む気持ちが一番大切です。

 

読み込み方法

ところでセンサのデータを読み込む方法ですが、以前説明したようにSPI通信でアドレス(1バイト)を送信した後クロックを8ビット分発生すると、そのアドレスのデータ(1バイト=8ビット)を返信してくれます。

さらにこの後クロックを発生し続けると、その次のアドレスのデータを返信してくれます。

これまでの記事で、指定したアドレスの1バイトを読み込む関数を作成しましたが、2バイト(16ビット)の読み込みも多用しますので、2バイト読み込み関数も作成しておきます。

また、動作設定は特定のアドレスに1バイトのデータを書き込むことになりますので、1バイトデータを送信する関数も作成しておきます。

 

SPI関数の追加

それでは、2バイト(16ビット)を読み込む関数を作成します。読み込みたいアドレスの1バイトを送信するとそのアドレスの1バイトのデータを読み込むことができ、さらにクロックを発生すると次のアドレスのデータを読み込むことができます。なお、取得した2バイトは、最初の1バイトが上位バイト、次の1バイトが下位1バイトになります。

1バイト読み込み関数を流用して以下のように作成しました。(すみません、表示不具合があります。この関数は最後のプログラムでは正常に表示されています)

//
// SPIデータを指定アドレスから2バイト読み込み
//
uint16_t spiRead2BytesData(uint8_t address) {

uint8_t data_low, data_high; // 読み込んだ1バイトデータ格納用
uint16_t data; // 2バイトデータ用

// チップセレクトを0にしてセンサモジュールとの通信開始
SPI_CSB = 0;

// 読み込みデータアドレス指定
spiSend8bit(address);

// 指定アドレスとその次のアドレスのデータを2バイト読み込み
data_low = spiReceive8bit();
data_high = spiReceive8bit();

//16ビットデータにする
data = (data_high << 8) | data_low; // チップセレクトを1にして通信終了 SPI_CSB = 1; return data; }

また、1バイト書き込む場合は、アドレスの1バイトとデータの1バイトを連続して送信すればOKです。なお、書き込む際のアドレスは、最上位ビットを0にします。つまり読み込みアドレスに対して0b01111111と&をとればOKです。以下のように作成してみました。

//
// SPIデータを指定アドレスに1バイト書き込み
//
void spiWrite1ByteData(uint8_t address, uint8_t data) {

    // チップセレクトを0にしてセンサモジュールとの通信開始
    SPI_CSB = 0;

    // アドレス指定(書き込みは最上位ビット0)
    spiSend8bit(address & 0b01111111);

    // データ書き込み
    spiSend8bit(data);

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;
  
}

 

(1)動作設定プログラム

動作設定は、BME280メモリマップの以下のメモリに設定値を書き込むことにより行います。

Pic practice 35 memory map config

動作設定のメモリは「config」「ctrl_meas」「ctrl_hum」の3個で、それぞれのアドレスの設定内容は以下のようになっています。

Pic practice 35 config register

動作設定のメモリは3個(3バイト)ですが、これらはさらに細かく分かれています。それぞれの設定値を詳しく説明すると長くなってしまいますので、必要な内容をなるべく簡潔に説明します。(とはいっても長くなりますが...)

t_sb(スタンドバイ時間)

このセンサは、温湿度・気圧データの測定タイミングとして2つのモードがあります。

ひとつは、一定の時間間隔で連続して測定を行うモードです。例えば500ms毎に測定する、などです。この場合、センサは(勝手に)500ms毎に継続して測定を行います。PICマイコン側は任意のタイミングでデータを取得でき、その時取得できるデータは最新の測定したデータになります。このセンサでは、この時間間隔のことを「スタンドバイ時間」と呼んでいるようです。

もうひとつは、普段はスリープモードになっていて、PICマイコンから測定指示した時に測定する、というモードです。PICマイコンから測定指示をすると、このセンサはスリープモードから動作モードに移行して温湿度・気圧を測定後、すぐにスリープモードに戻ります。なお、スリープモードでも測定したデータの取得はできます。今回はこのモードで測定を行います。

t_sbは連続測定モードの時間間隔設定で、以下の値を取ります。なお、連続測定モードを使用しない場合はこの設定は無効になります。デフォルトの0を指定しておきます。

設定値 設定内容(単位:ms)
000 0.5
001 62.5
010 125
011 250
100 500
101 1000
110 10
111 20

filter(フィルタ)

どのようなセンサもノイズの影響を受けます。そのため同じ環境下、例えば気圧が全く変わらない室内で測定した場合でも、測定値は若干のばらつきがあります。

そこで、測定したデータをそのまま使用するのではなく、今までの測定値と今回の測定値の平均をとってノイズの影響を減らす手法があります。株式投資やFXをされている方であれば「移動平均線」をご存知ではないでしょうか。移動平均線は、過去何日分かの価格の平均値を求めることにより、変動を滑らかにする手法です(=ノイズを減らしている)。

このセンサのfilter設定もそれと同じです。ただ、計算方法は株式投資やFXでいうところの指数移動平均線に近い計算を行なっています。

このfilter設定は、過去データをどのぐらい重要視するか、という設定で、数値が大きいほど過去のデータを重視してノイズの影響をより低減することができます。ただ数値が大きいほど今回の測定値はすぐに反映されなくなりますので、測定値自体の精度は落ちます。

設定値 設定内容
000 OFF
001 2
010 4
011 8
上記以外 16

データシートによると、環境測定の場合はfilter設定はOFFが推奨されています。今回はOFFで使用します。

spi3w_en

今までのSPI通信手順では、通信線は4本で説明してきましたが、3本線を使用する通信手順もあります。その場合はMOSIとMISO信号線を1本で実現することになります。通信手順は4本線の場合とほぼ同じです。ただ、3本線はあまり使用されていませんので説明は省略いたします。

spi3w_enはSPI通信の信号線数を指定します。以下の値を取ります。

設定値 設定内容
0 4線式
1 3線式

4線式で使用しますので、0を設定します。

osrs_t / osrs_p / osrs_h(オーバーサンプリング)

先ほどのfilter設定は、ノイズの影響を低減するために「測定値」の平均値を取るものでした。ノイズを低減する方法として、他の手法があります。

応用編で「ADコンバータ」を説明しました。このセンサも温湿度・気圧センサ自体はアナログ値のため、センサ内部でADコンバータを使用してデジタル値に変換しています。

ADコンバータでアナログ値をデジタル値に変換する時、通常は1回測定してそのデータを使用します。ただ、1回の測定ではノイズの影響を受けている可能性が高いため、より短い時間間隔で測定してノイズの影響を減らす方法があります。この方法は「オーバーサンプリング」と呼ばれています(数学的にはかなり難しい内容のため、概念のみの説明にとどめます)。

orsr_t/osrs_p/osrs_h設定は、どのくらい頻度を多くして測定するか(頻度を何倍にするか)、という設定で、以下の値をとります。「_t」は温度用、「_p」は気圧用、「_h」は湿度用のオーバーサンプリング設定です。

なお、この設定で「測定しない」という選択もできます。測定そのものは電力を消費しますので、電池を長持ちさせたい場合は必要なければ測定をスキップすることができます。

設定値 設定内容
000 測定しない
001 x1
010 x2
011 x4
100 x8
上記以外 x16

データシートによると、環境データ測定の場合で測定頻度が低い場合はオーバーサンプリング設定はx1で十分、とのことで、設定はx1にしてみます。

mode(動作モード)

センサの測定モードを指定します。センサの動作モードは「スリープモード」「連続測定モード」「1回測定モード」の3種類あります。

なお先ほど説明しましたが、電源投入後はスリープモードの状態で、「1回測定モード」に設定した場合、温湿度・気圧データ測定を行い、そのあと自動的にすぐにスリープモードに戻ります。

設定値 設定内容
00 スリープモード
01と10 1回測定後スリープモードに戻る
11 連続測定モード

以上の内容から、メモリの必要なところに設定値を書き込むプログラムを作成します。なお、設定値は以下のようにしました。(データシートの推奨値にしています)

0

設定項目 設定値 設定内容
t_sb 0 測定時間間隔設定は使用しないため0を設定
filter 0 フィルタは使用しない
spi3w_en SPIは4線式
osrs_t 1 温度オーバーサンプリングはx1
osrs_p 1 気圧オーバーサンプリングはx1
osrs_h 1 湿度オーバーサンプリングはx1
mode 0 スリープモード
//
// BME280初期化関数
//
void bme280Initialization(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式(=0)
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 0;  // スリープモード

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // センサ処理待ち
    __delay_ms(1000);

    // 補正値読み込み
    //   センサごとに固定ちのため、初期化時のみ読み込む
    bme280ReadTrimmingParameters();

}

 

(2)補正データ取得プログラム

ここからはデータシートに従ってプログラムを作成することになります。中身の詳細まではデータシートに説明されていませんので、ちょっと気持ち悪いですが中身がよくわからないままのプログラムとなります。

測定データを取得した後、補正データで補正する必要があります。補正データはセンサ個体ごとに異なりますが、あるセンサの補正データは固定で変わりませんので、電源投入後に1度取得したら、そのまま保存しておきます。そのため、補正データはグローバル変数で宣言しておきます。

補正データはメモリマップの以下のアドレスに保存されています。

Pic practice 35 calibration address

補正データはcalib00〜calib41の合計42バイトあります。この42バイトですが、データシートでは「以下のように変数に読み込んでおいてください」と説明がありますので、この通りにします。(中身がわからないのでこの通りにするしかない)

Pic practice 35 calibration data structure
(BOSCH社「BME280データシート」より引用・加工)

ちょっと表現がわかりづらいですが、例えば最初のアドレスにあるdig_T1はデータタイプはunsigned shourt (符号なし16ビット)で宣言しますが、

0x88/0x89  dig_T1[7:0] / [15:8]

というのは、0x88番地のデータをdig_T1の第7ビット〜第0ビット、つまり下位1バイトに格納し、0x89番地のデータをdig_T1の第15ビット〜第8ビット、つまり上位1バイトに格納しなさい、という意味になります。データシートからこの辺りを読み取るのも心が折れそうになるところです。

ということで、この表に従って以下のようにグローバル変数を宣言します。

// BME280気温補正データ
uint16_t dig_T1;
int16_t  dig_T2;
int16_t  dig_T3;

// BME280湿度補正データ
uint8_t  dig_H1;
int16_t  dig_H2;
uint8_t  dig_H3;
int16_t  dig_H4;
int16_t  dig_H5;
int8_t   dig_H6;

// BME280気圧補正データ
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;

次にこれらのデータを指定されたアドレスから読み取ればよいので以下のようにプログラムを作成します。

//
// 補正データ読み込み
//
void bme280ReadTrimmingParameters(void) {

    // 気温データ用補正データ
    dig_T1 = spiRead2BytesData(0x88);
    dig_T2 = (int16_t)spiRead2BytesData(0x8A);
    dig_T3 = (int16_t)spiRead2BytesData(0x8C);

    // 気圧データ用補正データ用補正データ
    dig_P1 = spiRead2BytesData(0x8E);
    dig_P2 = (int16_t)spiRead2BytesData(0x90);
    dig_P3 = (int16_t)spiRead2BytesData(0x92);
    dig_P4 = (int16_t)spiRead2BytesData(0x94);
    dig_P5 = (int16_t)spiRead2BytesData(0x96);
    dig_P6 = (int16_t)spiRead2BytesData(0x98);
    dig_P7 = (int16_t)spiRead2BytesData(0x9A);
    dig_P8 = (int16_t)spiRead2BytesData(0x9C);
    dig_P9 = (int16_t)spiRead2BytesData(0x9E);

    // 湿度データ用補正データ用補正データ
    dig_H1 = spiRead1ByteData(0xA1);
    dig_H2 = (int16_t)spiRead2BytesData(0xE1);
    dig_H3 = spiRead1ByteData(0xE3);
    dig_H4 = (int16_t)((spiRead1ByteData(0xE4) << 4) | (spiRead1ByteData(0xE5) & 0x0F));
    dig_H5 = (int16_t)((spiRead1ByteData(0xE6) << 4) | (spiRead1ByteData(0xE5) >> 4));
    dig_H6 = (int8_t)spiRead1ByteData(0xE7);

}

 

(3)測定指示プログラム

測定指示を行うには、(1)動作設定プログラムのmodeを0b01に設定すればOKです。

(1)の関数を流用して以下のようにプログラムを作成しました。

なお、測定指示をしてから測定が完了するまで数ms程度時間がかかるようです。BME280のレジスタにはStatusレジスタがあり、このレジスタを監視していると測定が完了したかどうかわかるようになっています。ただ、while文でこのレジスタを監視するようにプログラムしたのですが、どうしてもうまく動かなかったため、10ms時間待ちするようにプログラムしてあります。

//
// 温湿度・気圧データ測定指示
//
void bme280ForcedMeasurement(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 1;  // 測定指示(1回測定したらスリープモードに移行)

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // 測定待ち
    //   statusレジスタを監視して測定完了を判断すべきだが
    //   while文での動作ができなかったので時間待ちで代用
    __delay_ms(10);
    
    // この段階でBME280の温湿度大気圧レジスタに測定値が格納されている
    // このあと自動的にスリープモードに入る


}

 

(4)測定データ取得プログラム

1回の測定指示をした後は、以下のメモリに測定したデータが格納されています。

Pic practice 35 measured data

SPI通信でこれらのメモリアドレスのデータを読み取るだけですので、特に難しいところはないと思います。以下のようにプログラムを作成しました。メモリが連続していますので、最初のアドレス(0xF7)を指定した後、一気に8バイトを読み取っている点に注意してください。

なお、press_raw, temp_raw, hum_rawはuint32_t型としてグローバル変数宣言しています。あとで補正関数で補正計算するためです。

// グローバル変数
//
// BME280温湿度・気圧読み取りデータ
uint32_t hum_raw, temp_raw, pres_raw;

// 
// 補正前の生の温湿度・気圧データ読み込み
// 
void bme280ReadMeasuredRawData() {

    // データ読み取り用配列
    uint32_t data[8];

    // スレーブセレクトアクティブ
    SPI_CSB = 0;

    // 読み込み開始アドレス指定
    spiSend8bit(0xF7 | 0b10000000);

    // 8バイト分のデータ読み込み
    for(int8_t i=0; i<8; i++){
      data[i] = spiReceive8bit();
    }

    // スレーブセレクトインアクティブ
    SPI_CSB = 0;

    // 読み込みしたデータから気温、湿度、気圧データを生成
    pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    hum_raw  = (data[6] <<  8) | data[7];

}

 

(5)補正プログラム

最後に、取得した補正データを補正するプログラムです。データ補正方法についてはデータシートにCのサンプルコードがありますので、それを参考に作成します。

まず、データシートに記載されている温度の補正関数です。

// Returns temperature in DegC, resolution is 0.01 DegC. Output value of “5123” equals 51.23 DegC.
// t_fine carries fine temperature as global value
BME280_S32_t t_fine;

BME280_S32_t BME280_compensate_T_int32(BME280_S32_t adc_T)
{
    BME280_S32_t var1, var2, T;
    var1 = ((((adc_T>>3) – ((BME280_S32_t)dig_T1<<1))) * ((BME280_S32_t)dig_T2)) >> 11;
    var2 = (((((adc_T>>4) – ((BME280_S32_t)dig_T1)) * ((adc_T>>4) – ((BME280_S32_t)dig_T1))) >> 12) *
((BME280_S32_t)dig_T3)) >> 14; t_fine = var1 + var2;
    T =(t_fine*5+128)>>8;

    return T;
}

(BOSCH社「BME280データシート」より引用)

次に、湿度の補正関数です。

// Returns humidity in %RH as unsigned 32 bit integer in Q22.10 format (22 integer and 10 fractional bits).
// Output value of “47445” represents 47445/1024 = 46.333 %RH
BME280_U32_t bme280_compensate_H_int32(BME280_S32_t adc_H)
{
    BME280_S32_t v_x1_u32r;

    v_x1_u32r = (t_fine – ((BME280_S32_t)76800));
    v_x1_u32r = (((((adc_H << 14) – (((BME280_S32_t)dig_H4) << 20) – (((BME280_S32_t)dig_H5) * v_x1_u32r)) +
        ((BME280_S32_t)16384)) >> 15) * (((((((v_x1_u32r * ((BME280_S32_t)dig_H6)) >> 10) * (((v_x1_u32r * ((BME280_S32_t)dig_H3)) >> 11) + ((BME280_S32_t)32768))) >> 10) + ((BME280_S32_t)2097152)) *
((BME280_S32_t)dig_H2) + 8192) >> 14));

    v_x1_u32r = (v_x1_u32r – (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((BME280_S32_t)dig_H1)) >> 4)); v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);

    return (BME280_U32_t)(v_x1_u32r>>12);
}

(BOSCH社「BME280データシート」より引用)

最後は、気圧の補正関数ですが、データシートの本文には64ビットで計算するプログラムが記載されています。PIC16F18857の変数型は32ビットまでですので、データシートの付録に記載されている32ビット版のプログラムを参考にします。

// Returns pressure in Pa as unsigned 32 bit integer. Output value of “96386” equals 96386 Pa = 963.86 hPa
BME280_U32_t BME280_compensate_P_int32(BME280_S32_t adc_P) {
    BME280_S32_t var1, var2;
    BME280_U32_t p;

    var1 = (((BME280_S32_t)t_fine)>>1) – (BME280_S32_t)64000;
    var2 = (((var1>>2) * (var1>>2)) >> 11 ) * ((BME280_S32_t)dig_P6);
    var2 = var2 + ((var1*((BME280_S32_t)dig_P5))<<1);
    var2 = (var2>>2)+(((BME280_S32_t)dig_P4)<<16);
    var1 = (((dig_P3 * (((var1>>2) * (var1>>2)) >> 13 )) >> 3) + ((((BME280_S32_t)dig_P2) * var1)>>1))>>18;
    var1 =((((32768+var1))*((BME280_S32_t)dig_P1))>>15);

    if (var1 == 0)
    {
        return 0; // avoid exception caused by division by zero
    }

    p = (((BME280_U32_t)(((BME280_S32_t)1048576)-adc_P)-(var2>>12)))*3125;
    if (p < 0x80000000)
    {
        p = (p << 1) / ((BME280_U32_t)var1);
    }
    else
    {
       p = (p / (BME280_U32_t)var1) * 2;
    }

    var1 = (((BME280_S32_t)dig_P9) * ((BME280_S32_t)(((p>>3) * (p>>3))>>13)))>>12; var2 = (((BME280_S32_t)(p>>2)) * ((BME280_S32_t)dig_P8))>>13;
    p = (BME280_U32_t)((BME280_S32_t)p + ((var1 + var2 + dig_P7) >> 4));

    return p;
}

(BOSCH社「BME280データシート」より引用)

さて、、、、、、、、困りました。このままMPLABX IDEでビルドするとエラーになりますので、一つ一つ解決していきましょう。

まず、見慣れない変数型があります。「BME280_S32_t」や「BME280_U32_t」などです。これらはデータシートに説明されていて「「BME280_S32_t」は符号付32ビット、「BME280_U32_t」符号なし32ビット変数型とのことです。「S」がsigned、「U」がunsignedの意味で、数字がビット数のようです。これらは、「int32_t」や「uint32_t」などに置き換えます。

また、t_fineという変数がありますが、これはグローバル変数として宣言するようです。温度補正関数の中で計算後、湿度と気圧補正時にも使用するようです。

なお、これらの関数で補正した値は、そのまま実際の温湿度・気圧表示に使用できません。以下の変換をする必要があります。

(実際の温度) = (補正後の温度値) / 100
(実際の湿度) = (補正後の湿度値) / 1024
(実際の気圧) = (補正後の気圧値) / 100

これらの情報をもとに、補正プログラムは以下のように作成しました。関数の中でt_fineという変数が出てきますが、int32_t型のグローバル変数で宣言しています。また、サンプルプログラムでは、補正前のデータを引数にしていますが、以下のプログラムではグローバル変数にしています。

// グローバル変数
//
// BME280温湿度・気圧読み取りデータ
uint32_t hum_raw, temp_raw, pres_raw;
int32_t  t_fine;

//
// 気温データ補正
//   何をしているのかよくわからない
// 
int32_t bme280CompensateTemperature() {
  
    int32_t var1, var2, T;

    var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>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 bme280CompensatePressure() {

    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)-pres_raw)-(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 bme280CompensateHumidity() {

    int32_t v_x1_u32r;

    v_x1_u32r = (t_fine - ((int32_t)76800));
    v_x1_u32r = (((((hum_raw << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1_u32r)) + 
             ((int32_t)16384)) >> 15) * (((((((v_x1_u32r * ((int32_t)dig_H6)) >> 10) * 
             (((v_x1_u32r * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) * 
             ((int32_t) dig_H2) + 8192) >> 14));
    v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
    // v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    if( v_x1_u32r < 0 ){
        v_x1_u32r = 0;
    }
    // v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);
    if( v_x1_u32r > 419430400 ) {
        v_x1_u32r = 419430400;
    }

    return (uint32_t)(v_x1_u32r >> 12);

}

 

プログラム

最後に、温湿度・気圧データを取得するプログラムにまとめます。データ取得、補正後にLEDを点滅させるプログラムを追加しています。

/*
 * BME280温湿度大気圧センサ
 * 制御プログラム
 * 
 * https://tool-lab.com/
 *
 * 更新
 *   2018. 5.16: 新規作成
 * 
 */

//
// PIC16F18857コンィグレーション設定
//
// CONFIG1
#pragma config FEXTOSC = OFF    // External Oscillator mode selection bits (Oscillator not enabled)
#pragma config RSTOSC = HFINT1  // Power-up default value for COSC bits (HFINTOSC (1MHz))
#pragma config CLKOUTEN = OFF   // Clock Out Enable bit (CLKOUT function is disabled; i/o or oscillator function on OSC2)
#pragma config CSWEN = ON       // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enable bit (FSCM timer enabled)
//
// CONFIG2
#pragma config MCLRE = OFF      // Master Clear Enable bit (IO)
#pragma config PWRTE = ON       // Power-up Timer Enable bit (PWRT enabled)
#pragma config LPBOREN = OFF    // Low-Power BOR enable bit (ULPBOR disabled)
#pragma config BOREN = ON       // Brown-out reset enable bits (Brown-out Reset Enabled, SBOREN bit is ignored)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (VBOR) set to 1.9V on LF, and 2.45V on F Devices)
#pragma config ZCD = OFF        // Zero-cross detect disable (Zero-cross detect circuit is disabled at POR.)
#pragma config PPS1WAY = ON     // Peripheral Pin Select one-way control (The PPSLOCK bit can be cleared and set only once in software)
#pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a reset)
//
// CONFIG3
#pragma config WDTCPS = WDTCPS_31// WDT Period Select bits (Divider ratio 1:65536; software control of WDTPS)
#pragma config WDTE = OFF       // WDT operating mode (WDT Disabled, SWDTEN is ignored)
#pragma config WDTCWS = WDTCWS_7// WDT Window Select bits (window always open (100%); software control; keyed access not required)
#pragma config WDTCCS = SC      // WDT input clock selector (Software Control)
//
// CONFIG4
#pragma config WRT = OFF        // UserNVM self-write protection bits (Write protection off)
#pragma config SCANE = available// Scanner Enable bit (Scanner module is available for use)
#pragma config LVP = ON         // Low Voltage Programming Enable bit (Low Voltage programming enabled. MCLR/Vpp pin function is MCLR.)
//
// CONFIG5
#pragma config CP = OFF         // UserNVM Program memory code protection bit (Program Memory code protection disabled)
#pragma config CPD = OFF        // DataNVM code protection bit (Data EEPROM code protection disabled)

//
// ヘッダファイル
//   int_t型を使用するため、stdint.hをインクルード
//
#include <xc.h>
#include <stdint.h>

//
// 定数
//

// SPIピン設定
#define SPI_SCK    LATCbits.LATC7
#define SPI_MISO   PORTCbits.RC6
#define SPI_MOSI   LATCbits.LATC5
#define SPI_CSB    LATCbits.LATC4

//
// 関数プロトタイプ宣言
//

//   BME280センサー制御
void     bme280Initialization(void);            // BME280の初期化
void     bme280ForcedMeasurement(void);         // 温湿度・気圧データの測定指示
void     bme280ReadTrimmingParameters(void);    // 補正データの読み取り
void     bme280ReadMeasuredRawData(void);       // 補正前の温湿度・気圧データ読み取り
int32_t  bme280CompensateTemperature(void);     // 温度データ補正
uint32_t bme280CompensatePressure(void);        // 気圧データ補正
uint32_t bme280CompensateHumidity(void);        // 湿度データ補正

// SPI通信制御関数
uint16_t spiRead2BytesData(uint8_t address);
uint8_t  spiRead1ByteData(uint8_t address);
void     spiWrite1ByteData(uint8_t address, uint8_t data);
void     spiSend8bit(uint8_t data);
uint8_t  spiReceive8bit();

// クロック周波数
// __delay_ms()関数が時間基準に使用する
#define _XTAL_FREQ 4000000

// グローバル変数
//
// BME280温湿度・気圧読み取りデータ
uint32_t hum_raw, temp_raw, pres_raw;
int32_t  t_fine;

// BME280気温補正データ
uint16_t dig_T1;
int16_t  dig_T2;
int16_t  dig_T3;

// BME280湿度補正データ
uint8_t  dig_H1;
int16_t  dig_H2;
uint8_t  dig_H3;
int16_t  dig_H4;
int16_t  dig_H5;
int8_t   dig_H6;

// BME280気圧補正データ
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;


//
// main関数
//
void main(void) {

    // 動作周波数設定
    OSCCON1bits.NDIV = 0b0000;  // 分周1:1
    OSCFRQbits.HFFRQ = 0b010;   // 4MHz
    
    // ピン属性設定
    ANSELA = 0b00000000;
    ANSELB = 0b00000000;
    ANSELC = 0b00000000;
    TRISA  = 0b00000000;
    TRISB  = 0b00000000;
    TRISC  = 0b01001100;
    
    // SPI信号線初期設定
    SPI_SCK  = 0;  // クロックを0
    SPI_MOSI = 0;  // マスタ→スレーブを0
    SPI_CSB  = 1;  // チップセレクトを1(=無効)

    
    // BME280初期化
    //   コントロールレジスタ、補正係数取得
    bme280Initialization();

    // 実際の温湿度(人が読む場合の数値)
    float actual_temp;
    float actual_hum;
    float actual_press;

    // 補正データ(補正後の数値)
    int32_t  compensated_temp;
    uint32_t compensated_hum;
    uint32_t compensated_press;

    while(1){

        // SPI通信で温湿度センサの値を取得
        bme280ForcedMeasurement();
        bme280ReadMeasuredRawData();

        // 取得したデータをキャリブレーション
        compensated_temp  = bme280CompensateTemperature();
        compensated_hum   = bme280CompensateHumidity();
        compensated_press = bme280CompensatePressure();

        // 実際の温湿度に変換
        actual_temp  = (float)compensated_temp  / 100.0;
        actual_hum   = (float)compensated_hum   / 1024.0;
        actual_press = (float)compensated_press / 100.0;

        LATBbits.LATB0 = 1;
        LATBbits.LATB1 = 1;
        __delay_ms(1000);
        LATBbits.LATB0 = 0;
        LATBbits.LATB1 = 0;
        __delay_ms(1000);
    }

}

//
// BME280初期化関数
//
void bme280Initialization(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式(=0)
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 0;  // スリープモード

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // センサ処理待ち
    __delay_ms(1000);

    // 補正値読み込み
    //   センサごとに固定ちのため、初期化時のみ読み込む
    bme280ReadTrimmingParameters();

}


//
// 温湿度・気圧データ測定指示
//
void bme280ForcedMeasurement(void) {

    // 動作パラメータ設定
    uint8_t t_sb     = 0;  // スタンドバイ時間は使用しない
    uint8_t filter   = 0;  // フィルタOFF
    uint8_t spi3w_en = 0;  // SPIは4線式
    uint8_t osrs_t   = 1;  // 温度オーバーサンプリング x1
    uint8_t osrs_p   = 1;  // 大気圧オーバーサンプリング x1
    uint8_t osrs_h   = 1;  // 湿度オーバーサンプリング x1
    uint8_t mode     = 1;  // 測定指示(1回測定したらスリープモードに移行)

    // 設定値をフォーマットに合わせる
    uint8_t ctrl_meas_reg = (osrs_t << 5) | (osrs_p << 2) | mode;
    uint8_t config_reg    = (t_sb << 5) | (filter << 2) | spi3w_en;
    uint8_t ctrl_hum_reg  = osrs_h;

    // BME280動作パラメータ書き込み
    spiWrite1ByteData(0xF2, ctrl_hum_reg);
    spiWrite1ByteData(0xF4, ctrl_meas_reg);
    spiWrite1ByteData(0xF5, config_reg);
    
    // 測定待ち
    //   statusレジスタを監視して測定完了を判断すべきだが
    //   while文での動作ができなかったので時間待ちで代用
    __delay_ms(10);
    
    // この段階でBME280の温湿度大気圧レジスタに測定値が格納されている
    // このあと自動的にスリープモードに入る

}


//
// 補正データ読み込み
//
void bme280ReadTrimmingParameters(void) {

    // 気温データ用補正データ
    dig_T1 = spiRead2BytesData(0x88);
    dig_T2 = (int16_t)spiRead2BytesData(0x8A);
    dig_T3 = (int16_t)spiRead2BytesData(0x8C);

    // 気圧データ用補正データ用補正データ
    dig_P1 = spiRead2BytesData(0x8E);
    dig_P2 = (int16_t)spiRead2BytesData(0x90);
    dig_P3 = (int16_t)spiRead2BytesData(0x92);
    dig_P4 = (int16_t)spiRead2BytesData(0x94);
    dig_P5 = (int16_t)spiRead2BytesData(0x96);
    dig_P6 = (int16_t)spiRead2BytesData(0x98);
    dig_P7 = (int16_t)spiRead2BytesData(0x9A);
    dig_P8 = (int16_t)spiRead2BytesData(0x9C);
    dig_P9 = (int16_t)spiRead2BytesData(0x9E);

    // 湿度データ用補正データ用補正データ
    dig_H1 = spiRead1ByteData(0xA1);
    dig_H2 = (int16_t)spiRead2BytesData(0xE1);
    dig_H3 = spiRead1ByteData(0xE3);
    dig_H4 = (int16_t)((spiRead1ByteData(0xE4) << 4) | (spiRead1ByteData(0xE5) & 0x0F));
    dig_H5 = (int16_t)((spiRead1ByteData(0xE6) << 4) | (spiRead1ByteData(0xE5) >> 4));
    dig_H6 = (int8_t)spiRead1ByteData(0xE7);

}


// 
// 補正前の生の温湿度・気圧データ読み込み
// 
void bme280ReadMeasuredRawData() {

    // データ読み取り用配列
    uint32_t data[8];

    // スレーブセレクトアクティブ
    SPI_CSB = 0;

    // 読み込み開始アドレス指定
    spiSend8bit(0xF7 | 0b10000000);

    // 8バイト分のデータ読み込み
    for(int8_t i=0; i<8; i++){
      data[i] = spiReceive8bit();
    }

    // スレーブセレクトインアクティブ
    SPI_CSB = 0;

    // 読み込みしたデータから気温、湿度、気圧データを生成
    pres_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
    temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
    hum_raw  = (data[6] <<  8) | data[7];

}

//
// 気温データ補正
//   何をしているのかよくわからない
// 
int32_t bme280CompensateTemperature() {
  
    int32_t var1, var2, T;

    var1 = ((((temp_raw >> 3) - ((int32_t)dig_T1<<1))) * ((int32_t)dig_T2)) >> 11;
    var2 = (((((temp_raw >> 4) - ((int32_t)dig_T1)) * ((temp_raw>>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 bme280CompensatePressure() {

    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)-pres_raw)-(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 bme280CompensateHumidity() {

    int32_t v_x1_u32r;

    v_x1_u32r = (t_fine - ((int32_t)76800));
    v_x1_u32r = (((((hum_raw << 14) -(((int32_t)dig_H4) << 20) - (((int32_t)dig_H5) * v_x1_u32r)) + 
             ((int32_t)16384)) >> 15) * (((((((v_x1_u32r * ((int32_t)dig_H6)) >> 10) * 
             (((v_x1_u32r * ((int32_t)dig_H3)) >> 11) + ((int32_t) 32768))) >> 10) + ((int32_t)2097152)) * 
             ((int32_t) dig_H2) + 8192) >> 14));
    v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * ((int32_t)dig_H1)) >> 4));
    // v_x1_u32r = (v_x1_u32r < 0 ? 0 : v_x1_u32r);
    if( v_x1_u32r < 0 ){
        v_x1_u32r = 0;
    }
    // v_x1_u32r = (v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r);
    if( v_x1_u32r > 419430400 ) {
        v_x1_u32r = 419430400;
    }

    return (uint32_t)(v_x1_u32r >> 12);

}


//
// SPIデータを指定アドレスから2バイト読み込み
// 
uint16_t spiRead2BytesData(uint8_t address) {

    uint8_t  data_low, data_high;  // 読み込んだ1バイトデータ格納用
    uint16_t data;  // 2バイトデータ用

    // チップセレクトを0にしてセンサモジュールとの通信開始
    SPI_CSB = 0;

    // 読み込みデータアドレス指定
    spiSend8bit(address);

    // 指定アドレスとその次のアドレスのデータを2バイト読み込み
    data_low  = spiReceive8bit();
    data_high = spiReceive8bit();

    //16ビットデータにする
    data = (data_high << 8) | data_low;

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;

    return data;

}


//
// SPIデータを指定アドレスから1バイト読み込み
// 
uint8_t spiRead1ByteData(uint8_t address) {

    // 受信データ格納変数
    uint8_t data;

    // チップセレクトを0にしてセンサモジュールとの通信開始
    SPI_CSB = 0;

    // SPI通信手順によりアドレスを送信
    spiSend8bit(address);

    // SPI通信手順によりデータを受信
    data = spiReceive8bit();

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;

    // 受信したーデータを返す
    return data;

}


//
// SPIデータを指定アドレスに1バイト書き込み
//
void spiWrite1ByteData(uint8_t address, uint8_t data) {

    // チップセレクトを0にしてセンサモジュールとの通信開始
    SPI_CSB = 0;

    // アドレス指定(書き込みは最上位ビット0)
    spiSend8bit(address & 0b01111111);

    // データ書き込み
    spiSend8bit(data);

    // チップセレクトを1にして通信終了
    SPI_CSB = 1;
  
}

//
// SPIデータ8ビット書き込み
//   SCK/MOSI制御のための関数であるため
//   スレーブセレクト信号はこの関数の前後で制御すること
void spiSend8bit(uint8_t data) {

    // 8ビット分繰り返す
    for (int8_t i=7; i>=0; i--) {

        // (1)クロックを0にする
        SPI_SCK = 0;
        
        // (2)MOSIにデータをセットする
        if( data & (1<<i) ) {
            SPI_MOSI = 1;
        } else {
            SPI_MOSI = 0;
        }
      
        // (3)クロックを1にする
        SPI_SCK = 1;
    }

}

//
// SPIデータ8ビット読み込み
//   SCK/MOSI制御のための関数であるため
//   スレーブセレクト信号はこの関数の前後で制御すること
//
uint8_t spiReceive8bit() {

    // 受信データ格納変数
    uint8_t read_data = 0;

    // 8ビット分繰り返す
    for (int8_t i=7; i>=0; i--) {

        // 受信データ変数を1ビット左シフト
        read_data <<= 1;

        // (1)クロックを0にする
        SPI_SCK = 0;
        
        // (2)クロックを1にする
        SPI_SCK = 1;

        // (3)この時点でセンサからのデータを読めるので、MISOのピン状態を読む
        if(SPI_MISO){
            read_data |= 1;
        }
    }

    // 受信したデータを返す
    return read_data;

}

 

動作確認

それでは動作確認をしてみます。

センサからデータ取得をして補正データの計算ができるとLEDが点滅しますので、まずはそのままビルド→PICマイコンに書き込んでLEDが点滅するか確認してみてください。

次に、デバッグ機能を利用して実際に温湿度・気圧データが取得できているか確認してみてください。

気温は実際よりも2〜3度高くなっていればOKです。湿度は、冬であれば20%〜50%程度、夏であれば60%以上であればだいたい合っていると思います。

気圧については、おそらく気圧計をお持ちの方は少ないと思いますので、以下の手順で現在の気圧の値を計算します。

まず、気象庁のアメダスのサイトで一番近い地点の気圧を調べます。以下のサイトにアクセスして、近い地点の地域を選択して気圧を調べます。

気象庁アメダス

Pic practice 35 pressure data

このサイトで確認できるデータは、海抜0mの気圧です。気圧は標高が10m高くなると約1hPa低くなりますので、現在測定している場所の標高を以下の手順で調べます。

国土地理院のサイトに標高を調べるページがありますので、アクセスします。

国土地理院 標高データサイト

検索機能がありますので、住所やランドマークなどを入力して検索します。

Pic practice 35 altitude map 1

近辺の地図が表示されたら、地図の拡大縮小を行い、中央の十字を調べたい地点に合わせます。十字地点の標高は以下に表示されます。(以下の画像はJR新宿駅西口前あたりです)

Pic practice 35 altitude map 2

なお、戸建ての2階以上やマンションの場合は、この標高よりも高くなりますので、だいたいの高さを足してみてください。

10mあたり約1hPa低くなりますので、海抜0mの気圧データから、標高分の気圧を引くと測定しているところの気圧がわかります。その値とそれほどずれていなければ正しい値が取得できています。

ところで、、、

プログラムがかなり複雑になってきましたよね。SPI通信部分はそれほど難しくないと思いますが、センサを制御するプログラム部分が難しいところだと思います。ただ、センサ制御の部分はデータシートに従って作成する、という感じですのでプログラミングの難しさより、データシートを読んで理解する方がかなり難しいです。

この後の記事では、温度センサと液晶ディスプレイのプログラムを作成していきますが、同様にデータシートを読んでいくことから始まります。

かなり難しく感じるかもしれませんが、一つ一つ紐解いていけば絶対に理解できますので、すぐにわからなくても時間をかけて理解していきましょう!

 

更新履歴

日付 内容
2018.5.17 新規投稿
2019.5.14 BME280の強制測定モードの際、測定が完了するまで待つ処理(10ms)を追加