技術約6分で読めます

M5Stack CoreS3とCO2L Unit (SCD41)でCO2モニターを作ってSwitchBot実測値に校正した

いけさん目次

前回のmicroSD切り分けに続いて、M5Stack CoreS3にCO2センサーをつないで部屋のCO2モニターを作った。
センサーはM5公式のCO2L Unit。中身はSensirionのSCD41で、CO2に加えて温度・湿度も取れる。

検証環境は前回と同じくWindows 11 + arduino-cli 1.5.1 + esp32コア3.3.10。
ライブラリはM5Unified 0.2.17に加えてM5Unit-ENV 1.5.0を使う。

CO2L Unit (SCD41)

これもアキバのマルツで購入。

CO2L Unitのパッケージ

袋の中身はユニット本体とGroveケーブル1本。

CO2L Unit本体とGroveケーブル

ユニット裏のラベルに「SCD41 (SINGLE SHOT) / PORT A I2C / SCL SDA 5V GND」と書いてある。
つまり接続先はPort A、プロトコルはI2C。

SCD41とPort A I2Cのラベル

SCD41は光音響方式(photoacoustic)のCO2センサー。主なスペックは以下の通り。

項目
CO2出力範囲0〜40000 ppm
指定測定範囲400〜5000 ppm
CO2精度±50ppm ±2.5%m.v.〜±40ppm ±5.0%m.v.(レンジによる)
CO2応答速度 (τ63%)60秒
湿度精度±6%RH(動作範囲0〜95%RH)
温度精度±0.8℃
供給電圧2.4〜5.5V
供給電流平均15mA / 最大205mA
動作温度-10〜60℃

SCD40との違いは2つ。指定測定範囲がSCD40の400〜2000ppmに対して400〜5000ppmと広く、シングルショット測定(間欠測定で省電力化)にも対応する上位版にあたる。
この部屋はかつて3000ppmを叩き出したことがあるので、測定上限が高いほうを選んだ。SCD40だと振り切れる。

Port Aはどれか

CoreS3には見た目の似たコネクタが3つある。
ユニットを挿すPort Aは本体側面の赤いコネクタで、USB-Cと同じ面にある。

ポート場所用途
Port A本体側面(USB-Cの隣)I2C
Port BDIN BASE背面GPIO
Port CDIN BASE背面UART

本体側面の赤いPort A

Port AのI2Cピンはハードコードせず、M5Unifiedから取得できる。

int sda = M5.Ex_I2C.getSDA();  // CoreS3では G2
int scl = M5.Ex_I2C.getSCL();  // CoreS3では G1

実行するとCoreS3ではSDA=G2、SCL=G1が返ってくる。
起動画面にピンを表示しておくと、配線を疑うときに便利だった。

センサー探索中の画面。SDA=G2 SCL=G1

コード

M5Unit-ENVにSCD4X用のクラスが入っている(SparkFunのSCD4xライブラリがベース)。
センサー未接続でも起動して、見つかるまで1秒おきに探し続ける作りにした。
これならユニットを後から挿しても勝手に認識される。

#include <M5Unified.h>
#include "M5UnitENV.h"

SCD4X scd4x(SCD4x_SENSOR_SCD41);
bool sensorFound = false;

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  Serial.begin(115200);
  M5.Display.setTextSize(2);
  M5.Display.println("searching sensor...");
}

void loop() {
  M5.update();

  if (!sensorFound) {
    if (scd4x.begin(&Wire, SCD4X_I2C_ADDR,
                    M5.Ex_I2C.getSDA(), M5.Ex_I2C.getSCL(), 400000U)) {
      sensorFound = true;
      scd4x.stopPeriodicMeasurement();
      scd4x.startPeriodicMeasurement();
    } else {
      delay(1000);
    }
    return;
  }

  if (scd4x.update()) {  // 新しい測定値があればtrue(5秒間隔)
    uint16_t co2 = scd4x.getCO2();
    float temp = scd4x.getTemperature();
    float hum = scd4x.getHumidity();
    // 画面描画(CO2値で色分け: <1000緑 <1500黄 それ以上赤)
  }
  delay(1000);
}

測定は5秒間隔の連続測定モード。
update()は新しいデータが来たときだけtrueを返す。

