ESP32とCO2センサー(MH-Z19C)で二酸化炭素の濃度を測定する。
Contents
CO2センサー
ESP32-WROOM-32 にて CO2センサー(MH-Z19C)を使って空気中の二酸化炭素の濃度を測定して、その結果を I2C 接続した LCD(液晶ディスプレイ)に表示した時の備忘録。
室内で換気をしないと、どの様に二酸化炭素濃度が上昇するのかを測定したくて試してみた。
MH-Z19C の主な特徴は以下の通り。
- NDIR(非分散型赤外)方式で空気中のCO2濃度を測定する
- PWN 出力と UART(Universal Asynchronous Receiver/Transmitter) 接続の2パターンの出力がある
- 温度も同時に検出可能(UART接続の時のみ)
尚、使用時の注意点として本体に貼ってある白いシート状の物は剥がさない事。
つい剥がしてしまいたくなるが、これは剥がさずに使うものらしい。
NDIRとは
NDIR(Non-Dispersive InfraRed)とは特定の波長の赤外線(3~5μm辺り)を使って CO2 濃度を測定する方法。
CO2 分子は 4.26μm の波長の赤外線を吸収する性質を持っているので、濃度が増えればより強く吸収する。
その性質を利用して光源(LED や白熱電球、MEMS ヒーターなど)からの赤外線の吸収率から CO2 濃度を算出する方式。
スペック
細かいスペックは以下の通り。
検出可能なガス | CO2(二酸化炭素) |
動作電圧 | 5.0 ± 0.1V DC |
消費電力 | 平均40mA 以下、最大125 mA(@5V 電源供給時) |
インターフェイス電圧 | 3.3 V (5V 互換) |
検出範囲 | PMW:400 ~ 2,000 ppm UART:400 ~ 10,000 ppm |
精度 | ±(50 ppm + 5% 読取値) |
準備時間 | 1分程度のプリヒート時間が必要 |
使用湿度・温度 | 湿度:0 ~ 95%(結露しない事) 温度:-20 ~ 60 ℃ |
コネクタ
コネクタは裏からみると左側に5ピン、右側に4ピンある。
HD | ゼロポイント(400 ppm)キャリブレーションする時に 7 秒間以上 GND に接続する |
SR | 未使用(予約) |
Tx | UART(TXD) データ出力 |
Rx | UART(RXD) データ入力 |
Vo | 未使用 |
PWN | PWN 出力 |
AOT | 未使用 |
GND | GND |
Vin | Vin(5Vを接続する) |
ライブラリー
事前に Arduino IDE に必要なライブラリーをインストールする。
MH-Z19用ライブラリのインストール
尚、PWN 接続であればロジックも簡単なのでライブラリーを使わなくとも簡単なコーディングで CO2 濃度は取得できる。
ただ UART(シリアル接続)の場合はキャリブレーションのオン/オフのコマンドや読み取った値のチェックサムの計算や数値化など、面倒な処理もあるので先人が用意してくれたライブラリーを使った方が簡単だ。
Arduino IDE からツールー>ライブラリーを管理、から “MH-Z19″ で検索して、”MH-Z CO2 Sensors” ライブラリーをインストールした。
Github はこちら。
I2C LCDライブラリのインストール
Arduino IDE に I2C 接続の LCD(液晶ディスプレイ)用のライブラリーをインストールする。
インストール方法については以前の記事を参照して欲しい。
PWN接続
まずは簡単な PWN 接続から試す。
配線図
配線図は以下の通り。
尚、今回使用した ESP32-WROOM-32 モジュールのピン配置は ESP32 DEVKIT V1(30ピン)の配置になっている。
基板上では左下のピンは VIN となっているが実際には 5V の端子になっている。
ESP32 DEVKIT V1 のピン配置についてはこちらの記事を参照して欲しい。
また I2C 接続した LCD(液晶ディスプレイ)については以前の記事を参照して欲しい。
スケッチ(PWM)
センサーと ESP32-WROOM-32 とを PWM 接続した時のサンプルスケッチは以下の通り。
/*
* Co2 Sensor(MH-Z19C)の測定値をI2C LCDに表示する
* Co2 Sensor:GPIO13
* LCD:I2C
*/
#include <MHZ.h> // Co2 Sensor
#include <LiquidCrystal_I2C.h> // I2C LCD
#define co2pwmPin 13 // GPIO13
LiquidCrystal_I2C lcd(0x27,16,2); // I2C addr、16x2
MHZ co2(co2pwmPin, MHZ19C);
void setup() {
Serial.begin(115200);
lcd.init(); // LCD初期化
lcd.backlight(); // バックライト点灯
lcd.clear(); // クリア
lcd.setCursor(0, 0); // カーソル位置
lcd.print("Program Start"); // 文字表示
// Serial.println("Program Start");
pinMode(co2pwmPin, INPUT);
delay(100);
if (co2.isPreHeating()) { // 起動してから一定時間待つ
// Serial.println("Preheating");
lcd.clear(); // クリア
lcd.setCursor(0, 0); // 1行目
lcd.print("Preheating");
while (co2.isPreHeating()) {
// Serial.print(".");
delay(5000);
}
// Serial.println();
}
}
void loop() {
int ppm_pwm = co2.readCO2PWM();
// Serial.println("PPM = " + String(ppm_pwm));
lcd.clear(); // クリア
lcd.setCursor(0, 0); // 1行目
lcd.print("Co2 conc");
lcd.setCursor(0, 1); // 2行目
lcd.print("PPM = " + String(ppm_pwm));
}
補足説明
ppm の算出方法
PWN の場合は約 1,004 ミリ秒中の High の状態の割合で ppm を算出する。
下記データシートより引用。
400 ppm の場合の計算例は以下の通り。
- TH:High の状態の秒数(ミリ秒)
- T:出力時間(1,004ミリ秒 ±5%)
計算式は、2,000 × (TH-2ms)/(T-4ms) なので、
1004 ミリ秒中、202 ミリ秒が Highの場合は、
2,000 ✕ (202 - 2)÷(1,004 - 4)= 400 ppm
となる。
readCO2PWM() 関数
ループの最初(39行目)で先程、インストールしたMH-Z19用ライブラリ(デフォルトだと C:¥Users¥ユーザ名¥Documents¥Arduino¥libraries¥MH-Z_CO2_Sensors にインストールされる)の readCO2PWM() を呼び出している。
MHZ.cpp の関数の中身は以下の通り(※GitHubより)
int MHZ::readCO2PWM() {
if (!PwmConfigured) {
if (debug) Serial.println(F("-- pwm is not configured "));
return STATUS_PWM_NOT_CONFIGURED;
}
//if (!isReady()) return STATUS_NOT_READY; not needed?
if (debug) Serial.print(F("-- reading CO2 from pwm "));
unsigned long th, tl, ppm_pwm = 0;
do {
if (debug) Serial.print(".");
th = pulseIn(_pwmpin, HIGH, 1004000) / 1000;
tl = 1004 - th;
ppm_pwm = 2000 * (th - 2) / (th + tl - 4);
} while (th == 0);
if (debug) {
Serial.print(F("\n # PPM PWM: "));
Serial.println(ppm_pwm);
}
return ppm_pwm;
}
9 ~ 14行目で pulseIn 関数で High の時間(マイクロ秒)を計測して 1,004 ミリ秒中の割合からCO2濃度を計算している。
th=0 つまり Hignが0(=Lowになるまで)while でループしているのだが、ちょっと気になるのが1,004 ミリ秒固定にしている事。
データシートによると1,004±5% とあるので厳密に誤差を考慮するのであれば、Hign → Low → 再びHign になるまでの時間を計測して、その値を分母にした方が正確な値が検出できる様に思う。
実行結果
実行結果は以下の通り。
240 ppm と表示されているが大気中の二酸化炭素濃度が約400 ppm なので、この値は低すぎる。
最初は電源投入直後でゼロキャリブレーションがされていない状態だと思ったのだが、どうも様子がおかしい。
色々と調べてみると、前述の計算式で 2,000 を乗算している部分で 5,000 とする必要があるとの記述を見つけた。
データシートが間違っているのかは不明だが MHZ.cpp の readCO2PWM() 関数を下記の様に修正した所、UART 接続時と同等の値が得られた。
// ppm_pwm = 2000 * (th - 2) / (th + tl - 4);
ppm_pwm = 5000 * (th - 2) / (th + tl - 4);
ゼロキャリブレーション
デフォルト設定では自動キャリブレーションが24時間毎に発生して 400 ppm をベースにゼロキャリブレーションを行うとデータシートには載っていた。
詳細なアルゴリズムは不明だが24時間中に計測された最低濃度を 400 ppm としてゼロキャリブレーションする様だ。
自動キャリブレーションは「オフィスや家庭環境に適していますが、農業の温室、農場、冷蔵庫などには適していません。」との記述があった。
一日中、二酸化炭素濃度が 400 ppm 付近にならない環境では不向きなのだと思う。
また、後述するが自動キャリブレーションのオン/オフは UART(シリアル接続)で 0x79 コマンドを送信する事で変更が可能だ(PWN接続の時は出来ない)。
手動でゼロキャリブレーションを行うには大気中(二酸化炭素濃度が400 ppmに近い環境)で前述の HD ピンを 7秒間以上 GND に接続する。
UART接続
続いて UART(シリアル接続)を試してみる。
こちらの方が自動キャリブレーションの設定を変更できたり、二酸化炭素濃度と同時に温度も取得できたりと、多機能だ。
ピンさえ空いていれば UART 接続の方が良い様に思う。
配線図
配線図は以下の通り。
- I2C 接続の LCD は PWM接続の時と同様
- ESP32-WROOM-32 の TX2 を MH-Z19C の Rx に、RX2 を Tx に接続する※
※前述の MH-Z19C 用のライブラリの中で SoftwareSerial を使ってピンを指定しているのでハードウェアシリアルのピン(UART0、1、2)に拘る必要は無い。
GPIO6 ~ GPIO11、GPIO34、GPIO35 以外のピンであれば、 SoftwareSerial で通信が出来る。
実際、GPIO32、33に接続して測定が出来た。
今回は空いていたので RX2(GPIO16)、TX2(GPIO17)のピンを使用した。
スケッチ(UART)
UART(シリアル)接続の時のスケッチは以下の通り。
/*
* Co2 Sensor(MH-Z19C)の測定値をI2C LCDに表示する
* Co2 Sensor:UART接続
* LCD:I2C
*/
#include <MHZ.h> // Co2 Sensor
#include <LiquidCrystal_I2C.h> // I2C LCD
#define RX_PIN 16 // GPIO16
#define TX_PIN 17 // GPIO17
LiquidCrystal_I2C lcd(0x27,16,2); // I2C addr、16x2
MHZ co2(RX_PIN, TX_PIN, MHZ19C);
void setup() {
Serial.begin(115200);
lcd.init(); // LCD初期化
lcd.backlight(); // バックライト点灯
lcd.clear(); // クリア
lcd.setCursor(0, 0); // カーソル位置
lcd.print("Program Start"); // 文字表示
delay(100);
if (co2.isPreHeating()) { // 起動してから一定時間待つ
lcd.clear(); // クリア
lcd.setCursor(0, 0); // 1行目
lcd.print("Preheating");
while (co2.isPreHeating()) {
delay(5000);
}
}
co2.setAutoCalibrate(true); // 自動キャリブレーション
}
void loop() {
int ppm_uart = co2.readCO2UART();
lcd.clear(); // クリア
lcd.setCursor(0, 0); // 1行目
lcd.print("Co2:" + String(ppm_uart) + " ppm");
int temperature = co2.getLastTemperature();
lcd.setCursor(0, 1); // 2行目
lcd.print("temp:" + String(temperature) + " c");
delay(120000); // 2分程度間隔をあける
}
補足説明
以下、コードの補足説明。
二酸化炭素濃度取得
スケッチ中からは下記のコードでCO2濃度を取得している。
int ppm_uart = co2.readCO2UART();
UART 接続時のCO2濃度取得のデータシートは以下の様になっている。
Byte2 に 0x86 をセットして Sending command を送ると Byte2 と Byte3 に値(HIGHとLOW)がセットされた Return Value が返される仕様になっている。
(HIGH × 256)+ LOW を計算すれば CO2濃度が取得される。
尚、Byte8 は チェックサムになっており、他の Byte が何らかの理由でも正しく読み取れなかった際の整合性チェックに使用される。
MHZ.cpp の readCO2UART() 関数の中で上記処理を行ってくれるのでこのライブラリーを使用するのであれば関数を呼ぶだけで良い。
チェックサム
チェックサムの計算方法は以下の通り。
Byte0 -(Byte1+ Byte2+ Byte3+ Byte4+ Byte5+ Byte6+ Byte7)+ 0x01= Byte8
送信時は上記の式を満たすように Byte8 を組み立ててセットする。
受信時は Byte0 ~ Byte7 から Byte8 を計算して実際の値と比較して、途中でデータロスや文字化けなどの不整合が起きていないかをチェックする。
温度取得
データシートには載っていないが受信データの Byte4 に温度がセットされている模様。
Byte4 の値を調整(ライブラリー中では 44 を減算していた)して温度を取得しているが、データシートに載っていない機能なので目安程度に思っていた方が良いと思う。
自動キャリブレーション
デフォルトでは工場出荷時に自動キャリブレーションがオンになっている。
下記のコードで自動キャリブレーションをオン(true)/オフ(false)できる。
co2.setAutoCalibrate(true); // 自動キャリブレーション
データシートでは 0x79 のコードを送信することでオン/オフを切り替えるとある。
Byte3 が 0xA0 の時、オートキャリブレーション機能がオンとなり、Byte3 が 0x00 のとき、オートキャリブレーション機能が OFF となる。
間隔
測定間隔を 2分にしているのは短い間隔だと測定エラーが出てしまった為。
データシート上では測定間隔の記述は見つけられなかったが、あまり頻繁に測定を繰り返す用途には向かないのかも知れない。
尚、PWM 接続の際は 5秒間隔でCO2濃度を取得しても問題なく取得できた。
delay(120000); // 2分程度間隔をあける
以上で今回の記事は終了とする。
この記事が何処かで誰かの役に立つことを願っている。
尚、当記事中の商品へのリンクはAmazonアソシエイトへのリンクが含まれています。Amazonのアソシエイトとして、当メディアは適格販売により収入を得ていますのでご了承ください。
最近のコメント