勝手な電子工作・・

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

NDIR-CO2ロガーの製作と測定(CO2その3):換気は大事!

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

前にブレッドボードでの試作を紹介したNDIR(赤外線吸収型)CO2ロガーを組み立て、室内の測定等色々やってみたのでご紹介します。

前の記事はこれです:テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型 - 勝手な電子工作・・

今回のセンサー(MH-Z19b)は、改善の歴史の長いNDIR型です。さすがにキャリブレーションもしやすく安定し、ロガーの用途には使いやすいことがわかりました。

次はこのロガーによる実際の記録結果です。

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

測定開始から安定まで数分ですが一応開始後約10分間はカットしました。

ご覧のとおり、閉め切った部屋(広さ12平米)だと1人で作業しても、二酸化炭素濃度はみるみるあがっていきます。10時20分に2か所の窓を2センチずつあけて換気を開始すると素直に下がります。外の風向きなどで勾配は変化するようですが。

次に食事で不在の間、濃度は一気に下がっていきます(ただし廊下のドアは開けている)。自分がいかにCO2を出しているかがわかります^^; なにせ、戻ってくるとすぐまた上がります。13:45に同様の換気開始。その後14時28分に突然強い雨、窓を閉めざるを得なくなりました。するとまたぐんぐん上がります。15時22分に多少小降りとなり、窓を少し(2箇所15ミリほどずつ)開けて換気を再開。また少し下がります。

16時50分に雨が少し降りこむ側の窓を閉じ、空いている窓は1つで15mmだけ。閉まっている場合よりは勾配が緩やかですが、また徐々に上昇。結局18時55分に退室時は1000ppm寸前になっていました。しかし人がいなくなればどんどん下がります(廊下のドアを開けておきましたが)。

このセンサーは安いのになかなか大したものですね。

蛇足ですが、調べるとどのセンサーも長時間使えばドリフトが生じます。そこを工夫してたいていは自動キャリブレーション(Automated Baseline Calibration)があります。その場合、24時間、常に人がいる部屋では不都合であり、オフィスのように人がいない時間が毎日ある場所での測定に適しています。

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

このセンサーは3通りの測定法がありますが、前に書いたように、UART接続でデジタル数値を取り出して使用しています。

ロガーのケースには2箇所穴をあけて、センサーの空気の出入りができるようにしました。

このセンサーの現バージョンでは、コマンド(9バイトからなるセンサー宛信号)が5種類あります。①0x86(Co2濃度読取)、②0x87(ゼロ点校正)、③0x88(スパン点校正)、④0x79(自動校正設定/解除)、⑤0x99(測定レンジ設定)。

今出荷されるものは、⑤は既定、④はでデフォールトで設定済なので通常必要なコマンドは①のみです。また、②のゼロ点校正はコマンドからではなく物理的な回路設定(HgピンをLOWに設定)でもできます。具体的にはCO2が400ppm以下の環境に20分以上置いてHgを7秒間接地。

400ppm以下の環境は、植物の多いところなら日照の良い日の午後に確実に得られます(逆に、植物の少ないところや光合成が行われない暗さでは無理)。

以上はデータシート(User Manual)に記述されており、センサーはそのように動きます。ところが、念のためにArduinoのMHZ19ライブラリーを使って、Setupからのコマンドをトレースしたところ、データシートと異なり次のプロトコルになっています(Line1はマイコンからこのセンサーへ、Line2はセンサーからマイコンへの信号)。

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

これにはとまどいましたが調べていくと、単に、最新Arduinoライブラリーがこのセンサーの改善(バージョンアップ)に追い付いていないことがわかりました。

そして古い0x85や0xA2コマンドも互換性を維持するためにサポートされているものです。この際はライブラリーを使わずに躊躇なく^^;最新の仕様で手作りしたわけです。

なお、上に示したようなUART接続の信号トレースはArduino Unoで簡単にできます。必要な方は次の記事の最後の方をご参照ください。これを行ったArduinoスケッチをつけてあります:

ソフトウェアシリアルをコンパクトに作るーArduino編-そしてトレースに使う - 勝手な電子工作・・

 

以下に製作について、手短かに説明させていただきます。

まずは、あらためて回路図です。汚い手書きで毎度すみませんが。

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

