ESP32とMH-Z19CセンサでCO2濃度の測定

こんなこと、やります。

  • CO2センサ(MH-Z19C)とESP32でCO2濃度の測定
  • CO2センサの校正
  • 気象庁のCO2濃度データをグラフ化
  • MH-Z19Cで使われているUART(シリアル通信)の解説

ESP32とCO2センサ(MH-Z19C)で、CO2濃度の測定
ESP32とCO2センサ(MH-Z19C)で、CO2濃度の測定

つかうもの

はじめに、この記事で使うものをご紹介します。

MH-Z19C

秋月電子通商で販売している MH-Z19C を使いました。

CO2センサ(MH-Z19C)
CO2センサ(MH-Z19C)
MH-Z19Cの背面
MH-Z19Cの背面

▼ Amazonでもたくさん販売されてます。

MH-Z19Cセンサは、赤外線を使って空気中のCO2濃度を測定します。MH-Z19Cセンサからデータを読み取るには、UART通信または、PWMで値を読み取る方法があります。本記事ではもっとも簡単な方法として、既存のライブラリを使ってUART方式でMH-Z19からCO2濃度を読み取る方法をご紹介いたします。

ESP32(Arduino)

本記事ではWiFi通信のできるESP32を使用します。

ESP32だけでなく、ふつうのArduinoでもMH-Z19Cを扱えますので、お好きなボードをお使いください。

その他

ESP32・Arduino開発では、ブレッドボードやジャンプワイヤもあると便利です。

MH-Z19Cの概要

MH-Z19Cでは、UARTまたはPWM信号を読み取ってCO2濃度のデータを読み取ります。 今回はライブラリを使用しますが、ライブラリの仕組みとしてはUART通信でデータを読み取ってます。UARTについて記事の最後でくわしく解説してます。

MH-Z19Cの仕様

項目
電源電圧5V
測定レンジ400~5000ppm
消費電流40mA以下、最大125mA
インターフェース電圧3.3V
出力シリアルポート(UART、TTLレベル3.3V)、PWM

MH-Z19C データシート

MH-Z19Cライブラリのインストール

MH-Z19CとUARTで簡単にやり取りできるライブラリを使用します。こちらの「nara256/mhz19_uart」を使わせてもらいました。 GitHub - nara256/mhz19_uart 上記のページからzipでダウンロードして解凍してください。それをArduinoのライブラリディレクトリへ移動すればOKです。

私はPlatform IOで開発してますので、ディレクトリ構造は次のように配置しました。

.
├── include
│   └── README
├── lib
│   ├── README
│   └── mhz19_uart
│       ├── MHZ19_uart.cpp
│       ├── MHZ19_uart.h
│       └── examples
│           └── demo
│               └── demo.ino
├── platformio.ini
├── src
│   └── main.cpp
└── test
    └── README

MH-Z19CだけでなくMH-Z19Bでも使えます。また、このライブラリはArduinoとESP32のどちらでも使うことができます。UART通信ですので、Arduinoの場合はソフトウェアシリアルを、ESP32の場合はハードウェアシリアルを使用します。配線だけ間違えないように注意してください。

ESP32(Arduino)とMH-Z19Cの配線

センサのTXはデータ送信なので、ESP32ではデータ受信するRXにつなぐことを間違えないようにしてください。またESP32の場合は、安定性の問題からハードウェアシリアル2へつなぎます。Arduinoの場合は、ソフトウェアシリアルを使用します。

ESP32

MH-Z19CESP32
Vin5V
GNDGND
RXGPIO17 (TX)
TXGPIO16 (RX)

Arduino UNO

MH-Z19CArduino UNO
Vin5V
GNDGND
RXGPIO2 (TX)
TXGPIO1 (RX)

下記の写真は企業様からご依頼をいただき、CO2センサをはじめ、温度湿度センサ、照度センサを追加して環境モニターを作っている様子です。LANケーブル使って配線しました。

ESP32とMH-Z19C、DHT22を配線
ESP32とMH-Z19C、DHT22を配線
ESP32とMH-Z19C、DHT22、TSL25721を配線
ESP32とMH-Z19C、DHT22、TSL25721を配線

さらにST7735のTFT LCDへセンサのデータ表示や、AWSのIoT coreを使ってWiFi経由でMQTTでデータ送信したりできます。

▼ こちらは、企業様案件です。気温・湿度・体感温度・CO2濃度の表示&データ送信をESP32で製作しました。

温度・湿度・体感温度・CO2濃度ロガー
温度・湿度・体感温度・CO2濃度ロガー
温度・湿度・体感温度・CO2濃度ロガー
温度・湿度・体感温度・CO2濃度ロガー

二酸化炭素濃度が1000ppmを超えると集中力が低下していくんだとか。部屋に置くと換気の目安になりそうなので、自分用にもう一台作ろうかな?

