chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

Rust版 QMP6988 Driverを作る

背景:部屋の空気質(CO2、温度、湿度)をクラウドに上げて集計しているが、気圧が計測できていなかった。高精度の気圧センサ―らしいQMP6988を使って気圧を計測、集計する
課題:QMP6988はレジスタの値を読んだ後、補正が必要なのだが補正式が結構複雑。Arduino/MicroPyhon版のドライバであればGitHubに公開さえているがRust版は無いようだ(もし公開されていてもいざ使おうとすると引数の型変換等エラー出まくって苦労しそう)
取り組み:プログラミングの勉強も兼ねてRust版QMP6988のドライバを作ってみる
詳細:
補正式はQMP6988の仕様書にかかれているのだけど*1、結構分かりくい。最初読んだときは全く分からなかった。今も完全には理解していないけど、多分こういうことだろうと解釈して自分がPGしやすくなるよう適当にレイアウトを変えた。

補正式を見ながらゴリゴリ計算したら値が得られるのだろう。。気になるのは、GitHubに掲載されているドライバは補正で使っている係数の値が小数点になっていなかった。テーブルに書かれているE-NNって、10のマイナスNN乗と思うのだが、、誤差を抑えるため演算の最後にまとめて10のNN乗で割っているのか?
Rustで試行錯誤せず、まずはPythonで演算式を作った方がよさそうだ。(GitHubにもPython版ドライバがあるのだけど、struct moduleにより実装されており、ちょっと意味分からず)
Pythonでサンプル実装してみた。

#!/usr/bin/python3

# -----------------------------
#  fc: 22    TEMP_TXD0
#  fb: 2b    TEMP_TXD1
#  fa: 69    TEMP_TXD2
#  -----------------------------
#  f9: be    PRESS_TXD0
#  f8: 7e    PRESS_TXD1
#  f7: 9f    PRESS_TXD2
#  -----------------------------
#  
#  
#  b8: 81     COE_b00_a0_ex
#  ------------------------
#  b7: 73     COE_a2_0
#  b6: ec     COE_a2_1
#  b5: c4     COE_a1_0
#  b4: e4     COE_a1_1
#  b3: c6     COE_a0_0
#  b2: f7     COE_a0_1
#  b1: d4     COE_bp3_0
#  b0: f6     COE_bp3_1
#  af: 09     COE_b21_0
#  ae: ee     COE_b21_1
#  ad: ec     COE_b12_0
#  ac: 1f     COE_b12_1
#  ab: ac     COE_bp2_0
#  aa: 04     COE_bp2_1
#  a9: 9f     COE_b11_0
#  a8: 0d     COE_b11_1
#  a7: c6     COE_bp1_0
#  a6: 12     COE_bp1_1
#  a5: cd     COE_bt2_0
#  a4: 05     COE_bt2_1
#  a3: 16     COE_bt1_0
#  a2: 13     COE_bt1_1
#  a1: c1     COE_b00_0
#  a0: 5b     COE_b00_1
#  
#  
#         A          S
#  a1  -6.30E-03  4.30E-04
#  a2  -1.90E-11  1.20E-10
#  bt1  1.00E-01  9.10E-02
#  bt2  1.20E-08  1.20E-06
#  bp1  3.30E-02  1.90E-02
#  b11  2.10E-07  1.40E-07
#  bp2  -6.30E-10 3.50E-10
#  b12  2.90E-13  7.60E-13
#  b21  2.10E-15  1.20E-14
#  bp3  1.30E-16  7.90E-17
#  

conversion_factor = {
  'a1':  (-6.30E-03,  4.30E-04),
  'a2':  (-1.90E-11,  1.20E-10),
  'bt1': ( 1.00E-01,  9.10E-02),
  'bt2': ( 1.20E-08,  1.20E-06),
  'bp1': ( 3.30E-02,  1.90E-02),
  'b11': ( 2.10E-07,  1.40E-07),
  'bp2': ( -6.30E-10, 3.50E-10),
  'b12': ( 2.90E-13,  7.60E-13),
  'b21': ( 2.10E-15,  1.20E-14),
  'bp3': ( 1.30E-16,  7.90E-17),
}
cf = conversion_factor