今回はRTC(リアルタイムクロック、DS3231ブレークアウトモジュール)の更新機能はつけてありませんで、読み取るだけです。RTCは部品として簡単に抜き差しできるようにして、事前に「RTCライター」で時刻を設定すれば、少なくとも3年間は合わせなくても分単位ではずれません。今やDS323xはとても正確ですし電池は長期間持ちますが、電池交換の時や、もし時刻がずれた時だけ外して設定すればよいわけです。RTCライターについては、次の記事をご参照ください。

リアルタイムクロック書込器(ソースコード付き) - 勝手な電子工作・・

 RTC writer(リアルタイムクロック・ライター) 8ピンPICで簡素に - 勝手な電子工作・・

この際、回路不要のRTCライターを作りますので、この記事の中に追記しておきます。部品なにもなしでArduino-UNOだけでできる一番簡単なRTCライターです。今書きましたのでリンクを入れますね(2020.8.16更新)。

回路不要のかんたんなRTCライター:Arduinoで - 勝手な電子工作・・

なお、ArduinoでRTCライターを作るときにはたいていスイッチを3つつけて、RTC更新機能を組み込むようにしていますが今回のCO2ロガーではマイコンの容量を増やしたくないのでつけてないだけです。つけている一例は次。

デュアルGMTレコーダー - 勝手な電子工作・・

そして次が基板のデザインで、108mmx78mmです。部品面から見た図なので銅箔面にするときは裏返し。この記事の後の方に書くケースに合わせてあります。(また、銅箔を残さないところを黒にしているので、もしエッチングする場合はネガポジを反転。)

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

私はこういう片面基板はCNCルーターで削って作るので、ソフトで銅箔面のツールパス図に変換します。

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

作った基板は次です。今回は初めて使う中国製の頼りないA4ベーク樹脂生基板でしたが、幸い問題なくできました。

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

まず、ジャンパー線5つを配線。

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

そして部品をはんだ付けして基板は完成。

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

半田付けはきたないですが^^ この基板はアルコールで拭いてもハンダが結構つきにくいので、コテの温度を少し上げました。

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

これでテストをして問題なし。そしてケースに組み込んでできあがり。ケースは秋月で買ったポリカーボネート製。深さに3種類ありますがこれは中間の深さのもの。とても丈夫で蓋が開くので、こういう用途に重宝です。

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

センサーの2つの通気口が右端と上の蓋に近くなるようにデザインしました。それぞれの近くに5φの丸穴をあけてあります。蓋を閉めてのテストの結果、これで問題ありません。

最後に、ArduinoUnoのスケッチをつけておきます。UNOにIDEでプログラムし、ATmega328pマイコンだけを取り出して単体で使います。

