見出し画像

製品レビュー|電子機器8:光音響NDIR方式CO2センサ(SCD40)


1.概要

 購入した製品の使い方および感想用記事です。
 今回は「SCD40使用-光音響NDIR方式 CO2センサモジュール(5,980円/個(税込))」をレビューしました。

【参考:他の型式(SCD4x)】
 より高精度/広範囲なSCD41もありますが、センサだけで5,000円を超え、モジュールだと9,300円前後します。

https://sensirion.com/media/documents/0DCA7C78/640AF6F5/Sensirion_SCD4x_CO2_brochure_Japanese.pdf

 また性能の違いが分かっていませんがSensirion SCD30の方も同等くらいの値段はします(秋月電子で9,200(円/個(税込))。

1-1.基本仕様

 SENSIRION社の光音響NDIR方式(PASens®)センサSCD40を使用したCO2(二酸化炭素)センサモジュールであり、温度・湿度も同時計測できます。

 1-1-1.仕様の概要

 基本の概要は下記参照。値は公式の「技術ダウンロード」から参照

  1. 電源電圧:1.8~5V

    • 昇降圧スイッチング電圧レギュレータおよびロジックレベル変換回路内蔵により、低い電圧でも使用可能

  2. CO2濃度

    • 測定レンジ:400~2000ppm

    • 精度:±50ppm+読み取り値の5%

    • 応答速度:$${T_{63}}$$までに90sec

  3. 周囲温度

    • 測定レンジ:-10~60℃

    • 精度:±0.8~1.5℃

    • 応答速度:$${T_{63}}$$までに120sec

  4. 湿度

    • 測定レンジ:0~100%RH

    • 精度:±6~9%RH

    • 応答速度:$${T_{63}}$$までに60sec

  5. インターフェイス:I2C(Qwiic/STEMMA QT ピン互換)

  6. 周辺環境

    • 温度:-10~60℃

https://www.sensirion.com/jp/products/product-catalog/SCD40/

 1-1-2.ピン配置

 ピンの配置は下図の通り

1-2.詳細仕様

 1-2-1.図面

【回路図・パーツリスト】

 参考までに型式よりそれぞれの部品は下記の通りです。

  • CN1(SM04B-SRSS-TB):コネクタ

  • U1(TPS63802):昇降圧コンバータ

  • U2(TPS74033SF5):電圧レギュレーター

  • U3(FXMA2102):デュアル電圧供給トランスレータ(レベル変換回路)

    • 広範な入出力電圧レベルにわたって双方向電圧トランスレーションに対応します。I2Cバス・インターフェースを使用するアプリケーションでの電圧トランスレータとして使用されることを意図したものであり、入出力電圧レベルはI2Cデバイス仕様の電圧レベルに準拠

    • プルアップ抵抗器(10kΩ)が搭載されており、外部でのプルアップが不要

  • U4(SCD40):CO2センサー

  • L1(DFE322512F-R47M):パワーインダクタ - SMD

  • C1,2,3,5,9,10(GRM188R61A226M):一般用チップ積層セラミックコンデンサ

  • C6,7,8(GRM033R6YA104K):一般用チップ積層セラミックコンデンサ

  • R1(RK73Z1ETTP):抵抗器

  • R3(RK73H1ETTP6803F):抵抗器

  • R4(RK73H1ETTP9102F):厚膜抵抗器 - SMD 0.1watts 91Kohms 1%

  • R5,6,7,8(RK73B1ETTP103J):厚膜抵抗器 - SMD 0.1W 10Kohms 5%


https://www.digikey.jp/ja/products/detail/jst-sales-america-inc/SM04B-SRSS-TB/926710

【SCD4x Block Diagram】

 1-2-2.I2Cインターフェス

 通信はシリアル通信I2Cでありアドレスは0x62(7bit)です。

 1-2-3.SCD4xコマンド一覧

 SCD4xのコマンド一覧は下記の通り。

2.製品原理

 製品の動作原理に関する部分を説明します。

2-1.NDIRの種類

 NDIRセンサ技術として下記があります。

 NDIRには「透過型NDIR」と「光音響(Photoacoustic)NDIR」方式があります。透過型NDIRの詳細は下記記事をご参照ください。

2-2.光音響NDIRとは

 透過型NDIRは光検出器がガスサンプラーを透過した赤外線のエネルギー量を測定します。原理としてはランベルト・ベールの法則にしたがいます。

$$
\begin{aligned}
A & =log(\frac{1}{透過率})
\\&= log\frac{I_0}{I}
\\&=\epsilon\cdot c \cdot l
\\&= 分子吸光係数[M^{-1}cm^{-1}]\times 溶液濃度[M/L]\times 光路長[cm]
\end{aligned}
$$

https://sensirion.com/media/documents/A65391B9/65311890/CD_IN_SCDxx_Transmissive_and_photoacoustic_NDIR_sensing_D1_JP.pdf

 透過型NDIRセンサーとは対照的に、光音響NDIRセンサーはCO2分子が
吸収するエネルギー量を検出
します。IRエミッターをパルスするとCO2分子は周期的に赤外線を吸収し、これにより分子振動を発生させ分子の並進運動エネルギーは増加します。
 測定セルは密閉されているためセル内圧力が高まります。光源の調整によって測定セル内の周期的な圧力が変化するためガスチャンバー内のマイクロホンがこれを測定し、そこからCO2濃度を計算することができます。
 音波は全方向性のためエミッターとマイクロホンの相対的な位置関係に制約がなく、光音響NDIRセンサーは機械的および熱的により堅牢です。

https://sensirion.com/media/documents/A65391B9/65311890/CD_IN_SCDxx_Transmissive_and_photoacoustic_NDIR_sensing_D1_JP.pdf

3.部材購入

3-1.購入品

 部品は本体のみ購入しました。
 本モジュールは昇降圧スイッチング電圧レギュレータおよびロジックレベル変換回路内蔵により電圧3.3Vでも動作し、プルアップ抵抗器(10kΩ)が搭
載されており、外部でのプルアップが不要です。

3-2.準備必須品

 その他必需品は下記の通りです。

  • マイコン/シングルボード(Raspberry Pi/Pico)

  • ブレッドボード

  • ジャンピングワイヤー

4.環境構築

4-1.マイコン準備

 センサを制御するためのシングルボードやマイコンの準備を行います。
Raspberry PiやPicoの準備は下記記事参照のこと

 Raspberry PiにGPIOを制御するためのライブラリが無い場合は”RPi.GPIO”を事前にインストールしておきます。
 Picoの場合はMicropythonを使用できるようにしておきます。

[Terminal]
pip install rpi.gpio

4-2.ライブラリのインストール

 4-2-1.Case1:Pico

 Raspberry Pi Picoは組み込み関数と標準ライブラリで対応できるため、追加の環境構築は不要です。

 4-2-2.Case2:Raspberry Pi

 Raspberry Piの場合はRPi.GPIOだとシリアル通信(I2C)が実行できません。より簡単に実行するためAdafruitが公開している”adafruit-circuitpython-scd4x”を利用しました。

[Terminal]
pip3 install adafruit-circuitpython-scd4x

【スクラッチで作成する場合】
 Raspberry Piの場合、I2Cを使用してスクラッチで作成する場合はsmbus2でいけると思います。本記事では上記ライブラリを使用しました。

[Terminal]
pip install smbus2

5.使用前の準備

5-1.はんだ付け

 本製品の既にピンがつけられているため、はんだ付けは不要です。
 また、コネクタ(SM04B-SRSS-TB)付き信号線が同封されているため、配線も差し込むだけで問題ありません。

5-2.部品の組付け

 部品の組付けはジャンパー線を使用して下記の通り繋ぎました。

【Raspberry Pi】

$$
\begin{array}{|c|c|c|} \hline \textbf{No.} &\textbf{センサー} & \textbf{Raspberry Pi} \\
\hline \text{1} & \text{SCL} & \text{GPIO2(SCL1 I2C)}\\
\hline \text{2} & \text{SDA} & \text{GPIO3(SDA1 I2C)}\\
\hline \text{3} & \text{VIN(+)} & \text{PIN4(5V)}\\
\hline \text{4} & \text{GND} & \text{PIN6(GND)}\\
\hline \end{array}
$$

【Raspberry Pi Pico】

$$
\begin{array}{|c|c|c|} \hline \textbf{No.} &\textbf{センサー} & \textbf{Raspberry Pi Pico} \\
\hline \text{1} & \text{SCL} & \text{GPIO1(I2C0 SCL)}\\
\hline \text{2} & \text{SDA} & \text{GPIO0(I2C0 SDA)}\\
\hline \text{3} & \text{VIN(+)} & \text{PIN40(5V)}\\
\hline \text{4} & \text{GND} & \text{PIN38(GND)}\\
\hline \end{array}
$$

6.MicroPythonスクリプト(Pico)

 Micropythonのコードは下記記事を参考にさせていただきました。

6-1.任意:デバイス接続の確認

 MicroPythonの場合、i2cオブジェクトのscan()で接続確認できます。

[IN]
from machine import Pin, I2C

i2c = I2C(0, sda=Pin(16), scl=Pin(17), freq=400000)
print(i2c.scan(), hex(i2c.scan()[0]))  # I2Cデバイスのスキャン
[OUT]
[98] 0x62

6-2.コードの設計思想

 設計思想は下記の通りです。

  1. 前提として理解が難しいので、元コードを参考にしつつ、不要な場所を削りながら自分用にアレンジ

  2. 動きが分からないところは可視化用の出力を追加

 6-2-1.備忘録1:クラスI2Cクラスのメソッド

 I2Cクラスで使用しているメソッドは下記の通り。

  • 標準バスオペレーション:I2C.writeto(addr, buf, stop=True, /)

  • メモリ操作:I2C.readfrom_mem_into(addr, memaddr, buf, *, addrsize=8)

 6-2-2.備忘録2:I2Cコマンド

 I2C通信で指定のアドレスにコマンド送付/読取りをしたりします。

 コマンド一覧は下記の通りです。

 6-2-3.備忘録3:基本コマンド

 通常の連続計測においてI2CとSCD4xセンサは下記やり取りを行います。

  1. センサーのPowerをON

  2. I2Cからstart_periodic_measurement commandを送る。

    • 1回のアップデート時間は5sec

  3. I2Cから定期的に”read_measurement command”を用いてデータを読み込む

  4. I2Cから”stop_periodic_measurement command”を送り、センサーをアイドルモードに戻す。

【スタート】
 コマンドは"0x21b1"

【データ読み込み】
 コマンドは"0xec05"。バッファー内にデータがない場合センサーはNACKを返すため、”get_data_ready_status”でチェック可能。
 値は下記計算式から取得可能

【ストップ】
 コマンドは"0x3f86"

【状態確認】
 コマンドは"0xe4b8"

 6-2-4.備忘録4:Checksumの計算

 下記の通り

6-3.スクリプト実行

 スクリプトを作成し、実行しました。とりあえず数値が出ることは確認しました。

[IN]
from machine import Pin, I2C
import time
from micropython import const

class SCD4X:
    #I2Cアドレス
    DEFAULT_ADDRESS = 0x62 #SCD4xのI2Cアドレス
    #SCD4xコマンド
    ##低電力連続計測モード(Section3.8)
    DATA_READY = const(0xE4B8) #データ準備ステータス(Type:Read)/get_data_ready_status
    ##基本コマンド(Section3.5)
    STOP_PERIODIC_MEASUREMENT = const(0x3F86) #連続計測停止(Type:Send command)/stop_periodic_measurement
    START_PERIODIC_MEASUREMENT = const(0x21B1) #連続計測開始(Type:Send command)/start_periodic_measurement
    READ_MEASUREMENT = const(0xEC05) #計測値の読み出し(Type:Read)/read_measurement

    def __init__(self, i2c:I2C, address:int = DEFAULT_ADDRESS):
        self.i2c = i2c #I2Cオブジェクト
        self.address = address #I2Cアドレスを設定
        self._buffer = bytearray(18) #空Byte(18byte):バッファー(データ読み込み用)
        self._cmd = bytearray(2) #空Byte(2byte):コマンド送信用
        self._crc_buffer = bytearray(2) #空Byte(2byte):CRCチェック用

        # cached readings
        self._temperature = None
        self._relative_humidity = None
        self._co2 = None

        self.stop_periodic_measurement() #オブジェクト化時(初期)に連続計測を停止

    @property
    def co2(self):
        if self.data_ready:
            self._read_data()
        return self._co2

    @property
    def temperature(self):
        if self.data_ready:
            self._read_data()
        return self._temperature

    @property
    def relative_humidity(self):
        if self.data_ready:
            self._read_data()
        return self._relative_humidity

    def _read_data(self):
        self._send_command(self.READ_MEASUREMENT, cmd_delay=0.001)
        self._read_reply(self._buffer, 9)
        #CO2濃度データの取得
        self._co2 = (self._buffer[0] << 8) | self._buffer[1]
        #温度データの取得※要計算
        temp = (self._buffer[3] << 8) | self._buffer[4]
        self._temperature = -45 + 175 * (temp / 2 ** 16)
        #湿度データの取得※要計算
        humi = (self._buffer[6] << 8) | self._buffer[7]
        self._relative_humidity = 100 * (humi / 2 ** 16)

    @property
    def data_ready(self):
        self._send_command(self.DATA_READY, cmd_delay=0.001)
        self._read_reply(self._buffer, 3)
        return not ((self._buffer[0] & 0x03 == 0) and (self._buffer[1] == 0))

    def stop_periodic_measurement(self):
        self._send_command(self.STOP_PERIODIC_MEASUREMENT, cmd_delay=0.5)

    def start_periodic_measurement(self):
        self._send_command(self.START_PERIODIC_MEASUREMENT, cmd_delay=0.01)

    def _send_command(self, cmd, cmd_delay=0.00):
        self._cmd[0] = (cmd >> 8) & 0xFF
        self._cmd[1] = cmd & 0xFF  
        
        try:
            self.i2c.writeto(self.address, self._cmd)
        except OSError as e:
            raise RuntimeError("_send_commandメソッドでエラー:I2Cとの接続不良") from e
        time.sleep(cmd_delay)

    def _read_reply(self, buff, num):
        self.i2c.readfrom_into(self.address, buff, num)
        self._check_buffer_crc(self._buffer[0:num])

    def _check_buffer_crc(self, buf):
        for i in range(0, len(buf), 3):
            self._crc_buffer[0] = buf[i]
            self._crc_buffer[1] = buf[i + 1]
            if self._crc8(self._crc_buffer) != buf[i + 2]:
                raise RuntimeError("CRC check failed while reading data")

    #Cyclic Redundancy Check(巡回冗長検査)
    @staticmethod
    def _crc8(buffer):
        crc = 0xFF #CECレジスタの初期化
        for byte in buffer:
            crc ^= byte
            for _ in range(8):
                if crc & 0x80:
                    crc = (crc << 1) ^ 0x31
                else:
                    crc <<= 1
        return crc & 0xFF


if __name__ == "__main__":
    led = Pin("LED", Pin.OUT) #Pico:25, Pico W="LED"
    i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=100000)
    print(f'I2C接続確認:{[hex(i) for i in i2c.scan()]}')
    scd4x = SCD4X(i2c)
    scd4x.start_periodic_measurement() 
    
    while True:
        time.sleep(5)  # Delay for measurement interval
        if scd4x.co2 is not None:
            print(f'CO2濃度:{scd4x.co2:.2f}ppm')
            print(f'温度:{scd4x.temperature:.2f}℃')
            print(f'湿度:{scd4x.relative_humidity:.2f}% RH', end='\n\n')
        #CO2濃度が2000ppm以上の場合、LEDを点灯
        if scd4x.co2 and scd4x.co2 >= 2000:
            led.on()
        else:
            led.off()
