差圧センサDLHR-F50DをRaspberry Pi + Pythonで使えるようにする

All sensors (Amphenol)製 差圧センサー DLHR-F50D-E1BD-C-NAV8 をRaspberry Pi + Pythonで動かしてみた記録です。

## はじめに
差圧を計測してあれこれ検出するデバイスを作ってみたくて、買ってきたセンサーで値を取得するところまでの記録となります。
I2Cでアドレスを指定せずにデータを読み書きした経験がなく、「アドレスは指定するもの」という思い込みで`read_byte` の存在を知らずステータスの読み出しではまりました。

## 環境

- Raspberry Pi 4 model B 4GB（Raspberry Pi OS arm64 Release:10 Codename:buster ) 
- Python: Python 3.9.2

## センサーの選定

趣味の工作なので手に入るならOKな感じで選びます。

###要求
| 項目 | 説明 |
|:-|:-|
|高感度   |数Paを検出したい   |
|測定周波数   |40Hz   |
|ポート   |チューブ接続が可能   |
|電源電圧   |3.3Vで動くとうれしい   |
|接続   |デジタル、I2Cだと楽  |
|パッケージ   |使いやすければよし   |

###候補

| メーカー | 型式 | 仕様 |
|:-|:-|:-|
|All sensors (Amphenol)   |DLHR-F50D   |±125Pa   |
|Honeywell   |HSCDRRN001ND2A3   |±125Pa   |
|Sensirion   |SDP810-125PA   |±125Pa   |
|Omron   |D6F-PH0505   |±50Pa   |

お財布と相談しながら実験のしやすさ等考慮した結果

