MicroPython的午睡(51) ATOMLite、BMP280の補償計算大変なのね

Joseph Halfmoon

前々回、温度と湿度を測ってNode-REDに報告しました。今回は気圧も測るべしということで、定番の圧力センサBosch BMP280をMicroPythonから制御してみることに。ただし読み取りは自前のコードで実施。簡単に圧力読めるものと思っていたら、補償の計算大変なのね。自分でやってみないと身にしみませぬ。

※「MicroPython的午睡」投稿順 Indexはこちら

(実験に使用したMicroPythonスクリプト全文は末尾に)

まずは圧力センサ BMP280 のホームページへのリンクを以下に掲げます。

Pressure Sensor BMP280 | Bosch Sensortec

人気、定番のセンサなので、私も使用させていただいた経験はあります。しかし過去の使用はすべて「人様」がおつくりになったライブラリに乗っかっての使用でした。今回、ESP32用GenericポートのMicroPython上でBMP280から圧力を取得するにあたり、不埒にも以下のように考えました。

どうせI2C接続のセンサでしょ、設定後、レジスタ読めばチョロイでしょ。

この考えは偉大なBosch社の前に敗れさることになります。まずはその前にハードウエアの接続から。

今回、接続からしてイレギュラー

BMP280は3.3V電源で動作するデバイスです。よってデバイス単体があれば(ただし手半田は困難と思われます)3.3VのIO端子をもつM5 ATOMLiteに直結することができます。しかし今回、手元にあったのは、

Arduino用にGroveモジュール化されたBMP280

のみです。5VのIOのArduinoに適合させるために、このモジュールは5V電源で動作するようにレギュレータを搭載しており、また、外部信号線は5Vでプルアップし、3.3VのBMP280の信号へはMOSFETを介して「安全」に接続してくれているのです。5V電源、5VのIOの相手であれば完璧。しかし、M5 ATOMLiteには直結するわけにもいかないです。

まあ、そういうこともあるかも、ということで以前からM5 ATOMLiteの横にPCA9306を搭載してありました(秋月製のDIP化ボード、デバイスはTI製だと思います。)今回それを「活用」し接続することにいたしました。3.3Vの信号をいったん5Vに持ち上げて、また3.3Vに落とすとな。いったい何をやっているのだか、というよゐこは真似しないでね的回路が以下に。

BMP280_5Vmodule_SCH悪いことはできないもので、ピンソケットに差してある AE-PCA9306の接触が悪く、押さえてないと誤動作します。付属していたの細ピンヘッダはブレッドボードに刺すには良いですが、ピンソケットに差すにはコンタクトが弱いです。失敗。。。後で何とかしないと。

I2C接続は簡単、BMP280の読み書きもOK、でも補償計算大変

今回はハードウエアI2C接続で、2個あるうちの1番の方を使用してみましたが、0番でも大丈夫そうです。またSoftI2Cでも動きそうです。ESP32の端子のフレキシブルさは素晴らしいです。BMP280は多数のレジスタを内部に持つので、アクセスには以下の関数を使えば良い感じです。

    • readfrom_mem( I2Cアドレス, レジスタアドレス, 読み取りバイト数)
    • writeto_mem( I2Cアドレス, レジスタアドレス, byte型ストリング)

読み書きは特に問題なくできるようでした。いろいろ設定できるのですが、IIRフィルタなどは使わず、また、何度も測定を繰り返して測定精度を上げる技も使わず、一番ズボラな設定で、温度と圧力の「生」数値をまずは読み取ってみました。読めました。

しかし何これ、どう解釈したら良いの?

という値です。ここにいたってようやくBosch社のデータシートを真面目によんで愕然としました。センサには個体差があるので、デバイスに固有の補正のためのトリミングパラメータがあり、それを用いて「コンペンセーション」することが必要なのです。1個2個のパラメータで補正するイメージでおったのですが、

    • 温度の測定に3パラメータ
    • 圧力の測定に9パラメータ
    • そして圧力の測定時には温度測定で計算した内部パラメータが必須

