M5Stack + Grove CO2 センサSCD30の動作テスト

2025.2.16 Coskx Lab  

1 はじめに

M5Stack Basic V2.7にGrove CO2 Sensor SDC30 を接続して二酸化炭素濃度を測定します。

Grove CO2 SensorはSensirion社のCO2 Sensor SCD30を使用していて,Groveインターフェイスを使用してマイコンに接続できるようになっています。(I2Cインターフェイス(クロック信号SCLと、データ信号SDA)+ +V + GNDの4線ケーブル使用)
M5Stackとは付属の4線ケーブルで接続するだけで配線完了となります。



2 使用環境

3 接続

M5Stack Basic V2.7にはPort.A.I2Cコネクタに付属ケーブルで接続します。




4 テストプログラム

https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library
を参考にさせていただきました。
利用関数
SCD30_Arduino_Libraryから次の関数を利用しています。

SCD30#getTemperatureOffset()
 SCD30内の不揮発メモリから温度補正値(deg)を取得します。(float)
SCD30#setMeasurementInterval(uint16_t)
 SCD30内の不揮発メモリに測定インターバル(sec)を保存しま。す
 Sensirion社の推奨範囲の5秒に設定しています。
SCD30#setTemperatureOffset(float)
 SCD30内の不揮発メモリに温度補正値(deg)を保存します。
 最後に設定した値が,再電源投入時に有効になります。
 有効になっても,データに現れるのはしばらく待ってからです。
SCD30#setForcedRecalibrationFactor(uint16_t)
 二酸化炭素センサの較正を行います。
 現在測定中のCO2濃度をこの関数で指示した値にします。
 実際には,新鮮な外気中にセンサを5分程おいてから,
 この関数で400ppmの値を与えます。
 400ppmは大気中の平均的な二酸化炭素濃度です。

SCD30#dataAvailable()
 SCD30がデータ取得が終わり,データ呼び出し準備ができたことを示します。(boolean)
 設定された測定インターバルごとにデータ取得し,内部の準備OKのフラグが立ち,
 trueを読みだした直後に準備OKのフラグは消えてしまいます。
SCD30#getCO2()
 CO2濃度(ppm)を取得します。(uint16_t)
SCD30#getTemperature()
 温度(degC)を取得します。(float)
SCD30#getHumidity()
 湿度(%)を取得します。(float)

M5Stackのボタン
M5Stackの3つのボタンを使用します。
Aボタン setForcedRecalibrationFactor関数で二酸化炭素センサの較正を行います。
     新鮮な外気中にセンサを5分程おいてから,このボタンを押します。
Bボタン 温度補正値を0.1度増やします
Cボタン 温度補正値を0.1度減らします
温度補正では,信頼できる温度計とSCD30が表示している温度との差を使います。
表示されている補正値(offset)は電源再投入時に有効になります。
また補正値がほかの測定値に影響を与えるためにはしばらく時間がかかります。
補正値は次の式で得られますが,センサの方が周囲の温度より高温のため,必ず正の値になります。(現在の補正値が0の場合)
補正値=SCD30の表示温度ー標準温度計の温度
すでに,補正値がある場合の新規補正値は次の式になります。
新規補正値=(SCD30の表示温度ー標準温度計の温度)+現在の補正値


  動作テスト用プログラム本体

//grobe-SCD30_Co2_sensor.ino

#include <M5Stack.h>
#include <SparkFun_SCD30_Arduino_Library.h>
#include <Wire.h>

#define CO2_CALIBRATION_FACTOR 400  // CO2 concentration in fresh outdoor air (ppm)
#define TEMPERATURE_OFFSET_MAX 999  // tempreture offset 0..999 (0.0 .. 99.9 deg)
#define OFFSET_WORK_DURATION 1000
#define SCD30_WORK_INTERVAL 5 //5sec

SCD30 scd30;

int tempoffset_deg=0; //decidegree