[OUT]
I2C接続確認:['0x62']
CO2濃度:565.00ppm
温度:30.88℃
湿度:46.27% RH

CO2濃度:570.00ppm
温度:30.45℃
湿度:46.97% RH

CO2濃度:569.00ppm
温度:30.21℃
湿度:47.63% RH

6-4.参考:原因不明のエラー

 実行をするとクラスSCD4Xの__init__内にあるself.stop_periodic_measurement()実行時に_send_commandメソッドのself.i2c.writeto(self.address, self._cmd)でEIOエラーが発生しました。原因として下記が考えられますが、すべて違いました。

  • 配線ミス:目視確認

  • I2C通信の不良:i2c.scan()で確認済み

  • I2Cアドレスの間違い:目視確認

  • I2Cコマンドの間違い:目視確認

  • コマンドの型違い:Byte型であることを目視確認

  • センサ不良:Raspberry Pi(別装置)で動作確認

  • プルアップ抵抗:機器側に入っている(Raspberry Piでも確認)

  • 待機時間:長めにとってみたけどエラー未解消

 どうしようもない状態で実行してみたらなぜかうまくいきました。電源に差してから1時間以上経過しているため原因は不明ですが、同じエラーが出るかもしれませんのでご参考までに。

7.Pythonスクリプト(Raspberry Pi)

 Raspberry Piでも実行しました。「Adafruit_CircuitPython_SCD4X」ライブラリは公式Docs参照のこと。

