高精度気圧センサーモジュール - DPS310 をPythonで動くようにした


よく調べようという教訓が得られます。

2021/10/12：コード修正（2個目以降のファイルの値が更新されていなかった）

# 初めに
Raspberry Piで気圧ロガーを作るためにセンサーモジュールを買ったが、販売元のページにはArduino用のCライブラリしか無かったので、仕方なくPythonのドライバを書いたら、実はチップメーカーのGithubにすでにPythonのコードが公開されていた。というオチの記録です。

## 目標

- 気圧センサを使用したデータロガーを作る。

## 環境

- Raspberry Pi 4 model B 4GB（Raspberry Pi OS arm64 Release:10 Codename:buster )
    - [この記事](https://qiita.com/emguse42/items/1c14d1dca662f16ff394)で作成したDockerコンテナ
    - Python 3.9.2
    - RPi.GPIO, i2c-tools, smbus :などが利用可能
    - grove.py :Grove Base Hat for Raspberry PiとGroveシステムのセンサ類のライブラリ
- Grove Base Hat for Raspberry Pi 
    - GPIO,I2C,ADC,URAT,PWMを手軽に使えるようにするコネクターがいっぱい生えたHAT、便利
- Grove - High Precision Barometric Pressure Sensor DPS310
    - 高精度な気圧センサモジュール
    - infineon社のDPS310を搭載
    - I2C接続で使用

# リソース
[販売店のページ](https://www.switch-science.com/catalog/6286/)には以下のリンクがある。

- [製造元Wiki](https://wiki.seeedstudio.com/Grove-High-Precision-Barometric-Pressure-Sensor-DPS310/)：Seeed Technology社解説Wikiページへのリンク
- [Githubライブラリ](https://github.com/Infineon/DPS310-Pressure-Sensor)：搭載センサーのメーカーinfineonのDPS310ライブラリリポジトリ（Arduino用）
- [データシート](https://files.seeedstudio.com/products/101020812/res/DPS310-datasheet.pdf)：搭載センサーのメーカーinfineonのDPS310データシートへのリンク（リンク先はSeeed社）
- [回路図](https://files.seeedstudio.com/wiki/Grove-High-Precision-Barometer-Sensor-DPS310/res/Grove%20-%20High%20Precision%20Barometer%20Sensor%20(DPS310)_v1.0.zip)：モジュールの回路図やCADデータ

製造元WikiにはArduino用のリソースはあるがRaspberry Pi用はToDoとなっており[記載がない](https://wiki.seeedstudio.com/Grove-High-Precision-Barometric-Pressure-Sensor-DPS310/#platforms-supported)
製造元Wikiに記載があるSeeed社の[リポジトリ](https://github.com/Seeed-Studio/Seeed_Arduino_DPS310)もArduino用でした。

ここまで見て仕方ないので自分で書くしかないかなと思いセンサーを注文。

# Pythonコードの作成
データシートとにらめっこしながらPythonでドライバを書きます。
ついでにCSVファイルを保存してデータロガーにしてしまいます。
[infineon社ライブラリのCのコード](https://github.com/Infineon/DPS310-Pressure-Sensor/blob/dps310/src/Dps310.cpp)を多分に参考してます。[参考：ライセンス](https://github.com/Infineon/DPS310-Pressure-Sensor/blob/dps310/LICENSE.md)

```python:pressure_DPS310_csv.py
import time
from grove.i2c import Bus
import multi_timer
import csv
import datetime

READ_INTERVAL = 1/128 # [sec]
FILE_SAVE_INTERVAL = 3600 # [sec]
SAVE_DIR = 'press_log/'

# i2c address setting
ADDRESS = 0x77

# Pressure Configuration (PRS_CFG) 
PRESS_CONF = 0x71 
    # Pressure measurement rate: 0111 XXXX- 128 measurements pr. sec.
    # Pressure oversampling rate: XXXX 0001 - 2 times (Low Power).  Precision : 1 PaRMS.
    # 0111 0001 = 0x71 highest measurements rate 
'''DPS310 data sheet P.29 - 30
Pressure measurement rate: **)      |Pressure oversampling rate:
0000 - 1 measurements pr. sec.      |0000 - Single. (Low Precision)
0001 - 2 measurements pr. sec.      |0001 - 2 times (Low Power).
0010 - 4 measurements pr. sec.      |0010 - 4 times.
0011 - 8 measurements pr. sec.      |0011 - 8 times.
0100 - 16 measurements pr. sec.     |0100 *)- 16 times (Standard).
0101 - 32 measurements pr. sec.     |0101 *) - 32 times.
0110 - 64 measurements pr. sec.     |0110 *) - 64 times (High Precision).
0111 - 128 measurements pr. sec.    |0111 *) - 128 times.
Applicable for measurements         |1xxx - Reserved
in Background mode only             |*) Note: Use in combination with a bit shift.
**) note: There is a limit to the oversampling rate setting from 8 measurements pr. sec.
        (At 128 measurements per second, the oversampling rate is limited to 2)
'''

# Temperature Configuration(TMP_CFG) 
TEMP_CONF = 0xF0
    # Temperature measurement: 1XXX XXXX - External sensor (in pressure sensor MEMS element)
    # Temperature measurement rate: X111 XXXX - 128 measurements pr. sec.
    # Temperature oversampling (precision): 0000 - single. (Default) - Measurement time 3.6 ms.
    # 1111 0000 = 0xF0 highest measurements rate 
'''DPS310 data sheet P.31
Temperature measurement                                 |
0 - Internal sensor (in ASIC)                           |
1 - External sensor (in pressure sensor MEMS element)   |
Temperature measurement rate:                           |Temperature oversampling (precision):
000 - 1 measurement pr. sec.                            |0000 - single. (Default) - Measurement time 3.6 ms.
001 - 2 measurements pr. sec.                           |
010 - 4 measurements pr. sec.                           |
011 - 8 measurements pr. sec.                           |
100 - 16 measurements pr. sec.                          |
101 - 32 measurements pr. sec.                          |
110 - 64 measurements pr. sec.                          |
111 - 128 measurements pr. sec.                         |
Applicable for measurements in Background mode only     |
'''

# Interrupt and FIFO configuration (CFG_REG)
INT_AND_FIFO_CONF = 0x00
    # T_SHIFT: bit3: Temperature result bit-shift: 0 - no shift result right in data register.
    # P_SHIFT: bit2: Pressure result bit-shift: 0 - no shift result right in data register.
    # not use interrupts.: 0000 XXXX
    # not use FIFO.      : XXXX XX0X
    # not use SPI.       : XXXX XXX0
    # 0000 1100 = 0x00
'''DPS310 data sheet P.33
If don't use interrupts. = 0000 XXXX 
T_SHIFT     | 3     | rw    | Temperature result bit-shift
                            | 0 - no shift.
                            | 1 - shift result right in data register.
                            | Note: Must be set to '1' when the oversampling rate is >8 times.
P_SHIFT     | 2     | rw    | Pressure result bit-shift
                            | 0 - no shift.
                            | 1 - shift result right in data register.
                            | Note: Must be set to '1' when the oversampling rate is >8 times.
If don't use FIFO.  = XXXX XX0X
If don't use SPI.   = XXXX XXX0
'''

# Sensor Operating Mode and Status (MEAS_CFG)
OP_MODE = 0x07
# 111 - Background Mode Continous pressure and temperature measurement
# 0000 0111 = 0x07
'''DPS310 data sheet P.32
Bits 7 to 4 are read-only and various ready statuses.
Bit 3 is reserved.
MEAS_CTRL   | 2:0   | rw    | Set measurement mode and type:
                            | Standby Mode
                            | 000 - Idle / Stop background measurement
                            | Command Mode
                            | 001 - Pressure measurement
                            | 010 - Temperature measurement
                            | 011 - na.
                            | 100 - na.
                            | Background Mode
                            | 101 - Continous pressure measurement
                            | 110 - Continous temperature measurement
                            | 111 - Continous pressure and temperature measurement
'''

# Compensation Scale Factors
SCALE_FACTOR_KP = 1572864 # Oversampling Rate 2 times (Low Power)
SCALE_FACTOR_TP = 524288 # Oversampling Rate 1 (single)
'''DPS310 data sheet P.15
Compensation Scale Factors
Oversampling Rate           | Scale Factor (kP or kT)   | Result shift ( bit 2and 3 address0x09)
-------------------------------------------------------------------------------------------------
1 (single)                  | 524288                    | 0
2 times (Low Power)         | 1572864                   | 0
4 times                     | 3670016                   | 0
8 times                     | 7864320                   | 0
16 times (Standard)         | 253952                    | enable pressure or temperature shift
32 times                    | 516096                    | enable pressure or temperature shift
64 times (High Precision)   | 1040384                   | enable pressure or temperature shift
128 times                   | 2088960                   | enable pressure or temperatureshift
'''

class pressure_sensor_DPS310():
    def __init__(self):
        time.sleep(0.1)
        # I2C bus
        self.bus = Bus(None)
        # Measurement Settings
        self.bus.write_byte_data(ADDRESS, 0x06, PRESS_CONF) 
        self.bus.write_byte_data(ADDRESS, 0x07, TEMP_CONF)
        self.bus.write_byte_data(ADDRESS, 0x09, INT_AND_FIFO_CONF)
        self.bus.write_byte_data(ADDRESS, 0x08, OP_MODE)

    def __getTwosComplement(self, raw, length):
       value = raw
       if raw & (1 << (length - 1)):
           value = raw - (1 << length)
       return value

    def read_calibration_coefficients(self):
        # Read Calibration Coefficients
        reg = {}
        for i in range(0x10, 0x22):
            reg[i] = self.bus.read_byte_data(ADDRESS,i)
        Factors = {}
        Factors['c0'] = self.__getTwosComplement(((reg[0x10]<<8 | reg[0x11])>>4), 12)
        Factors['c1'] = self.__getTwosComplement(((reg[0x11] & 0x0F)<<8 | reg[0x12]), 12)
        Factors['c00'] = self.__getTwosComplement((((reg[0x13]<<8 | reg[0x14])<<8 | reg[0x15])>>4), 20)
        Factors['c10'] = self.__getTwosComplement((((reg[0x15] & 0x0F)<<8 | reg[0x16])<<8 | reg[0x17]), 20)
        Factors['c01'] = self.__getTwosComplement((reg[0x18]<<8 | reg[0x19]), 16)
        Factors['c11'] = self.__getTwosComplement((reg[0x1A]<<8 | reg[0x1B]), 16)
        Factors['c20'] = self.__getTwosComplement((reg[0x1C]<<8 | reg[0x1D]), 16)
        Factors['c21'] = self.__getTwosComplement((reg[0x1E]<<8 | reg[0x1F]), 16)
        Factors['c30'] = self.__getTwosComplement((reg[0x20]<<8 | reg[0x21]), 16)
        return Factors

    def __calc_temp(self, raw_temp, Factors):
        scaled_temp = raw_temp / SCALE_FACTOR_TP # Traw_sc = Traw/kT
        compd_temp = Factors['c0'] * 0.5 + Factors['c1'] * scaled_temp # Tcomp (°C) = c0*0.5 + c1*Traw_sc
        return scaled_temp, compd_temp

    def read_temperature(self, Factors):
        reg = {}
        # read raw temperature
        for i in range(0x03, 0x06):
            reg[i] = self.bus.read_byte_data(ADDRESS,i)
        raw_temp = self.__getTwosComplement(((reg[0x03]<<16) | (reg[0x04]<<8) | reg[0x05]), 24)
        # calculate temperature
        scaled_temp, compd_temp = self.__calc_temp(raw_temp, Factors)
        return scaled_temp, compd_temp

    def __calc_press(self, raw_press, scaled_temp, Factors):
        # Praw_sc = Praw/kP
        scaled_press = raw_press / SCALE_FACTOR_KP
        # Pcomp(Pa) = c00 + Praw_sc*(c10 + Praw_sc *(c20+ Praw_sc *c30)) 
        #                + Traw_sc *c01 + Traw_sc *Praw_sc *(c11+Praw_sc*c21) 
        compd_press = Factors['c00'] + scaled_press * (Factors['c10'] + scaled_press\
                         * (Factors['c20'] + scaled_press * Factors['c30']))\
                            + scaled_temp * Factors['c01'] + scaled_temp * scaled_press\
                                 * (Factors['c11'] + scaled_press * Factors['c21'])
        return compd_press

    def read_pressure(self, scaled_temp, Factors):
        reg = {}
        for i in range(0x00, 0x03):
            reg[i] = self.bus.read_byte_data(ADDRESS,i)
        raw_press = self.__getTwosComplement(((reg[0x00]<<16) | (reg[0x01]<<8) | reg[0x02]), 24)
        compd_press = self.__calc_press(raw_press,scaled_temp, Factors)
        return compd_press

def main():
    bus = Bus(None)
    INTERVAL = READ_INTERVAL
    timer = multi_timer.multi_timer(INTERVAL) 

    dps310 = pressure_sensor_DPS310() # Instance creation

    Factors = dps310.read_calibration_coefficients() # Read Calibration Coefficients

    while True:
        today = datetime.date.today()
        filename = str(SAVE_DIR + today.strftime('%Y%m%d') + '-' + time.strftime('%H%M%S') + '.csv')
        try:
            with open(filename, 'w', newline='') as f:
                bus.write_byte_data(ADDRESS, 0x08, OP_MODE)
                writer = csv.writer(f)
                file_start_time = time.time()
                while True:
                    timer.timer()
                    if timer.up_state == True:
                        timer.up_state = False

                        # process
                        scaled_temp ,temperature = dps310.read_temperature(Factors) # read and compensation temperature
                        press = dps310.read_pressure(scaled_temp, Factors) # read and compensation pressure
                        #print(str(time.time()) + "," + str(press))
                        data = [str(time.time()),str(press)]
                        writer.writerow(data)
                    if file_start_time+FILE_SAVE_INTERVAL <= time.time():
                        break
        finally:
            bus.write_byte_data(ADDRESS, 0x08, 0x00)

if __name__ == "__main__":
  main()
```

上記コードは、時間分解能を上げたかったのでハイレートなセッティングです。
せっかくの精度を生かすなら4サンプル/secくらいで定数を
`READ_INTERVAL = 1/4` # [sec]
`PRESS_CONF = 0x2E` # 4spp 64 times oversampling
`TEMP_CONF = 0xA0` # 4spp
`INT_AND_FIFO_CONF = 0x04` # Pressure result bit-shift enable
`SCALE_FACTOR_KP = 1040384` # 64 times (High Precision) 
こんな感じに設定するといいと思います。

importしているタイマーは自作のクラスで単純なポーリングタイマーです。

```python:multi_timer.py
import time

class multi_timer():
    '''
    Multiple timers can be used simultaneously in this class.
    Create an instance with the interval [sec] as an argument.
    '''
    def __init__(self, interval):
        self.last_time = 0.0
        self.up_state = False
        self.interval = interval
        
    def timer(self):
        '''
        The timer method compares the current time with the interval 
        and updates the up_state at each call.
        '''
        if self.last_time + self.interval <= time.time():
            self.last_time = time.time()
            self.up_state = True

# Usage Example
def main(): 
    INTERVAL_1s = float(1.0) # Enter the interval time in seconds
    INTERVAL_10s = float(10.0)  # Enter the interval time in seconds

    # timer instance creation
    timer_1s = multi_timer(INTERVAL_1s)
    timer_10s = multi_timer(INTERVAL_10s)

    while True:
        timer_1s.timer() # Call the method to update the timer
        if timer_1s.up_state == True:
            timer_1s.up_state = False # required to manually rewrite 'up_state' to False
            # Write the process here
            print("1sec: " + str(time.time()))
        timer_10s.timer()
        if timer_10s.up_state ==True:
            timer_10s.up_state = False # required to manually rewrite 'up_state' to False
            # Write the process here
            print("10sec: " + str(time.time()))
    return

if __name__ == "__main__":
  main()
```

実行するとカレント配下の`press_log/`にCSVファイルが出力されます。

```csvs:date-time.csv
1633533232.8374667,97542.54109208909
1633533232.8451686,97542.48074125986
1633533232.8530242,97543.69100189407
1633533232.8607929,97543.91064694956
1633533232.8689406,97543.04499677646
1633533232.8765638,97544.49214130614
1633533232.884714,97543.75997487147
1633533232.8925848,97543.13983414807
1633533232.9000964,97543.26033221664
1633533232.9077988,97542.63161833295
```

# 書いている途中（ほぼ完成）で見つけてしまったリポジトリ

https://github.com/Infineon/RaspberryPi_DPS

あるじゃないですか（涙）

#おわりに

デバイスの仕様書を読みながらセンサを使えるようにするのは良いトレーニングになります。（涙）
動作はしましたが、精度の検証とかテストはしていないのでバグがあったらすみません。

# 参考文献
探すのを止めた時見つかることはよくある話で
- [Infineon/RaspberryPi_DPS](https://github.com/Infineon/RaspberryPi_DPS)
- [高精度気圧センサDPS310の実力を試してみた 【前編】](https://houwa-js.co.jp/2018/12/20181211/)
- [高精度気圧センサDPS310の実力を試してみた 【後編】](https://houwa-js.co.jp/2018/12/20181212/)