背景:部屋の空気質(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