勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

室内炭酸ガスの濃度測定―今どき役立ちそう

f:id:a-tomi:20200628175750j:plain

テレワーク中の部屋の換気が悪いと眠くなります。また、人が少しでも集まる部屋は換気が推奨されているところですね。換気の指標として部屋のCO2(二酸化炭素)を測り、1000PPM(0.1%)以下に保てれば「換気がとても良い」と言えるようです(注1)。

世界中で部屋に籠り勝ちの昨今、二酸化炭素の高価な測定器もよく出回っているようですが、ここではもちろん自作に挑戦ですね。上の写真は今回テストした1つです。

注1:換気については次のサイトの説明が分かり易いと思いました。http://www.jvia.jp/column/igi_13.htm

 

古典的なCO2の測定方法は、CO2が赤外線の特定波長を吸収する性質を使う大仕掛けなもの(NDIR)。最近は小さくて精度の高い金属酸化物のセンサーが開発され、世界で多く使われています。今回使うCCS811はオーストリアの超小型AirQualityセンサーです。

f:id:a-tomi:20200628182905j:plain

世界に出回っているブレークアウトモジュールを使います。

f:id:a-tomi:20200628183128j:plain

センサー自体の表面印字が少し違いますが、メーカーのリリースノートを調べてみたらこちらのほうが版が新しいようです。このモジュールにはI2Cプルアップ抵抗も組み込まれていますね。テストのため海外ネットで2個入手しましたが1つ8.5ドル程でした。

ストロベリーリナックスでは次のブレークアウトモジュールを日本国内で売っています。テストのためこちらも1つ購入しました。ずいぶん小さいですが価格は倍でした(^^;)。こちらにはPull-up抵抗等は入ってません。

f:id:a-tomi:20200628200716j:plain

どちらにしても電圧は最大3.3Vで、ボードにレギュレータ等はつけられていません。ですので、5Vのマイコンで動かすにはロジックレベル変換が必要です。

I2C接続部分なので2チャンネルのレベル変換があればよいわけですが、手持ちが4チャネル用ICでそれを使用します。次の写真ですが、安価なぶん、ずいぶん雑な作りかも。とはいえどれもこれまでちゃんと動き、なんら不満はないですが^^;

f:id:a-tomi:20200628202453j:plain

次に汚い手書きですみませんが、回路(というより接続図)です。

f:id:a-tomi:20200628202917j:plain

環境表示器としてこのAirQualityセンサーの他に、気象センサー(ボッシュのBME280)、リアルタイムクロック(RTC-DS3231)、表示用のLCD(I2C 20x4行)、時刻設置用のボタンなどをとりつけます。CCS811以外のプログラムは単純です(すべてのスケッチをこの記事の後のほうにつけておきます)。

3Vの供給は当初は3.3VレギュレータICを使っていましたが、CCS811では最大値に近いことがわかり可変三端子レギュレータに取り替えて3V未満にしました。Arduino出力の3.3Vは使っていません(このセンサーの最大値に近いので注意が必要です、詳しくは後のほうに書きます)。Arduinoは開発時だけ使い、組み立てるときはどっちみちATmega328P単体だけで動かすわけなので。

試作の結果、感度はとても鋭敏で30センチほどのところから少し息を吹きかけただけでも即反応するのがわかります。

f:id:a-tomi:20200628204753j:plain

仕様として、動かし初めに常に「Run-inタイム」が20分必要とされます。別途ロガーを作って記録したら、毎回ほぼ12分で測定値が安定するのがわかりました。

f:id:a-tomi:20200628204454j:plain

閉め切った狭い部屋で長時間テレワークをするときは、換気を忘れるとCO2濃度がどんどん上昇し1000PPMをすぐに越えます。そうすると眠気やら飽きなど色々感じます。換気の必要性を観察・記録するためにロガーがあると便利そうです。この際はもう1つのセンサーでそれも欲張ってトライしました。殆ど同じですからね。