TEMP_TXD0  = 0x22
TEMP_TXD1  = 0x2b    
TEMP_TXD2  = 0x69    
PRESS_TXD0 = 0xbe    
PRESS_TXD1 = 0x7e    
PRESS_TXD2 = 0x9f    

COE_b00_a0_ex = 0x81
C0E_a2_0  = 0x73     
C0E_a2_1  = 0xec     
C0E_a1_0  = 0xc4     
C0E_a1_1  = 0xe4     
C0E_a0_0  = 0xc6     
C0E_a0_1  = 0xf7     
C0E_bp3_0 = 0xd4     
C0E_bp3_1 = 0xf6     
C0E_b21_0 = 0x09     
C0E_b21_1 = 0xee     
C0E_b12_0 = 0xec     
C0E_b12_1 = 0x1f     
C0E_bp2_0 = 0xac     
C0E_bp2_1 = 0x04     
C0E_b11_0 = 0x9f     
C0E_b11_1 = 0x0d     
C0E_bp1_0 = 0xc6     
C0E_bp1_1 = 0x12     
C0E_bt2_0 = 0xcd     
C0E_bt2_1 = 0x05     
C0E_bt1_0 = 0x16     
C0E_bt1_1 = 0x13     
C0E_b00_0 = 0xc1     
C0E_b00_1 = 0x5b     


C0E_b00_ex = COE_b00_a0_ex >> 4
C0E_a0_ex = COE_b00_a0_ex & 0x0F

a1 = cf['a1'][0]   + cf[ 'a1'][1]*((C0E_a1_1<<8)  + C0E_a1_0) / 32767
a2 = cf['a2'][0]   + cf[ 'a2'][1]*((C0E_a2_1<<8)  + C0E_a2_0) / 32767
bt1 = cf['bt1'][0] + cf['bt1'][1]*((C0E_bt1_1<<8) + C0E_bt1_0) / 32767
bt2 = cf['bt2'][0] + cf['bt2'][1]*((C0E_bt2_1<<8) + C0E_bt2_0) / 32767
bp1 = cf['bp1'][0] + cf['bp1'][1]*((C0E_bp1_1<<8) + C0E_bp1_0) / 32767
b11 = cf['b11'][0] + cf['b11'][1]*((C0E_b11_1<<8) + C0E_b11_0) / 32767
bp2 = cf['bp2'][0] + cf['bp2'][1]*((C0E_bp2_1<<8) + C0E_bp2_0) / 32767
b12 = cf['b12'][0] + cf['b12'][1]*((C0E_b12_1<<8) + C0E_b12_0) / 32767
b21 = cf['b21'][0] + cf['b21'][1]*((C0E_b21_1<<8) + C0E_b21_0) / 32767
bp3 = cf['bp3'][0] + cf['bp3'][1]*((C0E_bp3_1<<8) + C0E_bp3_0) / 32767
a0  = ((C0E_a0_1 << 12) + (C0E_a0_0 << 4) + C0E_a0_ex) /16
b00 = ((C0E_b00_1 << 12) + (C0E_b00_0 << 4) + C0E_b00_ex) /16

Dt = (TEMP_TXD2 << 16) +  (TEMP_TXD1 << 8) +  TEMP_TXD0  - 2**23
Dp = (PRESS_TXD2 << 16) + ( PRESS_TXD1 << 8) +  PRESS_TXD0  - 2**23

print(a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3, a0, b00)
print(Dt, Dp)

Tr = a0 + a1 * Dt  + a2 * Dt * Dt
Pr = b00 + bt1 * Tr  + bp1*Dp + b11*Tr*Dp + bt2*Tr*Tr + bp2*Dp*Dp + b12 * Dp*Tr*Tr + b21 * Dp * Dp * Tr + bp3 * Dp * Dp * Dp

print("Tr:", Tr)
print("Pr:", Pr)

出力結果は以下の通り、何か間違っている。やはり先人のコードを見て検証しないとだめだ。

Tr: 72160.48901926124
Pr: 151907.33116591972

Rustで試行錯誤するのでは時間がかかりすぎるので、、比較的分かりやすそうなAliOS-Things様のドライバでPMP6988を制御してみた。結果以下と表示された。温度は大体合ってそうなのだが、気圧が99926ってのはどんな単位なんだろうか。

pressure:99926.062500
temp:33.898438