7-1.任意:デバイス接続の確認

 I2C通信の場合は下記コマンドで接続時にアドレス確認ができます。
 デフォルトI2C アドレス:0x62(7 ビット)が確認できました。

[Terminal]
sudo i2cdetect -y 1

7-2.スクリプト実行

 7-2-1.シンプル版

  公式Docsより、数行で実行できました。

[IN]
import time
import board
import adafruit_scd4x

i2c = board.I2C()
scd4x = adafruit_scd4x.SCD4X(i2c)
print("Serial number:", [hex(i) for i in scd4x.serial_number])

scd4x.start_periodic_measurement()
print("Waiting for first measurement....")

while True:
    if scd4x.data_ready:
        print("CO2: %d ppm" % scd4x.CO2)
        print("Temperature: %0.1f *C" % scd4x.temperature)
        print("Humidity: %0.1f %%" % scd4x.relative_humidity, end="\n\n")
    time.sleep(1)
[OUT]
Serial number: ['0x63', '0x2', '0x8f', '0x7', '0x3b', '0x4a']
Waiting for first measurement....
CO2: 556 ppm
Temperature: 28.1 *C
Humidity: 49.7 %

CO2: 552 ppm
Temperature: 28.2 *C
Humidity: 50.2 %

CO2: 556 ppm
Temperature: 28.1 *C
Humidity: 50.6 %

 7-2-2.可視化グラフ追加

 下記設計思想でデータを可視化しました。実行とともににセンサに息を吹きかけて、CO2+その他の値変化を確認しました。

  • センサの応答速度が遅いため、各処理(timestampとデータの読み取り)のズレは無視できるものとする。

  • データを格納するためのクラス:Datalistを作成

    • データ追加も本クラスで実施

    • データの可視化はMatplotlibで行うため、やりやすいように最初からDataFrameにしておく

  • データの可視化はMatplotlibで行う

    • リアルタイムで見れるようにplt.gcf()やclear_outputを利用

    • 可視化時のy軸の上限・下限は仕様書の値を使用(値は10分割表示)

  • PoCのためロギング機能は追加していない

    • ロギング機能を付けるなら、停止前にPandasからCSVファイル出力+画像の保存をつけたい