画面はCO2値を大きく出して、1000ppm未満は緑、1500ppm未満は黄、それ以上は赤で色分けした。
1000ppmは建築物環境衛生管理基準で換気の目安とされる値。

Groveケーブルを赤いPort Aに挿すと、数秒でセンサーが見つかって測定が始まった。

CO2モニター動作中。1697ppm

いきなり1697ppmの赤表示。
ただこれは換気が悪いのではなく、机の上で作業している自分の呼気がセンサーに直接かかっていたのが原因だった。
SCD41は至近距離の呼気で簡単に1500〜2000ppmまで跳ねる。

SwitchBotのCO2モニターと数字が合わない

値の妥当性を確認するため、SwitchBotのCO2センサー(NDIR方式の市販品)と並べて比較した。

最初の比較では M5Stack側 1697ppm vs SwitchBot 533ppm で「全然違うじゃないか」となったが、これはフェアな比較になっていなかった。
SwitchBotの画面には「28 minutes ago」とあり、28分前の測定値と今の呼気スパイクを見比べていたことになる。
SwitchBotは本体ボタンを押すとその場で再測定できるので、以降は押した直後の値で比較した。

SwitchBot側の表示。533ppmは28分前の値

呼気の影響を除くため2台を並べて放置し、SwitchBotを再測定した直後の同時比較がこれ。

CO2温度湿度
SwitchBot(基準)748 ppm28.9℃62%
CoreS3 + SCD41976 ppm28.4℃81.6%

CO2は+228ppm(約+30%)、湿度は+20ポイントのズレ。
CO2精度スペックを最も緩い条件(±40ppm ±5.0%m.v.)で見ても976ppmなら±89ppmなので、明確にスペック外。湿度も±6%RHを大きく超えている。
この個体は工場校正(Factory calibration)のままで、自動セルフキャリブレーション(ASC)が一度も走っていない状態。
ASCは「1週間のうちに屋外相当の新鮮な空気(約400ppm)に定期的に触れる」ことを前提に少しずつ補正する仕組みなので、買ってきた直後の値には反映されていない。

強制校正(FRC)でSwitchBotに合わせる

すぐ合わせたい場合はFRC(Forced Recalibration)を使う。
リファレンスとなるCO2濃度を指定すると、現在の読み値がその値になるよう内部オフセットを書き換える機能で、SCD4XライブラリではperformForcedRecalibration()がそれ。

本来は屋外の新鮮な空気(約400〜420ppm)でやるのが正攻法だが、今回は隣にNDIRの実測値があるので、SwitchBotの読み値をリファレンスにした。

FRCの手順には制約が2つある。

  • 実行前に3分以上、連続測定を回しておく
  • 測定を止めてから実行する(stopPeriodicMeasurement() → FRC → 再開)

コードにシリアルコマンドを足して、PCからfrc 1069のように送ると校正が走るようにした。

// loop()内: シリアルから "frc 1069" で強制校正
if (Serial.available()) {
  String cmd = Serial.readStringUntil('\n');
  cmd.trim();
  if (cmd.startsWith("frc ")) {
    uint16_t ref = cmd.substring(4).toInt();
    scd4x.stopPeriodicMeasurement();  // FRCは測定停止中に実行
    delay(600);
    float correction = 0;
    bool ok = scd4x.performForcedRecalibration(ref, &correction);
    Serial.printf("FRC %s, correction=%.1f ppm\n", ok ? "OK" : "FAILED", correction);
    scd4x.startPeriodicMeasurement();
  }
}

SwitchBotをボタンで再測定して1069ppmだったので、それをリファレンスに実行した。

FRC start: ref=1069 ppm
FRC OK, correction=73.0 ppm
CO2: 1171 ppm, Temp: 34.3 C, Hum: 60.1 %
CO2: 1089 ppm, Temp: 33.9 C, Hum: 61.1 %
CO2: 1057 ppm, Temp: 33.6 C, Hum: 62.0 %
CO2: 1016 ppm, Temp: 33.4 C, Hum: 62.7 %

校正後はSwitchBotの読み値に追従するようになった。
湿度も81%→62%前後になり、SwitchBotの66%とほぼ揃った。
温度が34℃前後と高く出ているのは測定再開直後の一時的なもので、数分で下がって安定する。