という念の入り具合で、結局12パラメータ全てが必要です。高精度であることは納得、でも補償の計算式みてその面倒くささに愕然。Cのサンプルコードが掲載されていたのですが、当然ながら固定小数点を駆使したコードでありました。MicroPythonに載せ替えるのはとても大変そう。。。

そう思ったら、その次に具体的な数値を示している計算例があり、そちらの方はdouble型の浮動小数点数で処理していました。計算量が圧倒的に違いますが、計算は浮動小数の方が楽、とても楽。そのうえ計算例ついているので検算も楽。ついお楽に流れて書いた(Bosch社のCのサンプルコードをMicroPythonに移植した)のが末尾のMicroPythonスクリプトの中の幾つかの関数です。一応動いているみたいなので、自前のClassとしてM5 ATOMLiteのlibディレクトに後で置いておこうかと。

実際に動かしてみたところがこちら。RAWで示されているのが、センサから得られる生の値(センサ内部のADC読み値)、COMPENSATEの値がデバイス固有の補償を加えた後、人間が分かる形式で印字したもの。

ID = 0x58
TEMPERATURE: RAW=529360 COMPENSATE=23.1 [C]
PRESSURE : RAW=354256 COMPENSATE=100134.5 [Pa]

動いたのは良かったけれども、BMP280の始末だけで終わってしまいました。Node-REDに圧力を報告するところまでたどり着きませんでした。

MicroPython的午睡(50) ATOMLite、やらかしTZ、カレンダ変換 へ戻る

MicroPython的午睡(52) ATOMLite、気圧の測定結果をNode-REDへ に進む

実験に使用したMicroPythonのスクリプト全文
import time
from machine import Pin, I2C
i2c = I2C(1, scl=Pin(21), sda=Pin(25), freq=100000)
#print(i2c.scan())

bmp280adr = 0x77
bmp280_ID = 0xD0
bmp280_CONF = 0xF5
bmp280_CTRL = 0xF4
bmp280_STAT = 0xF3
bmp280_REST = 0xE0
bmp280_TMPX = 0xFC
bmp280_TMPL = 0xFB
bmp280_TMPM = 0xFA
bmp280_PRSX = 0xF9
bmp280_PRSL = 0xF8
bmp280_PRSM = 0xF7
bmp280_T1 = 0x88
bmp280_T2 = 0x8A
bmp280_T3 = 0x8C
bmp280_P1 = 0x8E
bmp280_P2 = 0x90
bmp280_P3 = 0x92
bmp280_P4 = 0x94
bmp280_P5 = 0x96
bmp280_P6 = 0x98
bmp280_P7 = 0x9A
bmp280_P8 = 0x9C
bmp280_P9 = 0x9E

def readParamsU(rAdr):
    dig_b = i2c.readfrom_mem(bmp280adr, rAdr, 2)
    tmp = dig_b[1]<<8 | dig_b[0]  
    return tmp

def readParamsS(rAdr):
    tmp = readParamsU(rAdr)  
    if (tmp & 0x8000) != 0:
        tmp = -((~tmp + 1) & 0xFFFF)
    return tmp

#This function based on BOSCH reference code.
def bmp280_compensate_T(adc_T, opt=False):
    global dig_T1, dig_T2, dig_T3, t_fine
    var1 = (adc_T/16384.0 - dig_T1/1024.0) * dig_T2
    A1=adc_T/131072.0
    T1=dig_T1/8192.0
    var2 = ((A1-T1)*(A1-T1))*dig_T3
    t_fine = var1 + var2
    if opt:
        print("var1=",var1)
        print("var2=",var2)
        print("t_fine=",t_fine)
    return (var1+var2)/5120.0

#Following test values from BOSCH reference code.
def test_bmp280_compensate_T():
    global dig_T1, dig_T2, dig_T3
    dig_T1 = 27504
    dig_T2 = 26435
    dig_T3 = -1000
    adc_T  = 519888
    print("TEST_T(Expected 25.08)=", bmp280_compensate_T(adc_T, opt=True))

