2017年8月19日土曜日

BME280をPIC32MX250F128B+Harmonyで使う

前回に引き続き、I2CセンサをPIC32で扱うお話です。

BME280はBOSCH製の温湿度・気圧センサです。
個人的にはBOSCHと言えばドイツの自動車部品メーカーというイメージですが、こういった半導体部品も作ってるんですね。驚きです。

さてさて、こいつもI2C/SPI両対応のチップで、分解能が温度0.01℃、湿度0.008%、気圧0.18Paとかなり細かくデータを取ることができます。データシートもドイツメーカーだからか英語が非常にシンプルで読みやすく、内容もコンパクトにまとめられています。

このセンサの使用方法としては
  1. 設定の書き込み
  2. Trimming Parameterの読み込み
  3. (生の)測定データの読み込み
  4. 測定データの演算
の流れとなっています。
Trimming ParameterはCalibration Dataとも書かれており、生の測定値を℃、%、hPaに変換するにあたって使われる係数です。
センサによって個体差があるので、メーカー出荷時にその個体差に合わせて係数を設定しておき、ユーザーは生の測定データをTrimming Parameterを使って演算することで正確な温湿度や気圧を得ることができます。

順に追って見ていきましょう。

void I2CEventHandler(DRV_I2C_BUFFER_EVENT event, DRV_I2C_BUFFER_HANDLE bufferHandle, uintptr_t context)
{
    /*
    switch (event)
    {
        case DRV_I2C_BUFFER_EVENT_COMPLETE:
            break;
        case DRV_I2C_BUFFER_EVENT_ERROR:
            break;
        default:
            break;
    }
    */
}

_Bool I2C_InitAndOpen(I2C_HANDLE *pobj, SYS_MODULE_INDEX index)
{
    pobj->hDriver = DRV_I2C_Open(index, DRV_IO_INTENT_READWRITE);
    pobj->hBufferDriver = NULL;
    
    if(pobj->hDriver == (DRV_HANDLE)NULL)
        return false;
    
    DRV_I2C_BufferEventHandlerSet(pobj->hDriver, I2CEventHandler, (uintptr_t)pobj);

    return true;
}

DRV_I2C_BUFFER_EVENT I2C_GetTransferStatus(I2C_HANDLE *pobj)
{
    return DRV_I2C_TransferStatusGet(pobj->hDriver, pobj->hBufferDriver);
}

_Bool I2C_IsCommunicating(I2C_HANDLE *pobj)
{
    if(pobj->hBufferDriver == NULL)
        return false;
    else {
        DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj);        
        return (event != DRV_I2C_BUFFER_EVENT_COMPLETE) && (event != DRV_I2C_BUFFER_EVENT_ERROR);
    }
}

uint32_t I2C_GetTransferredBytes(I2C_HANDLE *pobj)
{
    return DRV_I2C_BytesTransferred(pobj->hDriver, pobj->hBufferDriver);
}

まずはI2C周りのコードです。I2CバスをBME280とMPU-9250で共有しているので、共通するI2C関係のプログラムを分離しています。

#define BME280_SENDDATA_COUNT_MAX  4

typedef struct {
    I2C_HANDLE *pHandle;
    uint8_t WriteBuffer[BME280_SENDDATA_COUNT_MAX * 2];
    uint8_t SlaveAddress;
    
    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;    
} BME280;

_Bool BME280_Init(BME280 *pobj, I2C_HANDLE *pHandle, uint8_t SlaveAddr)
{
    pobj->SlaveAddress = SlaveAddr;
    pobj->pHandle = pHandle;

    return true;
}

そのためBME280の初期化関数はアドレスの登録だけになっています。
ちなみにBME280のデータを保持する構造体にはdig_**みたいな変数をたくさん用意してありますがこれらがTrimming Parametersです。配列にしたほうがカッコイイ気もしますが、データシート等に倣ってこう書いておくことにします。

_Bool BME280_StartWritingResistor(BME280 *pobj, uint8_t address, uint8_t data)
{
    BME280_DATA_ADDRESS_PAIR pair;
    
    pair.Address = address;
    pair.Data = data;
    
    return BME280_StartBurstWritingResistor(pobj, &pair, 1);
}