f:id:a-tomi:20200628205501j:plain

次がその試作です。値はマイクロSDにCSV形式で記録します。なおCO2のPPM値4桁のみをTM1637Displayに出力します。ロガーとしてはこれで十分だと思います。

f:id:a-tomi:20200628205640j:plain

どちらの試作機でも、センサーがエラーを出したときは、9xEEとしてEEにエラーコード(Error status)を出すようにしました。

両方を比較するとセンサーの示す値は大きくは異なりません。

f:id:a-tomi:20200628210021j:plain

上の写真ではほぼ一致していますが、大きく差が出る場合も5%程度なので一安心です。また、最近は換気に気を付けているので空気が綺麗です。

このAirQualityセンサーは早い段階で48時間のBurn-inをするように勧められていますので早速開始し、その間ロガーで記録を続けました。

ところがバーンイン中の真夜中に無人の工作室で突然記録値が上昇し、エラーコード16(つまり0x10)が表示され止まっていました。エラー内容は「ヒーター温度が範囲外になった」とのエラーです。もう1台の表示装置側には異常なく、表示とバーンインを続けていました。

原因を詳しく調べるためにセンサーのRawデータを取り出して電流値などを見ると、仕様より少し高めでした。3.3Vレギュレータ電圧が高すぎたのかもしれないと気づいて測ると3.36Vです。限界近くで使うのはエラーも起きやすいわけで、これを可変にとりかえて、どちらの装置のCS811へも3V未満の供給としました。エラーを出したロガー側は思い切り低く2.7V付近に設定しました。可変の電圧レギュレータとしては、LM317T、LM1084Tなど何でもOKです。

余談ではありますが、回路図のように1つのポテンシオメーターを使い、可変部を

Adjにセットすると分圧に温度の影響がないですね。

f:id:a-tomi:20200628211201j:plain

f:id:a-tomi:20200628211246j:plain

念のためにロガーのI2C信号授受に問題が全く出てないことを確認しました。

f:id:a-tomi:20200628213732j:plain

 

さて、思わぬエラーの説明で手間取りましたが、このCCS811AirQualityセンサーはずいぶん鋭敏な賢いセンサーだと思います。奥が深い仕様で点検すべきマニュアルの種類がデータシートの最後にまとめられています。アプリケーションノートなどを含めて10種にもおよびます。

それらで気づいた注意点や、その後の使用状況や記録などについては記事が長くなり過ぎますので、機会を改めて書こうと思います。この段階での確認が終わったら単体のATmega328pでケースに組みたいと思っています。

次に表示装置のプログラム、ロガーのプログラムの順にArduinoスケッチを掲載しておきます。

/* *******************************************************
   Environment display V02  
                (c)2020.06.20-2020.6.27 Akira Tominaga
   Function:
    Display temperature,  humidity, pressure, CO2, and TVOC.
   Revisions:
    V00: Original program devloped on June 20, 2020.
    V01: Show sensor error status to serial printer.
    V02: Show sensor error status on display as "9XEE".
 *  ******************************************************/