CALC_INTフラグがあって、これは多分整数演算を優先するモードと思うのだが、これを0にすると結果が少し変わって以下

pressure:100009.906250
temp:33.976463

1hPa = 100Pa らしく、上記の単位がPaだとすると、今の気圧は1000hPa(今日は晴天なので、、そんなものかも)。気象情報のサイトで確認すると、1009hPaとのことで、ちょっと誤差がある。めちゃくちゃ精度が高いと売りなのに、、いいのか?? 標高が関係している??*2 細かい精度はおいといて、だいたい合っている。次はこのPythonコードをばらして、どんな計算をしているのか、その時の係数は何か?を確認すると、自分のコードのどこが間違っているのかわかるはず。。

自分の実装ミス
仕様書から読み解けなかったのだが、係数を生成する時、単にビットを連結していただけだったのだが、この係数は符号付きの値なのであった。だから、符号付き20bitと解釈しないといけない。(単純にunsigned の20bitと考えてのだがそれは間違い)

サンプルソースで、12bit左シフトして、signed型にしてから右12bitシフトしてるのだが、なぜわざわざシフトして戻しているのか最初分からなかった(ソースは下記)。これは係数を12bit左シフトすることで、20bitの最上位を32bit長の最上位bit(MSB)までずらして符号の位置に持って行ってからsigned 32bit長に変換して、符号付き整数に型変換した後、右シフトして元の値に戻しているのであった。

   qmp6988.qmp6988_cali.COE_a0 =
        (QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
                         (a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) |
                         (a_data_uint8_tr[24] & 0x0f))
                        << 12);
    qmp6988.qmp6988_cali.COE_a0 = qmp6988.qmp6988_cali.COE_a0 >> 12;

MicroPyton版ドライバでも同様にunsigned->signedにするためシフトして戻している

        COE_a0 = self.int32(((a_data_u8r[18] << SHIFT_LEFT_12_POSITION) | (a_data_u8r[19] << SHIFT_LEFT_4_POSITION) | (a_data_u8r[24] & 0x0f)) << 12)
        COE_a0 = self.int32(COE_a0 >> SHIFT_LEFT_12_POSITION)

    def int32(self, dat):
        #return int(dat)
        if dat > (1 << 31):
            return dat - (1 << 32)
        else:
            return dat

係数は8bit + 8bitを結合して16bit長にする演算もあり、ソースを確認するとそちらも同様に符号付き16bitと解釈する必要があるようであった。その辺りを修正したら、サンプル実装と同じ値になるのではと期待。

■追記
16bit長の補正用係数、20bit長の補正用係数いずれも符号付き数値と解釈してサンプルコードを作り直した。コードは以下

#!/usr/bin/python3

conversion_factor = {
  'a1':  (-6.30E-03,  4.30E-04),
  'a2':  (-1.90E-11,  1.20E-10),
  'bt1': ( 1.00E-01,  9.10E-02),
  'bt2': ( 1.20E-08,  1.20E-06),
  'bp1': ( 3.30E-02,  1.90E-02),
  'b11': ( 2.10E-07,  1.40E-07),
  'bp2': ( -6.30E-10, 3.50E-10),
  'b12': ( 2.90E-13,  7.60E-13),
  'b21': ( 2.10E-15,  1.20E-14),
  'bp3': ( 1.30E-16,  7.90E-17),
}
cf = conversion_factor

PRESS_TXD2 = 159
PRESS_TXD1 = 114
PRESS_TXD0 = 230
TEMP_TXD2  = 105
TEMP_TXD1  = 53
TEMP_TXD0  = 224


COE_b00_a0_ex = 0x81
COE_a2_0  = 0x73     
COE_a2_1  = 0xec     
COE_a1_0  = 0xc4     
COE_a1_1  = 0xe4     
COE_a0_0  = 0xc6     
COE_a0_1  = 0xf7     
COE_bp3_0 = 0xd4     
COE_bp3_1 = 0xf6     
COE_b21_0 = 0x09     
COE_b21_1 = 0xee     
COE_b12_0 = 0xec     
COE_b12_1 = 0x1f     
COE_bp2_0 = 0xac     
COE_bp2_1 = 0x04     
COE_b11_0 = 0x9f     
COE_b11_1 = 0x0d     
COE_bp1_0 = 0xc6     
COE_bp1_1 = 0x12     
COE_bt2_0 = 0xcd     
COE_bt2_1 = 0x05     
COE_bt1_0 = 0x16     
COE_bt1_1 = 0x13     
COE_b00_0 = 0xc1     
COE_b00_1 = 0x5b     