[IN]
import time
import board
import adafruit_scd4x
import datetime
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from IPython.display import display, clear_output

#I2C測定準備
i2c = board.I2C()
scd4x = adafruit_scd4x.SCD4X(i2c)
print("Serial number:", [hex(i) for i in scd4x.serial_number])
scd4x.start_periodic_measurement()
print("Waiting for first measurement....")

#データ取得・可視化
class Datalist:
    def __init__(self):
        self.data = pd.DataFrame(columns=['Timestamp', 'CO2', 'Temperature', 'Humidity'])
        self.range_MINMAX = {'CO2': [400, 2000], 
                        'Temperature': [-10, 60], 
                        'Humidity': [0, 100]}
        self.color_graph = {'CO2': 'blue', 'Temperature': 'red', 'Humidity': 'green'}
        
    def append(self, timestamp, CO2, temperature, humidity):
        new_data = {'Timestamp': timestamp, 'CO2': CO2, 'Temperature': temperature, 'Humidity': humidity}
        df_new = pd.DataFrame(new_data, index=[0])
        self.data = pd.concat([self.data, df_new], ignore_index=True)
        
#データベース(データ格納用)の作成
datalist = Datalist()

#可視化用グラフ
fig, axs = plt.subplots(3, 1, figsize=(10, 10), sharex=True) #Sharex=Trueでx軸を共有