/*
   CO2 Logger with MH-Z19b NDIR CO2 sensor (May-2020),
   with RYO coding for RTC, TM1637, MHZ19, and Soft-Serial
        Initial version V.00 Aug.1, 2020 (c) Akira Tominaga
        V.01: FileName=.csv. Replaced pins 8,9. Aug.10,2020
*/
#include "Wire.h"             // I2C for RTC-DS3231 
#include "SPI.h"              // SPI for SD drive interface
#include "SD.h"               // Micro SD drive
// *** 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 4                // button to close SD file
// *** for TM1637 display ***
#define lDIO 6                // DIO for TM1637 LED
#define lCLK 7                // CLK for TM1637 LED
#define lBrt 0x02             // duty-r 1/16x 02:4,03:10,05:12,07:14
#define lTu 50                // time unit in micro-second
byte lChr;                    // single byte sent to LED
byte Data[] = { 0x00, 0x00, 0x00, 0x00 }; // LED initial value 0000
// *** UART for CO2 sensor ***
#define sRx 9                 // Master-Rx (MISO) = MHZ19-Tx
#define sTx 8                 // Master-Tx (MOSI) = MHZ19-Rx
// (for sending)
#define sTuS 104-3            // time unit (9600bps) for sending
#define sTuhS 51              // time unit half for sending                  
char sByte;                   // character work area to be sent
// (for receiving)
#define tmChk 16              // timing checker for test (D16=A2)
#define sTuR 104-10           // time unit (9600bps) for receive
#define sTuhR 51-21           // time unit / 2 for receive
byte rByte;                   // character receiving byte
// *** for NDIR CO2 sensor MH-Z19b(May,2020)
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//                flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
// byte sABCcmd[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x86};
// //               flags, req, rABC,  0,    0,    0,    0,    0,  chksum
// ABC(auto baseline calib)default, since Jul.2015, hence no setup required.
// Ref:datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte hV;                      // response byte2 high value
byte lV;                      // response byte3 low value
uint16_t CO2 = 600;           // CO2 ppm smoothed value, init. 600
char strCO2[5];               // edit. area for SD recds and display
// *** for Real Time Clock DS3231
byte  vR[8];                  // values in RTC registers
uint8_t vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0};  // RTC data, init 0000
#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);         // start hardware serial to PC
  // *** for RYO-SW-UART
  pinMode(sRx, INPUT_PULLUP); // soft Rx to be pulled-up
  pinMode(sTx, OUTPUT);       // soft Tx as output
  digitalWrite(sTx, HIGH);    // set it high
  pinMode(tmChk, OUTPUT);     // timing checker
  digitalWrite(tmChk, LOW);   // set it LOW
  // *** for RYO-TM1637 interface
  pinMode(lCLK, OUTPUT);      // lCLK as output
  pinMode(lDIO, OUTPUT);      // lDIO as output
  digitalWrite(lCLK, HIGH);   // lCLK high
  digitalWrite(lDIO, HIGH);   // then lDIO high to avoid Start sig
  lDispData();                // display 0000
  // *** for SD
  pinMode(SDsw, INPUT_PULLUP); // SD file closing button
  Wire.begin();
  if (!SD.begin(Cs)) {
    Data[0] = "E";            // show error
    lDispData;                // display E000
    while (1) {}
  }
  // set SD file name MMDDhhmm
  getTime();
  String s = strMDhm;
  s.concat(".csv");           // complete file name
  aqLog = SD.open(s, FILE_WRITE);
  aqLog.println("MMDDhhmm,CO2"); // write column hdr
  mmSv = vI[mdm];             // save current minute value
  // *** for MHZ19 -- no setup required for current MHZ19b
  // for (uint8_t j=0; j<9; j++){ // set-ABC-logic command
  //  sByte=sABCcmd[j];
  //  sSend();                    // and send it
  // }
  delay(1000);                 // enough time for MHZ19 readiness
}

void loop() { // ***** Arduino loop *****
  getTime();
  // *** send "Read CO2" command ***
  for (uint8_t j = 0; j < 9; j++) {
    sByte = rCO2cmd[j];
    sSend();
  }
  // *** get CO2 value ***
  sRcv();                     // byte0 is 0x86
  sRcv();                     // byte1 is Sensor number
  sRcv();                     // byte2 is CO2 value high
  hV = rByte;                 // set it to hV
  sRcv();                     // byte3 is CO2 value low
  lV = rByte;                 // set it to lV
  // for (uint8_t j = 0; j < 5; j++) { // read residual 5 bytes
  // sRcv();                         // *** can be ignored
  // }                                 // hence, test use only
  uint16_t mCO2 = hV * 256 + lV; // get CO2 PPM value
  CO2 = (float)(0.250 * mCO2 + 0.750 * CO2); // smoothed with exp.factor 0.25
  // plot bottom, ceiling and CO2 to PC via HW-serial
  int lowL = 400;
  int highL = 1000;
  Serial.print(lowL);
  Serial.print(",");
  Serial.print(CO2);
  Serial.print(",");
  Serial.println(highL);
  edDisp();                    // edit CO2 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(strCO2);          // set data
    aqLog.println(s);          // write a record
    //Serial.println(s);       // this is for debug only
  }
  // *** 2s delay, w/ SD-close-button checks every 100mS ***
  for (int i = 0; i < 20; i++) {
    if (digitalRead(SDsw) == LOW) { // if SD sw to close
      aqLog.close();
      // clear display and stop
      for (int j = 0; j < 4; j++) {
        Data[j] = 0x10;
      }
      lDispData();
      while (1) {}
    }
    delay(100);
  }
}

/**************************************
      User defined functions
* *********************************** */
// *** edDisp() *** Edit and display values
void edDisp(void) {
  sprintf(strCO2, "%04d", CO2);
  // display CO2 value via Data
  for (int i = 0; i < 4; i++) {
    Data[i] = strCO2[i] & 0x0F;
  }
  lDispData();
}