COE_b00_ex = COE_b00_a0_ex >> 4
COE_a0_ex = COE_b00_a0_ex & 0x0F

import pdb

def cnv2s16(val):
  #print(hex(val))
  if val & 0x80_00 == 0:
      pass
  else:
      val = val - 0x1_00_00
  #pdb.set_trace()
  return val

def cnv2s20(val):
  #print(hex(val))
  if val & 0x8_00_00 == 0:
      pass
  else:
      val = val - 0x10_00_00
  #pdb.set_trace()
  return val

a1 = cf['a1'][0]   + cf[ 'a1'][1]*cnv2s16((COE_a1_1<<8)  + COE_a1_0) / 32767
a2 = cf['a2'][0]   + cf[ 'a2'][1]*cnv2s16((COE_a2_1<<8)  + COE_a2_0) / 32767
bt1 = cf['bt1'][0] + cf['bt1'][1]*cnv2s16((COE_bt1_1<<8) + COE_bt1_0) / 32767
bt2 = cf['bt2'][0] + cf['bt2'][1]*cnv2s16((COE_bt2_1<<8) + COE_bt2_0) / 32767
bp1 = cf['bp1'][0] + cf['bp1'][1]*cnv2s16((COE_bp1_1<<8) + COE_bp1_0) / 32767
b11 = cf['b11'][0] + cf['b11'][1]*cnv2s16((COE_b11_1<<8) + COE_b11_0) / 32767
bp2 = cf['bp2'][0] + cf['bp2'][1]*cnv2s16((COE_bp2_1<<8) + COE_bp2_0) / 32767
b12 = cf['b12'][0] + cf['b12'][1]*cnv2s16((COE_b12_1<<8) + COE_b12_0) / 32767
b21 = cf['b21'][0] + cf['b21'][1]*cnv2s16((COE_b21_1<<8) + COE_b21_0) / 32767
bp3 = cf['bp3'][0] + cf['bp3'][1]*cnv2s16((COE_bp3_1<<8) + COE_bp3_0) / 32767

a0  = cnv2s20((COE_a0_1 << 12) + (COE_a0_0 << 4) + COE_a0_ex) / 16
b00 = cnv2s20((COE_b00_1 << 12) + (COE_b00_0 << 4) + COE_b00_ex) / 16


Dt = (TEMP_TXD2 << 16) +  (TEMP_TXD1 << 8) +  TEMP_TXD0  - 2**23
Dp = (PRESS_TXD2 << 16) + ( PRESS_TXD1 << 8) +  PRESS_TXD0  - 2**23

print("------------------------")
print("Dt", Dt)
print("Dp", Dp)
print("------------------------")


Tr = a0 + a1 * Dt  + a2 * Dt * Dt
Pr = b00 + bt1 * Tr  + bp1*Dp + b11*Tr*Dp + bt2*Tr*Tr + bp2*Dp*Dp + b12 * Dp*Tr*Tr + b21 * Dp * Dp * Tr + bp3 * Dp * Dp * Dp

print("Tr:", Tr)
print("Tr:", Tr/256.0)


print("Pr:", Pr)
print("Pr:", Pr/100.0, "hPa")

実行結果は以下となり、計測タイミングの違い、丸め誤差?で結果に差があるが、サンプルコードによる出力とかなり近い値になっている。

------------------------
Dt -1493536
Dp 2061030
------------------------
Tr: 7356.718938025345
Tr: 28.737183351661503
Pr: 100029.38430679489
Pr: 1000.293843067949 hPa

Pythonによる計算式はできたと判断して、これをRust版に置き換えると・・
■追記
かつて発生して悩まされた? wait usb downloadのメッセージで止まってしまう件が再発した、これはbootモードがおかしくなっているためと推測。GPIO9がbootモードの選択を担当しており、誤ってLになっていたりするとReset投入後Downloadモードかなにかになってしまう(たしか。仕様書で再確認必要だが)。このような状況になったら、GPIO9を確実にHに固定するため、もしオープンだったらPullUp等を介して3.3Vに引き上げると通常通りにBootする可能性が高い。自分の場合は、GPIO9をH固定にしたら、起動の問題は解消した。一度正常に起動すると、GPIO9のH固定をはずしてオープンにしても普通に起動するのであった。ただ、、危ういのは危ういので、PullUpしておくのが無難かも