CO2濃度を計測するサンプルプログラム(ソースコード)

さて、次はCO2濃度を計測するサンプルプログラムです。ライブラリのおかげで、とても簡単にCO2濃度のデータを読み取ることができます。

demo.ino
/**
 * @date 2022-11-23
 * @author Toshihiko Arai
 * @copyright https://101010.fun
*/
#include <Arduino.h>
#include <MHZ19_uart.h>

/**
 * @brief MHZ19C <--> ESP32 の配線
 *        VIN    <--> 5V
 *        GND    <--> GND
 *        Tx     <--> GPIO16 (RX2)
 *        Rx     <--> GPIO17 (TX2)
*/
const int rx_pin = 16;
const int tx_pin = 17;

MHZ19_uart mhz19;

void setup()
{
  Serial.begin(115200);
  mhz19.begin(rx_pin, tx_pin);
//   mhz19.setAutoCalibration(true);

  Serial.println("MH-Z19 is warming up now.");
  delay(10 * 1000); // 安定するまで10秒ほど待つ
}

void loop()
{
  int co2ppm = mhz19.getCO2PPM();
  int temp = mhz19.getTemperature();

  Serial.print("co2: ");
  Serial.println(co2ppm);
  Serial.print("temp: ");
  Serial.println(temp);

  delay(3000);
}
setAutoCalibration を使うと自動で校正しくれるそうですが、工場出荷時に校正されているハズなのでコメントアウトしてます。また、MH-Z19Cには温度センサもついます。気温を読み取ることも可能でしたが、あまり精度は期待できません。気温を測定したい場合は、DHT11やDHT22がおすすめです。

MH-Z19Cセンサのゼロ点校正

念のため、MH-Z19Cセンサのゼロ点校正する方法をご紹介します。MH-Z19Cセンサは400ppmをゼロ点として校正されます。現在、大気中のCO2濃度は410〜420ppmと少し高いのですが、ここでは大気中のCO2濃度を400ppmとして扱ってます。MH-Z19Cセンサのゼロ点校正する方法は、主に次の2つです。

  1. ハードウェア方式
  2. 自動キャリブレーション

(他にもソフトウェア方式があるようですが、ここでは解説しません。)

こちらは更生
こちらは更生

ハードウェア方式でのゼロ点校正

ハードウェア方式でのゼロ点校正では、MH-Z19CのHD端子を7秒間ロー(0V)に引き下げることで、ゼロ点校正します。実際にゼロ点校正する場合は、MH-Z19Cを起動してから20分以上たった後、夜の外で行いましょう。

自動キャリブレーションでのゼロ点校正

実は先ほど紹介したプログラムでは、自動キャリブレーションで校正をしてます。それが、 mhz19.setAutoCalibration(true) の部分です。ライブラリのソースコードをのぞくと、特定のコマンドをUARTで MH-Z19C へ送信してます。

そのコマンドを受け取ると、センサは観測された最小値をゼロ点(400ppm)として校正されるそうです。データシートを読むと、24時間毎に数週間にわたって最小値を読んで自己調整するようです。

After the module works for some time, it can judge the zero point intelligently and do the zero calibration automatically. The calibration cycle is every 24 hours since the module is power on. The zero point is 400ppm.

またデータシートによれば、オフィスや家庭の環境には適してますが、農業温室、農場、冷蔵庫などの環境で使用する場合は、自動キャリブレーションはをオフにするよう推奨されてます。

This method is suitable for office and home environment, not suitable for agriculture greenhouse, farm, refrigerator, etc.. If the module is used in latter environment, please turn off this function.

MH-Z19C データシートより

【コラム】CO2濃度のアレコレ

CO2濃度を測定できる仕組み

MH-Z19Cは、IR赤外線を利用したCO2濃度測定器です。IRとは「Infrared」の略で「下の」という意味があります。可視光でもっとも周波数の低い色は赤色ですが、そのすぐ下の周波数帯の電磁波が赤外線であるからです。

MH-Z19CはNDIR赤外線ガスモジュールでして、非分散型赤外線吸収法と呼ばれる方式でCO2濃度を測定してます。これは、光源から放射された赤外線が、ガス分子により吸収される現象を利用するものです。実は、二酸化炭素分子(CO2)は赤外領域の波長4.26µmを吸収する性質があるのです。 このことから、CO2濃度が高くなればなるほど、赤外線が吸収されることになり、赤外線の強さを測定すればCO2濃度を測定できるわけです。

また、CO2濃度の単位にはppmがよく使われます。これはパーツパーミリオンの略で、1㎥中に0.001㎥(1㍑)のCO2が含まれることを単位としてあらわします。

CO2濃度の月次推移(気象庁)

地球温暖化とCO2濃度の関係が問題視されている昨今ですが、2021年時点で大気中のCO2濃度は410〜420ppmほどになってます。

