製品レビュー|電子機器8:光音響NDIR方式CO2センサ(SCD40)
1.概要
購入した製品の使い方および感想用記事です。
今回は「SCD40使用-光音響NDIR方式 CO2センサモジュール(5,980円/個(税込))」をレビューしました。
【参考:他の型式(SCD4x)】
より高精度/広範囲なSCD41もありますが、センサだけで5,000円を超え、モジュールだと9,300円前後します。
また性能の違いが分かっていませんがSensirion SCD30の方も同等くらいの値段はします(秋月電子で9,200(円/個(税込))。
1-1.基本仕様
SENSIRION社の光音響NDIR方式(PASens®)センサSCD40を使用したCO2(二酸化炭素)センサモジュールであり、温度・湿度も同時計測できます。
1-1-1.仕様の概要
基本の概要は下記参照。値は公式の「技術ダウンロード」から参照
電源電圧:1.8~5V
昇降圧スイッチング電圧レギュレータおよびロジックレベル変換回路内蔵により、低い電圧でも使用可能
CO2濃度:
測定レンジ:400~2000ppm
精度:±50ppm+読み取り値の5%
応答速度:$${T_{63}}$$までに90sec
周囲温度:
測定レンジ:-10~60℃
精度:±0.8~1.5℃
応答速度:$${T_{63}}$$までに120sec
湿度:
測定レンジ:0~100%RH
精度:±6~9%RH
応答速度:$${T_{63}}$$までに60sec
インターフェイス:I2C(Qwiic/STEMMA QT ピン互換)
周辺環境:
温度:-10~60℃
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%
【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}
$$
透過型NDIRセンサーとは対照的に、光音響NDIRセンサーはCO2分子が
吸収するエネルギー量を検出します。IRエミッターをパルスするとCO2分子は周期的に赤外線を吸収し、これにより分子振動を発生させ分子の並進運動エネルギーは増加します。
測定セルは密閉されているためセル内圧力が高まります。光源の調整によって測定セル内の周期的な圧力が変化するためガスチャンバー内のマイクロホンがこれを測定し、そこからCO2濃度を計算することができます。
音波は全方向性のためエミッターとマイクロホンの相対的な位置関係に制約がなく、光音響NDIRセンサーは機械的および熱的により堅牢です。
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.コードの設計思想
設計思想は下記の通りです。
前提として理解が難しいので、元コードを参考にしつつ、不要な場所を削りながら自分用にアレンジ
動きが分からないところは可視化用の出力を追加
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センサは下記やり取りを行います。
センサーのPowerをON
I2Cからstart_periodic_measurement commandを送る。
1回のアップデート時間は5sec
I2Cから定期的に”read_measurement command”を用いてデータを読み込む
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 技術関係
あとがき
エラーでめちゃくちゃ時間取られたので、少し理解が浅い。
思い出した時に再度振り返りたい。
この記事が気に入ったらサポートをしてみませんか?