~/lang/rust/esp32-c3/qmp6988$ cargo espflash monitor
[2024-07-07T14:44:28Z INFO ] 🚀 A new version of cargo-espflash is available: v3.1.0
[2024-07-07T14:44:28Z INFO ] Serial port: '/dev/ttyACM0'
[2024-07-07T14:44:28Z INFO ] Connecting...
[2024-07-07T14:44:29Z INFO ] Using flash stub
Commands:
    CTRL+R    Reset chip
    CTRL+C    Exit

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0x0 (USB_BOOT)
Saved PC:0x4038063e
wait usb download

■追記
Rustによるドライバは以下となった(係数計算手抜き版)
短い理由:補正用の係数計算はQMP6988のレジスタから値を取ってきて計算式を適用すべきなのだが、レジスタはいつも同じ値のようなので、あらかじめPython等で計算した定数で代用する*3

pub struct QMP6988 {
}

impl QMP6988 {

  const a0 :f32 = -2105.9375;
  const a1 :f32 = -0.006391493270668661;
  const a2 :f32 = -3.732941679128391e-11;
  const b00:f32 = 23489.5;
  const bt1:f32 = 0.11356932279427473;
  const bt2:f32 = 6.638398388622699e-08;
  const bp1:f32 = 0.03578676717429121;
  const b11:f32 = 2.248985259559923e-07;
  const bp2:f32 = -6.172249519333475e-10;
  const b12:f32 = 4.795419171727653e-13;
  const b21:f32 = 4.15744499038667e-16;
  const bp3:f32 = 1.2433906064027832e-16;

  pub fn calc_temp_press(PRESS_TXD2:u8, PRESS_TXD1:u8, PRESS_TXD0:u8, TEMP_TXD2:u8, TEMP_TXD1:u8, TEMP_TXD0:u8) -> (f32, f32){

     let Dt = (((TEMP_TXD2 as u32) << 16) + ((TEMP_TXD1 as u32) << 8) + TEMP_TXD0 as u32) as f32 - 2_u64.pow(23) as f32;
     let Dp = (((PRESS_TXD2 as u32) << 16) + (( PRESS_TXD1 as u32) << 8) + PRESS_TXD0 as u32) as f32 - 2_u64.pow(23) as f32;
     let Tr = QMP6988::a0 + QMP6988::a1 * Dt + QMP6988::a2 * Dt * Dt;
     let Pr = QMP6988::b00 + QMP6988::bt1 * Tr + QMP6988::bp1 * Dp + QMP6988::b11 * Tr * Dp + QMP6988::bt2 * Tr * Tr + QMP6988::bp2 * Dp * Dp + QMP6988::b12 * Dp * Tr * Tr + QMP6988::b21 * Dp * Dp * Tr + QMP6988::bp3 * Dp * Dp * Dp;
     (Tr/256.0, Pr/100.0)

  }

}

インスタンス変数?使っていないのでStruct型にしなくてもいい感じですが、、ゆくゆくは係数も正しく計算したいので、一旦Struct型で実装。理想的にはSoftI2CドライバのインスタンスはQMP6988に持たせたい。

■参考URL
原因特定につながると期待できるリファレンス実装(Arduino)
M5Unit-ENV/src/QMP6988.cpp at master · m5stack/M5Unit-ENV · GitHub
Python版ドライバ(比較的分かりやすい実装)
AliOS-Things/haas_lib_bundles/python/libraries/qmp6988/qmp6988.py at master · alibaba/AliOS-Things · GitHub
仕様書
QMP6988 Datasheet PDF - Digital Barometric Pressure Sensor

*1:引用したものの、この仕様書には何点か間違いがある [QMP6988] Preliminary Datasheet Rev.03(Dec.2016)の方が正確かと

*2:10mごとに約1hPaの割合で低くなるそうだ。若干高いのは高いが標高90mもないと思うのだが

*3:他のやるべきことをやり終えたら最後に正しい実装にしたい