#測定開始
while True:
    if scd4x.data_ready:
        #データ取得
        timestamp = datetime.datetime.now()
        CO2 = scd4x.CO2
        temperature = scd4x.temperature
        humidity = scd4x.relative_humidity
        
        #データ格納
        datalist.append(timestamp, CO2, temperature, humidity)

        #データ可視化
        if not datalist.data.empty:
            #ax毎に描画
            for i, (ax, column) in enumerate(zip(axs, ['CO2', 'Temperature', 'Humidity'])):
                ax.clear() #グラフ初期化
                y_min, y_max = datalist.range_MINMAX[column] #y軸の範囲指定
                ax.plot(datalist.data['Timestamp'], datalist.data[column], label=f'{column}', color=datalist.color_graph[column])
                ax.set_xlabel("Time")
                ax.set_ylabel(f"{column}")
                ax.set_yticks(np.linspace(y_min, y_max, 11))
                ax.legend()
                ax.grid(True)
                ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S"))
                
            # Jupyter Notebookでのリアルタイム表示のためのコード
            clear_output(wait=True)
            display(plt.gcf())
            
    time.sleep(1)
[OUT]
Serial number: ['0x63', '0x2', '0x8f', '0x7', '0x3b', '0x4a']
Waiting for first measurement....

8.所感

 簡単な所感は下記の通り

  • SENSIRION社は今の仕事でもセンサー関係で話は聞くけど、こんなところでお目にかかるとは思ってなかった

  • 検出原理が初めて聞くものだったので、結構面白い


参考資料

別添1 Python関係

別添2 技術関係

あとがき

 エラーでめちゃくちゃ時間取られたので、少し理解が浅い。
 思い出した時に再度振り返りたい。


この記事が気に入ったらサポートをしてみませんか?