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

こんなこと、やります。

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

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

つかうもの

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

MH-Z19C

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

MH-Z19C
MH-Z19C

▼ Amazonでもたくさん販売されているので、ご参考になさってみてください。

MH-Z19Cセンサは、赤外線を使って空気中のCO2濃度を測定します。MH-Z19Cセンサからデータを読み取るには、UART通信または、PWMで値を読み取る方法があります。

本記事では、もっともカンタンな方法として、既存のライブラリを使ってUART方式でMH-Z19からCO2濃度を読み取る方法をご紹介いたします。

Arduino

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

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

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には、ゼロ点校正の方法が2つあります。それは手動による方法と自己校正です。400ppmをゼロ点として校正されます。校正については後ほどくわしく解説します。

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プログラムもよろしければご活用ください。

'''
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濃度が高くなったら音を鳴らすなどして、換気をうながすアラームを作ってみても面白そうですね!

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

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

MH-Z19CとUARTでカンタンにやり取りできるライブラリを使用します。こちらの「nara256/mhz19_uart」を使わせてもらいました。

上記のページからzipでダウンロードして解凍してください。それをArduinoのライブラリディレクトリへ移動すればOKです。

MH-Z19CだけでなくMH-Z19Bでも使えると思います。

また、このライブラリはArduinoとESP32のどちらでも使うことができます。UART通信ですので、Arduinoの場合はソフトウェアシリアルを、ESP32の場合はハードウェアシリアルを使用します。

配線だけ間違えないように注意してください。

ESP32(Arduino)とMH-Z19Cの配線

TXはデータ送信なので、受信側ではRXにつなぐというルールを間違えないようにしてください。

ESP32の場合は、安定性の問題からハードウェアシリアル2へつなぎます。Arduinoの場合は、ソフトウェアシリアルを使用します。

ESP32

MH-Z19C ESP32
Vin 5V
GND GND
RX GPIO17 (TX)
TX GPIO16 (RX)

Arduino UNO

MH-Z19C Arduino UNO
Vin 5V
GND GND
RX GPIO2 (TX)
TX GPIO1 (RX)

LANケーブルを使ってESP32とMH-Z19C、DHT22を配線
LANケーブルを使ってESP32とMH-Z19C、DHT22を配線

CO2濃度を計測するサンプルプログラム

こちらが、CO2濃度を計測するサンプルプログラムです。ライブラリのおかげでカンタンにCO2濃度のデータを読み取ることができました。

#include <MHZ19_uart.h>

const int rx_pin = 16; // Serial rx pin no
const int tx_pin = 17; // Serial tx pin no

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(5000);
}

MH-Z19Cには温度センサもついますので、気温を読み取ることも可能です。ただし精度は良くありません。本格的に気温を測定したい場合は、DHT11やDHT22を使うことをオススメします。

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

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

実は今回、企業様から依頼された案件をきっかけに、CO2センサをはじめて使うことになりました。

写真のように、DHT22と組み合わせてTFTディスプレイへ温度・湿度・CO2濃度を表示しています。AWSのIoT coreと連携して、データをMQTTで送信させています。

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

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 データシートより

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

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

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

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

実はUART通信を普段使っているのですが、Ardudino IDEのシリアルモニタへ表示させるSerial.println()がまさにそれです。

Baud Rateで文字化けした経験は誰もがあると思います。クロック信号がないことからも、お互いの通信速度を同じにしないと解読できないというのは理解できるかと思います。

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

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

山と言ったら、川と返す

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

さて、UARTのシリアル通信はArduino IDEのシリアルモニタで慣れ親しんでいるように、誰でもカンタンに実装できます。

ここではUARTで「山と言ったら、川と返す」カンタンなプログラミングを作ってみましょう。

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

プログラム

Arduino IDEのシリアルモニタから「yama」と入力して送信すると「kawa」と返してくれるArduinoプログラムです。

このスケッチをArduinoへアップロードして、「yama」を送信して動作確認なさってみてください。

/**
 * @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++クラスの理解の必要はありますので、C++に興味ある方はこちらの記事もご参考になさってみてください。

さいごに

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

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

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

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

記事に関するご質問などがあれば、
Twitter または お問い合わせ までご連絡ください。
関連記事