- [All sensors (Amphenol) 製 DLHR-F50D-E1BD-C-NAV8](https://www.allsensors.com/products/dlhr-series)

を選定。

###主な仕様

| 項目 | 仕様 |
|:-|:-|
|メーカー   |All sensors (Amphenol)   |
|型式   |DLHR-F50D-E1BD-C-NAV8   |
|レンジ   |±125Pa(±0.5inH2O) 　|
|デジタル分解能   |17.7bit   |
|データ更新時間   |4.1ms max(Single測定) , 61.9ms max(16回平均化)   |
|耐圧   |25kPa   |
|電圧   |1.75 to 3.60 Vdc   |
|接続   |I2C / SPI   |
|パッケージ   |DIP8ピン   |

## コード

[仕様書](https://www.allsensors.com/datasheets/DS-0350_Rev_C.pdf)を読んで書きます

```python
from smbus2 import SMBus
import time

ADDRESS = 0x29

OS_DIG = 0.5 * 2 ** 24 # For Differential Operating Range sensors
FSS_IN_H2O = 2 * 0.5 # For Differential Operating Range sensors : 2 x Full Scale Pressure.
PA_CONVERSION = 249.089 # Conversion factor from "inH2O" to "Pa"
FSS_PA = FSS_IN_H2O * PA_CONVERSION # FSS/2 for "Pa"

START_SINGLE = 0xAA
START_AVERAGE2 = 0xAC
START_AVERAGE4 = 0xAD
START_AVERAGE8 = 0xAE
START_AVERAGE16 = 0xAF

class DifferentialPressureSensorDLHR_F50D():
    '''
    ### Class for using All sensoers (Amphenol) DLHR-F50D-E1BD-C-NAV8 with Raspberry Pi
    For simple use, you can instantiate it and then call the method "read_p()" to get the differential pressure in the return value.
    '''
    def __init__(self) -> None:
        self.address = ADDRESS
        self.status = 0x00
        self.power = False
        self.busy = False
        self.mode = 0x00
        self.memory_error = False
        self.alu_error = False
        self.rawp = 0
        self.rawt = 0
        self.pressure = 0
        self.temprature = 0
        self.bus = SMBus(1)
        pass

    def send_start(self) -> None:
        '''Send Measurement Commands to the sensor when called'''
        self.bus.write_byte(self.address, START_AVERAGE4)
    
    def status_read(self) -> None:
        self.status = self.bus.read_byte(self.address)
        if self.status & 0x40 == 0x40:
            self.power = True
        else:
            self.power = False
        if self.status & 0x20 == 0x20:
            self.busy = True
        else:
            self.busy = False
        self.mode = (self.status & 0x18) >> 3
        if self.status & 0x04 == 0x04:
            self.memory_error = True
        else:
            self.memory_error = False
        if self.status & 0x01 == 0x01:
            self.alu_error = True
        else:
            self.alu_error = False

    def read_busy(self) -> bool:
        '''
        ### After reading the status register, it retrieves only the Busy status from the result and returns the result.
        Return:
            Sensor busy status (bool)
        '''
        self.status = self.bus.read_byte(self.address)
        if self.status & 0x20 == 0x20:
            self.busy = True
        else:
            self.busy = False
        return self.busy
    
    def poll_busy(self):
        '''Keep reading the status until Busy becomes False.'''
        while self.read_busy():
            pass
    
    def correction_p(self) -> None:
        '''Returns the raw pressure value as a translated pressure value.'''
        self.pressure = 1.25 * ((self.rawp - OS_DIG)/ 2**24) * FSS_PA

    def correction_t(self) -> None:
        '''Returns the raw temperature value as a translated temperature value.'''
        self.temprature = (self.rawt * 125)/ 2**24 - 40

    def read(self):
        '''Get the raw value from the sensor.'''
        data = self.bus.read_i2c_block_data(self.address, 0x00, 7)
        self.rawp = (data[1] << 16) | (data[2] << 8) | data[3]
        self.rawt = (data[4] << 16) | (data[5] << 8) | data[6]

    def read_p(self) -> float:
        '''
        ### Get the corrected differential pressure
        Return:
            Corrected differential pressure
        '''
        self.send_start()
        self.poll_busy()
        self.read()
        self.correction_p()
        return self.pressure
    
    def read_t(self) -> float:
        '''
        ### Get the corrected temprature
        Return:
            Corrected temprature
        '''
        self.send_start()
        self.poll_busy()
        self.read()
        self.correction_t()
        return self.temprature

def main(): # Sample usage
    ZERO_OFFSET = 2 # Zero point correction
    dlhr_f50d = DifferentialPressureSensorDLHR_F50D()
    while True:
        p = dlhr_f50d.read_p()
        print("pressure :" + str(round(p - ZERO_OFFSET ,4)))
        time.sleep(1)

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

##はまりポイント

このセンサーは`EOC`ピンの状態を監視することで、測定開始コマンド書き込み後にセンサーが読み取りReadyになったのかを知ることができますが、`EOC`ピンを使用しない場合は、`Status Read`コマンドを送信し続けて`Busy`をポーリングすることができます。

仕様書によると、ステータスを読み出すには

> When requesting sensor status over I2C, the host simply performs a 1-byte read transfer.
> Read Sensor Status   |  i2C  |   Read of 1 byte from device.

とだけ書いてあります。
これをよく考えずに`Read Sensor Data`では最初の１バイトで返ってくるみたいだし、
`0x00`を読めばよいと勝手に解釈して、

```python
from smbus2 import SMBus
bus = SMBus(1)
status = bus.read_byte_data(0x29, 0x00)
print(status)
```

こう書くと電源のステータスと合わせて`status = 0b01100000 = 0x60 = 96`が返ってきて永遠にBusyのままです。
まだ`Measurement Command`を送ってないのにBusyが立ってるのはおかしいので、「壊しちゃったかな？」とか考えながら、とりあえず`Measurement Command`してからしばらくして`Read Sensor Data`するとちゃんと値が返ってきます。
Busyが正しく読めないとtime.sleep()などで待ってから読み出すしかありません。
困ったなぁと思いながらしばらく経って[smbus2のページ](https://pypi.org/project/smbus2/)をよく読んでみると、1バイト読み出す関数は2つあって、

レジスタアドレスを指定して読み出す`read_byte_data`と

```python
read_byte_data(i2c_addr, register, force=None)
```

単に1バイトを読み出す`read_byte`がありました。

```python
read_byte(i2c_addr, force=None)
```

> Read of 1 byte from device.

と指示され、読み出すアドレスの指定がないわけですから、`read_byte_data`ではなく `read_byte`を使わないといけなかったわけです。
`Measurement Command`は`write_byte_data`じゃなくてちゃんと`write_byte`使って書いてたのになぜ気が付かなかったのか...

## 参考リンク
[センサー仕様書](https://www.allsensors.com/datasheets/DS-0350_Rev_C.pdf)
[smbus2](https://pypi.org/project/smbus2/)