気になったので、CO2濃度の月次推移を気象庁のオープンデータから調べてみました。

CO2濃度の月次推移 (綾里地点)
CO2濃度の月次推移 (綾里地点)

CO2濃度は年々上昇していることがよく分かりますね。約30年の間にCO2濃度が60〜70ppm高くなってます。

年々暑くなってます
年々暑くなってます

グラフのデータは気象庁で公開している CO2濃度の観測結果 を利用させて頂きました。海外地点のCO2濃度データは、 WMO温室効果ガス世界資料センター (運営:気象庁)のホームページから閲覧可能です。

また、今回作ったCSVからグラフを作成するPythonプログラムもご活用ください。

py
'''
This tool is that make CO2 graph.

Referenced CO2 data:
https://ds.data.jma.go.jp/ghg/kanshi/obs/co2_monthave_ryo.html

Created by Toshihiko Arai
(c) https://101010.fun
'''
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

if __name__ == "__main__":
    # nkfなどでshift-jisからutf-8へ変換しておく
    # $ nkf -w --overwrite co2_monthave_ryo.csv
    
    # CSVヘッダー
    # 年,月,二酸化炭素濃度の月平均値(綾里)[ppm],
    filename = 'co2_monthave_ryo.csv'

    df = pd.read_csv(filename, usecols=['年', '月', '二酸化炭素濃度の月平均値(綾里)[ppm]'], na_values=['--']) # --文字列は欠損値とする

    # ## pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer
    # print(df['月'].astype(int))
    #
    # 原因: 下部にコメントが書かれてたため、NaNデータがまじり込んで数値変換できなかった
    #
    df = df.dropna(how='any') # 欠損値が一つでも含まれる行は削除する
    print(df)

    # TypeError: no numeric data to plotのエラー回避
    df['二酸化炭素濃度の月平均値(綾里)[ppm]'] = df['二酸化炭素濃度の月平均値(綾里)[ppm]'].astype(float)

    df['月'] = df['月'].astype(int).astype(str) # astype(str) -> cast float to str
    df['date'] = df['年'] + '年' + df['月']  + '月'

    df = df.drop(columns=['年', '月']) # 不要なカラム(列)の削除
    df['date'] = pd.to_datetime(df['date'], format='%Y年%m月') # グラフで日付を短く表示させるため必要
    df = df.set_index('date') # dateカラムをインデックスにする

    print(len(df)) # データの件数
    print(df.columns.values) # ヘッダー(項目名一覧)    
    print(df)

    fig, axes = plt.subplots(figsize=(14, 10))
    fig.suptitle("二酸化炭素濃度の月次推移 (綾里地点)")
#    fig.tight_layout() # 余白を少なく表示

# 大気環境観測所(綾里)は岩手県大船渡市三陸町綾里
# https://www.data.jma.go.jp/gmd/env/ghg_obs/station/station_ryori.html
    ax = df['二酸化炭素濃度の月平均値(綾里)[ppm]'].plot()
    ax.set_xlabel('')
    ax.set_ylabel('二酸化炭素濃度の月平均値(綾里)[ppm]')

#    plt.show()

    plt.savefig('ryozaki-co2-graph.png')
    plt.close('all')

室内CO2濃度と眠気・集中力の関係😪

▼ 「室内CO2濃度と眠気・集中力の関係」という面白い調査がありましたので紹介します。

室内CO2濃度と眠気・集中力の関係

とある高校で、CO2濃度を測定して、眠気と集中力の関係を検証したそうです。この資料によると、CO2濃度が高くなるにつれ、眠気が増し集中力が低下する現象が確認されたそうです。 脳を働かせるときは大量の酸素を使いますから、CO2濃度が高くなれば節約モードになり、眠気を起こすのも当然といえば当然でしょうね。 CO2センサを実用化するアイデアとして、CO2濃度が高くなったら音を鳴らすなどして、換気をうながすアラームを作ってみても面白そうですね!

集中しなくっちゃ!
集中しなくっちゃ!

【おまけ】やってみよう!UART通信

UARTとは「Universal Asynchronous Receiver Transmitter」の略です。簡単に言えば、クロック信号を必要としない非同期通信です。

UARTのシリアル通信では、次のような特徴が挙げられます。

  • ノイズに強い
  • 非同期で通信
  • プロトコルは自分で考えなければならない
  • お互いの通信速度を同じにする必要がある(Baud Rate)
  • 低速なデータ通信に向いている

実はUART通信を普段使っているのですが、Ardudino IDEのシリアルモニタへ表示させるSerial.println()がまさにそれです。 Baud Rateで文字化けした経験は何度もあります。I2Cなどのようなクロック信号がないことからも、お互いの通信速度を同じ設定にしないと解読できないというのは当たり前ですね。