#include "Wire.h"
#include "LiquidCrystal_I2C.h" // 20x4 I2C Liquid Crystal display
#include "ccs811.h"         // CCS811 Air quality (CO2) sensor
#include "BME280I2C.h"      // BME280 Weather sensor
// *** for LCD 20x4
#define LCDadr 0x27         // 20x4 I2C Liquid Crystal display
//LCD parameters are  I2Cadr, en,rw,rs,d4,d5,d6,d7,bl,blpolarity
LiquidCrystal_I2C lcd(LCDadr, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
// *** for BME280
BME280I2C BME;           // default standby time = 1000 ms
// *** for CCS811
#define AQadr 0x5A          // AQ sensor adr (0x5B alternative)
CCS811 AQ;
uint16_t CO2;               // CO2 ppm(400 to 8192), or AQ err_code
uint16_t TVOC;              // TVOC ppb(0 to 1187)
char strCT[10];             // edit area for CO2 and TVOC
// *** for clock control buttons
#define Md 2                // Mode button (L=on)
#define Tm 3                // Time button (L=on)
#define St 4                // Set button (L=on)
// *** for Real Time Clock
byte  vR[8];                // values in RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // integers for RTC data
#define RTC_addr 0x68
#define mdI 0               // Initial mode
#define mds 1               // ss
#define mdm 2               // mm
#define mdh 3               // hh
#define mdW 4               // WW=Day of Week
#define mdD 5               // DD
#define mdM 6               // MM
#define mdY 7               // YY
#define sS 0
#define mM 1
#define hH 2
char  strYMDHMS[20];        // edit area for calendar and clock
#define longPush 0x01
#define shortPush 0x00
byte pushMd = shortPush;
char Data[5];               // 4 bytes work area for RTC

void setup() { // ***** Arduino setup *****
  Serial.begin(9600);
  Wire.begin();
  pinMode(Md, INPUT_PULLUP);
  pinMode(Tm, INPUT_PULLUP);
  pinMode(St, INPUT_PULLUP);
  lcd.begin(20, 4);         // init lcd 20 x 4
  lcd.backlight();          // backlight on
  // init LCD
  lcd.setCursor(0, 0);
  lcd.print("Environment sensors ");

  if (!AQ.begin()) {
    Serial.println("AQ E");
    while (1) {}
  }
  if ( !AQ.start(CCS811_MODE_1SEC) ) {
    Serial.println("Am E");
    while (1) {}
  }
  delay(100);  // enough time for BME280
  if (BME.begin() == false) {
    Serial.println("BME280 err");
    while (1) {}
  }
  delay(2000);
  lcd.clear();
}

void loop() { // ****** Arduino Loop *****
  // calendar and time display
  getTime();
  lcd.setCursor(0, 0);
  lcd.print(strYMDHMS);

  // weather display
  float tempC = BME.temp();
  float humidRh = BME.hum();
  float presPa = BME.pres();
#define tAdj -2.18
  int tC = round(tempC + tAdj);
  int hP = round(humidRh);
  int phPa = round(presPa / 100.0);
  lcd.setCursor(3, 1);
  lcd.print(tC);
  lcd.print("C ");
  lcd.print(hP);
  lcd.print("% ");
  lcd.print(phPa);
  lcd.print("hPa   ");

    // *** measure air quality and display it
  uint16_t errSt, raw;
  AQ.read(&CO2, &TVOC, &errSt, &raw);
  if ( errSt == CCS811_ERRSTAT_OK ) {
    Serial.print(CO2); Serial.print(","); // for serial plotter
    Serial.println(TVOC * 10);            // for serial plotter
  } else if ( errSt == CCS811_ERRSTAT_OK_NODATA ) {
    CO2 = 9000; TVOC = 0;                // to show waiting-data
  } else if ( errSt & CCS811_ERRSTAT_I2CFAIL ) {
    CO2 = 9100; TVOC = 0;                // to show I2C-error
  } else {
    CO2 = 9200 + errSt; TVOC = 0; // show error status
  }
  
  lcd.setCursor(3, 2);
  lcd.print("CO2 = ");
  lcd.print(CO2);
  if (CO2<9000){
    lcd.print(" PPM  ");
    lcd.setCursor(3, 3);
    lcd.print("TVOC= ");
    lcd.print(TVOC);
    lcd.print(" PPB  ");
  }else{
    lcd.print(" Error "); 
  }
 
  // *** output for Serial-plotter
  if (CO2 > 300) {                // wait until stabilized
    Serial.print((tempC+tAdj) * 10,0);   // degree-C x 10
    Serial.print(" ");
    Serial.print(humidRh * 10,0); // relative humidity x 10
    Serial.print(" ");
    Serial.print(phPa);           // pressure hPa
    Serial.print(" ");
    Serial.print(CO2);            // CO2 ppm
    Serial.print(" ");
    Serial.println(TVOC*10);      // TVOC ppb x 10
  }

  // delay 1sec, checking Set button every 50mS
  for (int i = 0; i < 20; i++) {
    if (digitalRead(St) == LOW) { // if St button, update clk
      rtcSet();
    }
    delay(50);
  }
}
/**************************************
      User defined functions
* *********************************** */
// *** get calendar & time into strYMDHMS
void getTime(void) {
  rRTC();      // read RTC device
  cR2I();      // convert RTC-format to Integers
  sprintf(strYMDHMS, "20%02d/%02d/%02d %02d:%02d:%02d", vI[mdY], vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
}

// *** read Real-Time-Clock ***
void rRTC(void) {
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC_addr, 7);
  while (Wire.available() < 7) {}   // wait for data ready
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
}

// *** convert RTC-format to Integers ***
void cR2I(void) {
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}

// *** RTC update-mode when set button pushed ***
void rtcSet(void) {
  while (digitalRead(St) == LOW) { // wait for button released
  }
  lcd.clear();
  byte mC[8] = {0x00, 's', 'm', 'h', 'W', 'D', 'M', 'Y' };  // mode characters
  char Su[3];         // figures used to set RTC value
  int maxvI[8] = { 0, 59, 59, 23, 7, 31, 12, 99};
  int minvI[8] = {0, 0, 0, 0, 0, 1, 1, 0};
  while (digitalRead(Md) == LOW) {};  // wait until Md released
  int mD = mdY ;              // initial mode=mdY
  rRTC();                     // read RTC device
  cR2I();                   // convert RTC-format to Integers
  // loop waiting for Set button
  while (digitalRead(St) == HIGH) {
    Data[0] = mC[mD];
    Data[1] = '=';
    sprintf(Su, "%02d", vI[mD]);
    Data[2] = Su[0];
    Data[3] = Su[1];
    //    set.curseor and display Data to LCD
    lcd.setCursor(8, 1);
    lcd.print(Data);
    // Mode button loop within Set button loop
    while (digitalRead(Md) == HIGH) {   // do until Md pushed
      if (digitalRead(Tm) == LOW) {     // if Time button pushed
        vI[mD] = vI[mD] + 1;            // increase value
        if (vI[mD] == maxvI[mD] + 1) {  // if exceeded max
          vI[mD] = minvI[mD];           // then set minimum & go
        }
        sprintf(Su, "%02d", vI[mD]);  // get vI[mode] to Su
        Data[2] = Su[0];
        Data[3] = Su[1];
        lcd.setCursor(8, 1);
        lcd.print(Data);

        delay(250);
        // if Set switch on, go out of Time loop
        if (digitalRead(St) == LOW) break;
      } // repeat TM loop
      // if Set witch on, go out of Mode loop
      if (digitalRead(St) == LOW) break;
      // else repeat Mode button loop
    }
    // out of Mode loop within Set loop
    if (digitalRead(Md) == LOW) delay(300);
    if (digitalRead(Md) == LOW) delay(500);
    if (digitalRead(Md) == LOW) {     // if still on
      pushMd = longPush;              // it it long-push
      break;                          // go out of set loop
    }
    if (digitalRead(St) == LOW) break;  // if Set pushed, go out
    mD = mD - 1;
    if (mD == mdI) {
      mD = mdY;
    }
  }  // out of Set loop
  if (pushMd == longPush) {     // if Md long-push
    pushMd = shortPush;       // reset indicator
    lcd.setCursor(8, 1);
    lcd.print("    ");
    return;                   // and exit from update-mode
  }
  while (digitalRead(St) == LOW) {}   // else, update at St-end
  // write RTC
  vR[mds] = (vI[mds] / 10) * 16 + vI[mds] % 10;
  vR[mdm] = (vI[mdm] / 10) * 16 + vI[mdm] % 10;
  vR[mdh] = (vI[mdh] / 20) * 32 + ((vI[mdh] % 20) / 10) * 16 + vI[mdh] % 10;
  vR[mdW] = vI[mdW];
  vR[mdD] = (vI[mdD] / 10) * 16 + vI[mdD] % 10;
  vR[mdM] = (vI[mdM] / 10) * 16 + vI[mdM] % 10;
  vR[mdY] = (vI[mdY] / 10) * 16 + vI[mdY] % 10;
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  for (int i = 1; i < 9; i++) {
    Wire.write(vR[i]);
  }
  Wire.endTransmission();
  // show that update has been done
  lcd.setCursor(8, 1);
  lcd.print("Done");
  delay(1000);
  lcd.clear();
}

/* End of program */

 次がロガーのスケッチです。

/* *******************************************************
   CO2 Logger V02   (c)2020.06.20-6.25 Akira Tominaga
   Function:
    Measure CO2 and TVOC using CCS811, with RTC & TM1637.
   Revisions:
    V00: Original devloped on June 20, 2020 
    V01: Print error status to serial printer
    V02: Show error status as 9xee on display
 *  ******************************************************/
#include "Wire.h"           // I2C for CCS811 and RTC-DS3231 
#include "ccs811.h"         // CCS811 Air quality (CO2) sensor
#include "SPI.h"            // SPI for SD drive interface
#include "SD.h"             // Micro SD drive
#include "TM1637Display.h"  // TM1637 4dig LED Display
// *** for Air quality sensor CCS811
#define AQadr 0x5A          // AQ sensor adr (0x5B alternative)
CCS811 AQ;                  // AQ as air quality sensor symbol
uint16_t CO2;               // CO2 ppm(400-8192), or AQ err_status
uint16_t TVOC;              // TVOC ppb(0-1187)
char strCT[10];             // editing area for CO2 and TVOC
// *** for micro SD writer
File aqLog;                 // aqLog as SD file symbol 
#define Cs  10              // SD Chip-select pin is D10
//  MOSI=11, MISO=12, CLK=13
#define SDsw 2              // SD-file closing button (L=on)
// *** for TM1637 display
#define TMdio 6             // DIO for TM1637
#define TMclk 7             // CLK for TM1637
TM1637Display disp(TMclk, TMdio); // disp as TM1637 symbol
uint8_t Data[] = { 0, 0, 0, 0 }; // data area for TM1637
// *** for Real Time Clock DS3231
byte  vR[8];                // values in RTC registers
int vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // integers for RTC data
#define RTC_addr 0x68
#define mdI 0               // Initial mode
#define mds 1               // ss
#define mdm 2               // mm
#define mdh 3               // hh
#define mdW 4               // WW=Day of Week
#define mdD 5               // DD
#define mdM 6               // MM
#define mdY 7               // YY
#define sS 0
#define mM 1
#define hH 2
char  strMDhm[11];          // edit area for calendar and clock
uint8_t mmSv;               // minute-llvalue save area

void setup() { // ***** Arduino setup *****
  Serial.begin(9600);
  Wire.begin();
  pinMode(SDsw, INPUT_PULLUP);
  disp.setBrightness(0x0A);
  if (!SD.begin(Cs)) {
    Serial.println("SD E");
    while (1) {}
  }
  if (!AQ.begin()) {
    Serial.println("AQ E");
    while (1) {}
  }
  if ( !AQ.start(CCS811_MODE_1SEC) ) {
    Serial.println("Am E");
    while (1) {}
  }
  // set SD file name MMDDhhmm
  getTime();
  String s = strMDhm;
  s.concat(".txt");           // complete file name
  aqLog = SD.open(s, FILE_WRITE);
   aqLog.println("MMDDhhmm,CO2m,VOCb"); // write column hdr
  mmSv = vI[mdm];             // save current minute value
  edDisp();                   // edit Data and display (0000)
  delay(2000);                // enough time for AQ
}

void loop() { // ****** Arduino Loop *****
  getTime();
  // *** measure air quality and display it
  uint16_t errSt, raw;
  AQ.read(&CO2, &TVOC, &errSt, &raw);
  if ( errSt == CCS811_ERRSTAT_OK ) {
    Serial.print(CO2); Serial.print(","); // for serial plotter
    Serial.println(TVOC * 10);            // for serial plotter
  } else if ( errSt == CCS811_ERRSTAT_OK_NODATA ) {
    CO2 = 9000; TVOC = 0;                // to show waiting-data
  } else if ( errSt & CCS811_ERRSTAT_I2CFAIL ) {
    CO2 = 9100; TVOC = 0;                // to show I2C-error
  } else {
    CO2 = 9200 + errSt; TVOC = 0; // show error status
  }
  edDisp();                         // edit Data and display

  // *** Write SD data every minute
  if (vI[mdm] != mmSv) {            // if minute changed
    mmSv = vI[mdm];                 // save new minute
    String s = strMDhm;             // set date-time
    s.concat(",") ;                 // set comma
    s.concat(strCT);                // set data
    aqLog.println(s);               // write a record
    //Serial.println(s);            // this is for debug only
  }
  // 1000mS delay, with checking SD-close-button every 100mS
  for (int i = 0; i < 10; i++) {
    if (digitalRead(SDsw) == LOW) { // if SD sw to close
      aqLog.close();
      disp.clear();
      while (1) {}
    }
    delay(100);
  }
}

/**************************************
      User defined functions
* *********************************** */
// *** Edit and display values
void edDisp(void) {
  // edit CO2 and TVOC value into strCT
  sprintf(strCT, "%04d,%04d", CO2, TVOC);
  // display CO2 value via Data
  for (int i = 0; i < 4; i++) {
    Data[i] = disp.encodeDigit(strCT[i]);
  }
  disp.setSegments(Data);
}

// *** get MMDD & hhmm into strMDhm ***
void getTime(void) {
  rRTC();      // read RTC device
  cR2I();      // convert RTC-format to Integers
  sprintf(strMDhm, "%02d%02d%02d%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
}

// *** read Real-Time-Clock ***
void rRTC(void) {
  Wire.beginTransmission(RTC_addr);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(RTC_addr, 7);
  while (Wire.available() < 7) {}   // wait for data ready
  for (int i = 1; i < 8; i++) {
    vR[i] = Wire.read();
  }
  Wire.endTransmission();
}

// *** convert RTC-format to Integers ***
void cR2I(void) {
  vI[mds] = ((vR[mds] & B01110000) / 16) * 10 + (vR[mds] & B00001111);
  vI[mdm] = ((vR[mdm] & B01110000) / 16) * 10 + (vR[mdm] & B00001111);
  vI[mdh] = ((vR[mdh] & B00100000) / 32) * 20 + ((vR[mdh] & B00010000) / 16) * 10 + (vR[mdh] & B00001111);
  vI[mdW] = vR[mdW];
  vI[mdD] = ((vR[mdD] & B01110000) / 16) * 10 + (vR[mdD] & B00001111);
  vI[mdM] = ((vR[mdM] & B00010000) / 16) * 10 + (vR[mdM] & B00001111);
  vI[mdY] = ((vR[mdY] & B11110000) / 16) * 10 + (vR[mdY] & B00001111);
}
/* End of program */

ロガーもシリアルプロッター信号を出すようにしてあります。息を吹きかけた様子はこれ(これは立ち上げ後10分ほどのRun-in時間中ではありますが)。横軸は秒数です。

f:id:a-tomi:20200628222554j:plain

では今回はこのへんで。

 

©2020 Akira Tominaga, All rights reserved.