// *** lDispData() *** display data to TM1637 4digits
void lDispData(void) {
#define showDat 0x40          // show this is data
#define showAd0 0xC0          // LED register addr is zero
#define showDcB 0x88+lBrt     // show dCtl + brightness
  lStart();                   // start signal
  lChr = showDat;             // identifier for data
  lSend();                    // send it
  lStop();                    // stop signal
  lStart();                   // and restart
  lChr = showAd0;             // identifier for address
  lSend();                    // send it
  for (int j = 0; j < 4; j++) { // for Data[0] to Data[3]
    byte edChr = Data[j];     // set a byte to edChr for edit
    switch (edChr) {
      case 0x00: lChr = 0x3F; break; // 0
      case 0x01: lChr = 0x06; break; // 1
      case 0x02: lChr = 0x5B; break; // 2
      case 0x03: lChr = 0x4F; break; // 3
      case 0x04: lChr = 0x66; break; // 4
      case 0x05: lChr = 0x6D; break; // 5
      case 0x06: lChr = 0x7D; break; // 6
      case 0x07: lChr = 0x07; break; // 7
      case 0x08: lChr = 0x7F; break; // 8
      case 0x09: lChr = 0x6F; break; // 9
      case 0x10: lChr = 0x00; break; // blank
      default:   lChr = 0x79;        // E for error
    }
    lSend();                  // send each byte continuously
  }                           // end of four bytes
  lStop();                    // stop signal
  lStart();                   // restart
  lChr = showDcB;             // identifier for display brightness
  lSend();                    // send it
  lStop();                    // stop signal
}

// *** lSend() *** send a charater in lChr to TM1637
void lSend(void) {
#define LSb B00000001           // Least Significant bit
  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((lChr & LSb) == LSb) {  // if one then
      digitalWrite(lDIO, HIGH); // set lDIO high
    } else {                    // else
      digitalWrite(lDIO, LOW);  // set lDIO low
    }
    lCLKon();                   // clock on to show lDIO
    lChr = lChr >> 1;           // shift bits to right
  }
  digitalWrite(lDIO, LOW);      // pull down during Ack
  lCLKon();                     // clock on to simulate reading
}

// *** lStart() *** send Start signal to TM1637
void lStart(void) {
  digitalWrite(lDIO, LOW);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** lStop() *** send stoP signal to TM1637
void lStop(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lDIO, HIGH);
  delayMicroseconds(lTu);
}

// *** lCLKon() ** TM1637 clock on to show data to TM1637
void lCLKon(void) {
  digitalWrite(lCLK, HIGH);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

// *** getTime() *** get RTC 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]);
}

// *** rRTC() *** 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();
}

// *** cR2I() *** 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);
}

// *** sSend() *** send a charater in sByte via SW UART
void sSend(void) {
#define LSb B00000001         // Least Significant bit
  digitalWrite(sTx, LOW);
  delayMicroseconds(sTuS);

  for (int i = 0; i < 8; i++) { // do the following for 8bits
    if ((sByte & LSb) == LSb) { // if one then
      digitalWrite(sTx, HIGH); // set sTx high
    } else {                  // else
      digitalWrite(sTx, LOW); // set sTx low
    }
    delayMicroseconds(sTuS);  // take required time
    sByte = sByte >> 1;       // shift a bit to right
  }

  digitalWrite(sTx,  HIGH);   // set high as a stoP bit
  delayMicroseconds(sTuS);    // consume time-unit
}

// *** sRcv() *** receive a charater in rByte, via SW UART
void sRcv(void) {
#define MSb B10000000          // Most-Significant-bit
#define nMSb B01111111         // not Most-Significant-bit
  while (digitalRead(sRx) == HIGH) {} // wait for start bit
  delayMicroseconds(sTuR + sTuhR); // skip start-bit + sampling-timing
  rByte = 0x00;   // ** for debug
  for (uint8_t i = 0; i < 8; i++) { // do the following for 8bits
    digitalWrite(tmChk, HIGH); // sampling-time-checker on
    digitalWrite(tmChk, LOW);  // sampling-time-checker off
    if (digitalRead(sRx) == HIGH) { // if sRx high, then
      rByte = rByte | MSb;     // set MSb on
    } else {                   // else
      rByte = rByte & nMSb;    // set MSb zero
    }
    if (i < 7) rByte = rByte >> 1; // shift rByte excluding last i
    delayMicroseconds(sTuR);
  }
  while (digitalRead(sRx) == LOW) {}; // wait until HIGH (stop bit)
}

/* End of program */

 

このスケッチはUNOの容量の50%台に収まり、余裕があります。

 

後半少し急いで書いたのでよみにくいかもしれませんが、ご容赦ください。では今回はこのへんで。

 

©2020 Akira Tominaga, All rights reserved.