一方で、SPIやI2C通信では、同期するためのクロック信号が必要となります。

  • 高速で通信できる
  • 同期通信
  • ノイズに弱い

とくにSPIなどの同期通信がノイズに弱いというのは、身を持って体験しました。20メートルほどのLANケーブルを使ってSPI通信でセンサとやり取りしたのですが、取得したデータがどうも不安定でして、原因追求にかなりの時間を費やしてしまいました。結果、LANケーブルの配線を変えたりすることで解決したのですが、おそらくSPIのクロック信号の安定性が原因だったように思います。長距離伝送になればなるほど、ケーブルの寄生容量や、外部ノイズが深刻な問題になりますので。(ここらへんは今後も要研究ですね)

山と言ったら、川と返す

山と言ったら、川と返す
山と言ったら、川と返す

さて、UARTのシリアル通信はArduino IDEのシリアルモニタで慣れ親しんでいるように、誰でも簡単に実装できます。 ここではUARTで「山と言ったら、川と返す」簡単なプログラミングを作ってみましょう。

先にも述べましたが、UART通信には取り決めが何もなく、約束事を自分で設計する必要があります。ここでは「山と言ったら、川と返す」というのが約束事になります。このことをコンピュータの世界では、プロトコル(約束事)と呼んでいます。インターネットでよく使われるHTTPもプロトコルです。ほかにも数え切れない種類のプロトコルでコンピュータ世界が成り立ってます。

プログラム

Arduino IDEのシリアルモニタから「yama」と入力して送信すると「kawa」と返してくれるArduinoプログラムです。 このスケッチをArduinoへアップロードして、「yama」を送信して動作確認なさってください。

cpp
/**
 * @file        Echo.ino
 * @author      Toshihiko Arai
 * @date        2022/05/13
 * @brief       When "yama" is received by serial communication, "kawa" is returned.
 */

// Serial commands
const int COMMAND_SIZE = 5;
const char commandYama[COMMAND_SIZE] = {'y', 'a', 'm', 'a', '\0'}; // 終端文字\0(ヌル)が必要
const char commandKawa[COMMAND_SIZE] = {'k', 'a', 'w', 'a', '\0'};
char buf[COMMAND_SIZE];
int bufIndex = 0;

void resetBuffer()
{
    bufIndex = 0;
    memset(buf, 0, COMMAND_SIZE);
}

bool checkCommand(char *s1, char *s2) // 文字列比較
{
    return strncmp(s1, s2, COMMAND_SIZE) == 0;
}

void setup()
{
    Serial.begin(115200);
    resetBuffer();
}

void loop()
{
    while (Serial.available() > 0)
    {
        // buf = Serial.readStringUntil('\n');
        int d = Serial.read();
        if (d != 10) // 改行文字
        {
            buf[bufIndex] = (char)d;
            bufIndex++;
        }
        else
        {
            buf[bufIndex] = '\0';
            // for (int i = 0; i < COMMAND_SIZE; i++)
            // {
            //     Serial.println((int)buf[i]);
            // }

            if (checkCommand(buf, commandYama))
            {
                Serial.println(commandKawa);
            }
            else
            {
                Serial.println("Unknown command.");
            }
            resetBuffer();

            break;
        }
    }
}
Serial.println() はデータの出力でしたが、Serial.read()のようにデータの入力もできます。

改行文字や終端文字、制御コードなど、普段意識してない目に見えない文字まで考慮する必要がありますが、ここら辺は組み込み系プログラミングのスキルアップへ役立ちます。徐々に慣れていきましょう。 また、これらが理解できれば、今回使用した mhz19_uartライブラリ のソースコードを読み解くこともそんなに難しくないはずです。 MH-Z19C データシート と合わせて確認してみてください。 ▼ C++の詳しくはこちら。

さいごに

いろいろな通信がありますね
いろいろな通信がありますね

Arduino開発ではSPIやI2C通信のセンサモジュールが多いため、UARTを使う機会はあまりありませんでした。ですがUARTを使えば、Arduinoを子機にして操作できたり、既存のSPIセンサなど使わずとも、Arduino内臓のADCを使って自作のデジタルセンサも作れますよね。また、コマンド次第では複数の子機を選択・操作することも可能でしょう。

有線におけるUART通信での長距離伝送にも興味があります。クロック信号がない分、SPIやI2Cよりも長距離伝送に優れているのではと考えてます(そのうち実験検証できたらと)。もちろん遠隔でのやり取りを考えると、 ESP-NOW のような無線通信のほうが手っとり早い気もしますが。

兎にも角にも、SPIやI2C以外の手持ち札が増えたのは嬉しかったです。また何か発見しましたら記事へまとめていきますので、乞うご期待ください。

関連記事

 
最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

スマート農業(スマートアグリ)で使えるセンサー
水耕栽培はじめよう!
関連記事