#This function based on BOSCH reference code.
def bmp280_compensate_P(adc_P, opt=False):
    global dig_P1, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9, t_fine
    var10 = (t_fine/2.0) - 64000.0
    var20 = var10*var10*(dig_P6/32768.0)
    var21 = var20+var10*(dig_P5)*2.0
    var22 = (var21/4.0)+(dig_P4*65536.0)
    var11 = (dig_P3*var10*var10/524288.0+dig_P2*var10)/524288.0
    var12 = (1.0+var11/32768.0)*dig_P1
    p0 = 1048576.0 - adc_P
    p = (p0-(var22/4096.0))*6250.0/var12
    var1 = dig_P9*p*p/2147483648.0
    var2 = p*dig_P8/32768.0
    if opt:
        print("var10=", var10)
        print("var11=", var11)
        print("var12=", var12)
        print("var1=", var1)
        print("var20=", var20)
        print("var21=", var21)
        print("var22=", var22)
        print("var2=", var2)
        print("p0=", p0)
        print("p=", p)
    return p + (var1 + var2 + dig_P7)/16.0

#Following test values from BOSCH reference code.
def test_bmp280_compensate_P():
    global dig_P1, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9
    dig_P1 = 36477
    dig_P2 = -10685
    dig_P3 = 3024
    dig_P4 = 2855
    dig_P5 = 140
    dig_P6 = -7
    dig_P7 = 15500
    dig_P8 = -14600
    dig_P9 = 6000
    adc_P = 415148
    print("TEST_P(Expected 100653)=", bmp280_compensate_P(adc_P, opt=True))

def bmp280_getParamsT(opt=False):
    global dig_T1, dig_T2, dig_T3
    dig_T1 = readParamsU(bmp280_T1)
    dig_T2 = readParamsS(bmp280_T2)
    dig_T3 = readParamsS(bmp280_T3)
    if opt:
        print("DIG_T1:", dig_T1)
        print("DIG_T2:", dig_T2)
        print("DIG_T3:", dig_T3)
    
def bmp280_getParamsP(opt=False):
    global dig_P1, dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9
    dig_P1 = readParamsU(bmp280_P1)
    dig_P2 = readParamsS(bmp280_P2)
    dig_P3 = readParamsS(bmp280_P3)
    dig_P4 = readParamsS(bmp280_P4)
    dig_P5 = readParamsS(bmp280_P5)
    dig_P6 = readParamsS(bmp280_P6)
    dig_P7 = readParamsS(bmp280_P7)
    dig_P8 = readParamsS(bmp280_P8)
    dig_P9 = readParamsS(bmp280_P9)
    if opt:
        print("DIG_P1:", dig_P1)
        print("DIG_P2:", dig_P2)
        print("DIG_P3:", dig_P3)
        print("DIG_P4:", dig_P4)
        print("DIG_P5:", dig_P5)
        print("DIG_P6:", dig_P6)
        print("DIG_P7:", dig_P7)
        print("DIG_P8:", dig_P8)
        print("DIG_P9:", dig_P9)

def main():
#    test_bmp280_compensate_T()
#    test_bmp280_compensate_P()
    buf = str(i2c.readfrom_mem(bmp280adr, bmp280_ID, 1), "utf-8")
    print("ID = 0x{0:02x}".format(ord(buf[0])))
    i2c.writeto_mem(bmp280adr, bmp280_CONF, b'\x40') # 125mS, no filter, no spi
    i2c.writeto_mem(bmp280adr, bmp280_CTRL, b'\x27') # x1, x1, normal mode
    bmp280_getParamsT(False)
    bmp280_getParamsP(False)
    
    loopCounter = 0
    while( loopCounter < 60 ):
        loopCounter += 1
        prslis = i2c.readfrom_mem(bmp280adr, bmp280_PRSM, 3)
        tmplis = i2c.readfrom_mem(bmp280adr, bmp280_TMPM, 3)
        prs = (prslis[0]<<12) | (prslis[1] << 4)
        tmp = (tmplis[0]<<12) | (tmplis[1] << 4)
        print("TEMPERATURE: RAW={0} COMPENSATE={1:3.1f} [C]".format(tmp, bmp280_compensate_T(tmp)))
        print("PRESSURE   : RAW={0} COMPENSATE={1:7.1f} [Pa]".format(prs, bmp280_compensate_P(prs)))        
        time.sleep(10)  
    
if __name__ == "__main__":
    main()