void setup()
{
  Serial.begin(115200);
  M5.begin();
  M5.Power.begin();
  M5.Lcd.fillScreen(TFT_BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setTextSize(3);
  M5.Lcd.println("   // SCD 30 //");
  M5.Lcd.println("SCD30 setup...");
  Serial.println("// SCD 30 //");
  Serial.println("SCD30 setup...");
  Wire.begin();
  if (scd30.begin() == false) {
    Serial.println("fail!");
    Serial.println("SCD30 not detected. You should check wiring. Freezing...");
    M5.Lcd.println("fail!");
    M5.Lcd.setTextSize(2);
    M5.Lcd.println("SCD30 not detected. You should check wiring. Freezing...");
    while (1);
  }
  M5.Lcd.println("\r\ncompleted");
  Serial.println("completed");
  tempoffset_deg = (int)(scd30.getTemperatureOffset()*10.0);
  scd30.setMeasurementInterval(SCD30_WORK_INTERVAL); //stored in non-volatile memory of SCD30
  delay(2000);
  instructions();
  workOnTempOffset();
}

void instructions()
{
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(0, 5 * 24 + 4);
  M5.Lcd.println("Expose the CO2 sensor to\r\nfresh outdoor air for 5\r\nmin before calibrating it.\r\n");
  M5.Lcd.println("buttonA: calibrate sensor");
  M5.Lcd.println("buttonB: TempOffset -0.1");
  M5.Lcd.println("buttonC: TempOffset +0.1");
}

void workOnTempOffset()
{
  float tmpofs =(float)tempoffset_deg/10.0;
  scd30.setTemperatureOffset(tmpofs); //stored in non-volatile memory of SCD30
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setCursor(0, 4 * 24 + 4);
  M5.Lcd.setTextSize(2);
  M5.Lcd.printf("TempOffset %4.1f deg\n", tmpofs);
  Serial.printf("TempOffset %4.1f deg\n", tmpofs);
}

void displayDatas()
{
  uint16_t co2_ppm;
  float temperature_degc;
  float humidity_pct;
  float tmpofs =(float)tempoffset_deg/10.0;

  // get sensor datas
  co2_ppm = scd30.getCO2();
  temperature_degc = scd30.getTemperature();
  humidity_pct = scd30.getHumidity();

  Serial.print("co2(ppm):");
  Serial.print(co2_ppm);
  Serial.print(" temp(degC):");
  Serial.print(temperature_degc, 1);
  Serial.print(" humidity(%):");
  Serial.print(humidity_pct, 1);
  Serial.print(" tempoffs(deg):");
  Serial.println(tmpofs, 1);

  M5.Lcd.setCursor(0, 1 * 24);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  M5.Lcd.printf("CO2   %4d ppm\n", co2_ppm);
  M5.Lcd.printf("Temp  %4.1f degC\n", temperature_degc);
  M5.Lcd.printf("Humid %4.1f %%\n", humidity_pct);
}

void loop()
{
  M5.update();
  if (M5.BtnA.wasReleased()) {
    scd30.setForcedRecalibrationFactor(CO2_CALIBRATION_FACTOR);
    delay(3000);
  } else if (M5.BtnB.wasReleased()) {
    tempoffset_deg--;
    if (tempoffset_deg < 0) tempoffset_deg = 0;
    workOnTempOffset();
  } else if (M5.BtnC.wasReleased()) {
    tempoffset_deg++;
    if (TEMPERATURE_OFFSET_MAX < tempoffset_deg) tempoffset_deg = TEMPERATURE_OFFSET_MAX;
    workOnTempOffset();
  }
  if (scd30.dataAvailable()) {
    displayDatas();
  }
}

5 実行の様子と考察

窓を占めた状態の室内(6畳程度)にセンサをおいて,シリアルモニタで連続データを取得してみました。

02:56:26.147 -> co2(ppm):1145 temp(degC):21.0 humidity(%):40.5 tempoffs(deg):0.0
02:56:31.704 -> co2(ppm):1147 temp(degC):21.0 humidity(%):40.6 tempoffs(deg):0.0
02:56:37.301 -> co2(ppm):1147 temp(degC):21.0 humidity(%):40.6 tempoffs(deg):0.0
02:56:42.901 -> co2(ppm):1145 temp(degC):21.0 humidity(%):40.6 tempoffs(deg):0.0
02:56:48.474 -> co2(ppm):1149 temp(degC):21.0 humidity(%):40.4 tempoffs(deg):0.0
02:56:54.029 -> co2(ppm):1145 temp(degC):21.0 humidity(%):40.3 tempoffs(deg):0.0
02:56:59.611 -> co2(ppm):1143 temp(degC):21.0 humidity(%):40.3 tempoffs(deg):0.0
02:57:05.194 -> co2(ppm):1143 temp(degC):21.0 humidity(%):40.6 tempoffs(deg):0.0
02:57:10.778 -> co2(ppm):1143 temp(degC):21.0 humidity(%):40.4 tempoffs(deg):0.0
02:57:16.350 -> co2(ppm):1142 temp(degC):21.0 humidity(%):40.2 tempoffs(deg):0.0
02:57:21.903 -> co2(ppm):1140 temp(degC):21.0 humidity(%):40.4 tempoffs(deg):0.0
02:57:27.482 -> co2(ppm):1140 temp(degC):21.0 humidity(%):40.4 tempoffs(deg):0.0
02:57:33.079 -> co2(ppm):1140 temp(degC):20.9 humidity(%):40.4 tempoffs(deg):0.0

窓を開けてしばらく外気を導入するとCO2濃度は400ppmくらいになりました。
M5Stack単体での実行の様子は次のようになります。



なお,推奨使用手順がSensirion社の資料にあり,次のように書かれています。
(1)sampling interval を設定する。
(2)Forced recalibration を行うかまたは Automatic self-calibration を設定する。
(3)temperature offset を設定する。
(Sensirion_CO2_Sensors_SCD30_Low_Power_Mode.pdf)

6 まとめ

Grove CO2 センサSCD30を使って,二酸化炭素濃度測定値を表示することができました。
SCD30用のライブラリ(sparkfunのライブラリを使用しました)があるため,テストプログラムの作成は難しくはありませんでした。
精度よく測定するためには,推奨使用手順に従って,較正や温度補正を正しく行う必要があります。