_Bool BME280_StartBurstWritingResistor(BME280 *pobj, BME280_DATA_ADDRESS_PAIR *pairs, int count)
{
    int i;
    
    if(count <= 0 || count > BME280_SENDDATA_COUNT_MAX || I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    for(i = 0; i < count; i++) {
        pobj->WriteBuffer[i * 2 + 0] = pairs[i].Address;
        pobj->WriteBuffer[i * 2 + 1] = pairs[i].Data;
    }
    
    pobj->pHandle->hBufferDriver = DRV_I2C_Transmit(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, count * 2, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

_Bool BME280_StartReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata)
{
    return BME280_StartBurstReadingResistor(pobj, address, pdata, 1);
}

_Bool BME280_StartBurstReadingResistor(BME280 *pobj, uint8_t address, uint8_t *pdata, int length)
{
    if(I2C_IsCommunicating(pobj->pHandle))
        return false;
    
    pobj->WriteBuffer[0] = address;

    pobj->pHandle->hBufferDriver = DRV_I2C_TransmitThenReceive(pobj->pHandle->hDriver, pobj->SlaveAddress << 1, pobj->WriteBuffer, 1, pdata, length, NULL);
    
    return pobj->pHandle->hBufferDriver != NULL;
}

送受信関係のコードです。
基本的には、書きこみの場合はレジスタアドレスを送ってからデータ内容を送信、読み込みの場合はレジスタアドレスを送ってから読み込みモードでリスタートして受信という形になります。

ただ、複数レジスタを同時に読み書きするburst write / readは読み込むときと書きこむときで仕様が違います。
読み込みの場合はレジスタが自動的にインクリメントされるので、読み込みたいアドレスの先頭のアドレスを指定してやって読みたいバイト数分のデータを連続で読み込めば良いです。
書き込みの場合は自動的にインクリメントはされません。その代わり、「レジスタアドレス+書きこむ値」の計2バイトをセットで送る形になります。
読み込みは測定値のレジスタアドレスが連番になっていますから自動インクリメントが助かりますが、書きこみの場合は設定になるので連番とは限らず、このような書きこみ方ができるとかえって助かります。

ところで、湿度は16bit、温度と気圧は20bit値なわけで、2バイトないしは3バイトで1つのデータとなっています。この手の複数バイトを1バイトずつ分割して転送する処理をするときは「最初のバイトを送信してから最後のバイトを送信し終わるまでの間に値が更新されたらどうなるのか?」という疑問が付きまといます。
MPU-9250のときも気になってデータシートを探したのですが、特にそのようなことは書かれていませんでした。 ですが、このBME280にはそれに関する記述がありました。
In normal mode, the timing of measurements is not necessarily synchronized to the readout by the user. This means that new measurement results may become available while the user is reading the results from the previous measurement. In this case, shadowing is performed in order to guarantee data consistency. Shadowing will only work if all data registers are read in a single burst read. Therefore, the user must use burst reads if he does not synchronize data readout with the measurement cycle. Using several independent read commands may result in inconsistent data.
If a new measurement is finished and the data registers are still being read, the new measurement results are transferred into shadow data registers. The content of shadow registers is transferred into data registers as soon as the user ends the burst read, even if not all data registers were read.
The end of the burst read is marked by the rising edge of CSB pin in SPI case or by the recognition of a stop condition in I2C case. After the end of the burst read, all user data registers are updated at once. (データシート4.1章「Data register shadowing」より引用)
(拙訳)ノーマルモードでは、測定のタイミングはデータの読み出しと同期されるとは限りません。これはすなわち、ひとつ前の測定値を読み出しているときに新たな測定が完了する可能性があるということを意味しています。そのような場合は、シャドーイングがデータの整合性を保証します。シャドーイングはデータレジスタを1回のバーストリードで読み出しているときのみ機能します。そのため、データの測定サイクルに同期せずに読み出す場合は必ずバーストリードを行う必要があります。個々のレジスタを個別に読みだすとデータの整合性が崩れる可能性があります。
測定が終了したときがレジスタの読み出しをしている最中だった場合、その新しい測定値はシャドーレジスタに転送されます。バーストリードが終了したタイミングでシャドーレジスタの値はデータレジスタに転送されるため、他のレジスタをこれから読み出す予定だったとしてもそれは感知されません。
バーストリードの終了は、SPI接続の場合はCSBピンの立ち上がりエッジ、I2C接続の場合はストップビットで判断されます。バーストリードの終了時に、すべてのデータレジスタは一度に更新されます。
英文を読んでいると何度も同じようなことを言っていて混乱してきますが、要するに、バーストリード中に測定が完了した場合、新しい測定値はシャドーレジスタ(本来読み出すレジスタに書き込みができないときに、いったん測定データを保持しておくためのレジスタ。本来のレジスタの影のような存在であるため、このような名前になっている。)に一旦保存され、バーストリードの終了とともに読み出し用レジスタに転送されます。このため、データの整合性は保証されるということです。
データの整合性とは、マルチバイトデータのそれぞれのバイトが単一の測定から得られたものか、という意味以外にも、温度データと気圧データが同じ時刻に測定されたものである、という意味まで含んでいると思われます。ですので、原則として、このセンサではすべての測定値を同時にバーストリードする必要があります。


さて、今回は設定をこのようにしました。

BME280_RV_CONFIG_t config;
BME280_RV_CTRL_HUM_t ctrl_hum;
BME280_RV_CTRL_MEAS_t ctrl_meas;
BME280_DATA_ADDRESS_PAIR data[3];

ctrl_hum.OSRS_H = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_P = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.OSRS_T = BME280_VAL_OVERSAMPLING_x1;
ctrl_meas.MODE = BME280_VAL_MODE_NORMAL;
config.T_SB = BME280_VAL_TIME_STANDBY_0_5;
config.FILTER = BME280_VAL_FILTER_OFF;
config.SPI3W_EN = 0;

data[0].Address = BME280_RA_CTRL_HUM;
data[0].Data = ctrl_hum.Value;
data[1].Address = BME280_RA_CTRL_MEAS;
data[1].Data = ctrl_meas.Value;
data[2].Address = BME280_RA_CONFIG;
data[2].Data = config.Value;

if(BME280_StartBurstWritingResistor(&appData.bme280, data, 3))
    appData.state = APP_STATE_BME280_CHECK_CONFIG;
else {
    strcpy(szErrorMessage, "ERROR on APP_STATE_BME280_WRITE_CONFIG\r\n");
    appData.state = APP_STATE_ERROR;
}
break;

各設定用レジスタの値はビットフィールド構造体を作ってあげて、マクロでわかりやすく設定できるようにしました。
同じ物理量を複数回測定することで精度を出す手法「オーバーサンプリング」を使うことができますが、その分サンプリング周波数が落ちるので今回は使わないことにしています。
モードはスリープ(SLEEP)、1回のみ測定(FORCE)、連続で測定(NORMAL)の3種類から選べます。今回はもちろんNORMALです。
また、測定をしてから次の測定までのインターバルを設定することができます。今回は最短の0.5msにしましたが、あくまでもそれはインターバルで、測定周期とは異なることに注意が必要です。この辺の時間の見積もり方はデータシートに詳しく載っています。

さて、設定をしたら次はTrimming Parameterを読み込まなくてはいけません。
レジスタはアドレス0x88~0xA1の26バイトと、0xE1~0xF0の16バイトに分かれて格納されていますが、このうち使うのは前者26バイト分と後者7バイト分のようです。
かなり不規則なデータの入れ方をしていますが、格納するときはこのようなプログラムになります。

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_Low(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_T1 = ((uint16_t)pdata[ 1] << 8) | pdata[ 0];
        pobj->dig_T2 = ((uint16_t)pdata[ 3] << 8) | pdata[ 2];
        pobj->dig_T3 = ((uint16_t)pdata[ 5] << 8) | pdata[ 4];
        pobj->dig_P1 = ((uint16_t)pdata[ 7] << 8) | pdata[ 6];
        pobj->dig_P2 = ((uint16_t)pdata[ 9] << 8) | pdata[ 8];
        pobj->dig_P3 = ((uint16_t)pdata[11] << 8) | pdata[10];
        pobj->dig_P4 = ((uint16_t)pdata[13] << 8) | pdata[12];
        pobj->dig_P5 = ((uint16_t)pdata[15] << 8) | pdata[14];
        pobj->dig_P6 = ((uint16_t)pdata[17] << 8) | pdata[16];
        pobj->dig_P7 = ((uint16_t)pdata[19] << 8) | pdata[18];
        pobj->dig_P8 = ((uint16_t)pdata[21] << 8) | pdata[20];
        pobj->dig_P9 = ((uint16_t)pdata[23] << 8) | pdata[22];
        pobj->dig_H1 =            pdata[25];
    }
    return event;
}

DRV_I2C_BUFFER_EVENT BME280_CheckAndSetTrimingParam_High(BME280 *pobj, uint8_t *pdata)
{
    DRV_I2C_BUFFER_EVENT event = I2C_GetTransferStatus(pobj->pHandle);
    if(event == DRV_I2C_BUFFER_EVENT_COMPLETE) {
        pobj->dig_H2 = ((uint16_t)pdata[1] << 8) |   pdata[0];
        pobj->dig_H3 =                               pdata[2];
        pobj->dig_H4 = ((uint16_t)pdata[3] << 4) | ( pdata[4]       & 0x0F);
        pobj->dig_H5 = ((uint16_t)pdata[5] << 4) | ((pdata[4] >> 4) & 0x0F);
        pobj->dig_H6 =                               pdata[6];
    }
    return event;
}

前半部分で偶数バイトは下位、奇数バイトは上位としているのかと思いきやdig_H1だけ違ったり、後半部分では4ビットずつ使うところなんかが出てきていささか気持ち悪いですが、とりあえずこんな感じのコードになりました。


さて、今度はこれを使って生の測定値を変換することになりますが、これまた非常にわかりにくい処理になっています。

データシートを見ると、コードが書いてあるだけで、具体的にどのような数式で処理をしているかはっきりしません。変数もval1などと言った意味のない名前で、使いまわしもされており、完全に人に読ませるようなコードではありません。
もっと言えば、データシートでは符号付き整数型のシフト演算がされており、負の値に対するシフト演算はC言語では未定義(処理系依存)の動作となります。そのうえ、データシートに「値の変換はBosch Sensortecから出ているAPIを使うことを推奨します」と書かれている始末です。じゃあなんのためにデータシートにこんなわけわからんコード載せてんねん。
Bosch Sensortec BME280 sensor driver
これが例のAPIです。
見るとデータシートのコードから修正され、除算演算子を使った計算に置き換えられています。
実際は2^nの除算しか使ってないので、コンパイラの腕の見せ所(最適化でシフト処理に置き換えられるか)ですが、さすがに処理系依存のコードを書くわけにもいかないのでこの辺が妥当でしょう。

生の測定値とはいったい何を表している値なのか、この演算処理はどういった計算をしているのかはデータシートでの説明が皆無ですので置いておくとして、ここで重要なことは気圧や湿度の変換には温度データが必要な点です。上記APIには「t_fine」という変数に温度が保存され、気圧、湿度の変換関数で使われていますが、このt_fineには温度[℃]の値が5120倍された数値になっていて保存されています。
その点に注意しながら上記APIのコードを使いましょう。

これで無事測定ができるようになりました。


これは、外出から帰ってきたときからの部屋の温湿度・気圧の推移です。最初は32℃くらい室温がありましたが、エアコンを入れたので28℃くらいまで下がっている様子がわかります。湿度もエアコンによって下がっていますね。


今回はBME280に絞った記事ですが、実際は先日のMPU-9250と同時にデータを取っています。
もうちょっとプログラムに磨きを掛けて、細かな完成度を上げていきたいです。

0 件のコメント:

コメントを投稿