勝手な電子工作・・

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

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.

 

 

 

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

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

シリアル通信を小さく作る例をArduinoの例で説明したいと思います。少し複雑な工作でも、ESPなどを持ち出さずにUNO(ATmega328P)などありきたりのマイコンで作る「生活の知恵」でもありますし、シリアル通信のアナライザーを作る知恵でもあります。この前後の記事でご紹介する二酸化炭素濃度ロガーなど、通信種類が多い場合などに役立ちます。

上の写真は左がArduino-UNO、右がコンパチ機、間に信号を観察するための小さなブレッドボードを挟んでいます。

そもそもIDE環境がArduinoとPCの接続にハードウェア・シリアル通信を使うため、それが1つしかないUNOの場合には、2つ目が必要なら「ソフトウェアシリアル」を使います。ところがその標準ライブラリーは資源を意外に多く消費します。そこで必要な、小容量で複数つなぐ方法としてここで説明したいと思います。

ご紹介する手作りソフトの受信ロジックは、デバイス間のシリアル通信を外部システムとして勝手に受信(つまりトレースやブロードキャスト通信も)できます。そのスケッチ例は、この記事の最後のほうにつけておきます(2020.8.13追加)。

雑談ですが、シリアル通信はUART通信、非同期通信、スタートストップ通信、調歩式、等々色んな名称がありますかね。物理的なリンクだけ見れば各デバイスの時計に頼り、同期をとらずにビットをやりとりします。現代から見れば乱暴^^;  世界でオンラインシステムが始まった時から使われ、日本では1964年東京オリンピックがその通信システムの始まり。同期式が使われだしたのはだいぶ後です。

それがマイコンで今でも広く使われているのが不思議ですが、デバイスは今やどれも正確なクォーツクを使い問題はほぼないわけです。フィリップスI2C通信の絶妙アーキテクチャには程遠いですが、単純な2線で便利です。次の写真のマイクロSDリーダーライターは、シリアル通信で動かす日本製モジュールで、小さなマイコンでもSDカード処理がらくちんでできてしまいます。

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

2011年に放射能測定器を色々作りましたが、簡単なインターフェイスでロギングするのに随分重宝です。高価でしたが。今も時々使いますが、この製造元である塩尻市の(有)Suntechさんが2020年4月末で営業を終了されたのは残念。あ、雑談はここまで。つまりシリアル接続は単純なのでポピュラーに使われているわけです。

 

まずArduino標準ライブラリーのソフトウェアシリアル通信を使うサンプルスケッチを載せます。最初に送る側(仮にMasterと呼びます)です。ここではハードウェアシリアルでPCのキーボードから受けた文字を、そのままソフトウェアシリアルで送信します。

/* Software serial - exapmle 
 * Sender (Master) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // Master sRx (MISO) = Slave sTx
#define sTx 8                     // Master sTx (MOSI) = Slave sRx
#define sSbaud 9600               // SW-serial baud rate
SoftwareSerial sS(sRx, sTx);      // create SW-serial as symbol sS
String strTyped;                  // string from KB and to send to Slave
char rBt;                         // receiving char from software Serial

void setup() {  // ***** Arduino Setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial
  Serial.println("Master ready");
}

void loop() {  // ***** Arduino Loop *****
  while (Serial.available() < 1) {}
  strTyped = Serial.readStringUntil(0x10); // read KB until DLE
  uint8_t strL = strTyped.length(); // get length
  for (uint8_t i = 0; i < strL; i++) {
    sS.write(strTyped.charAt(i)); // send each byte
  }
  Serial.print(strTyped);         // show what was sent
  Serial.println("was sent by Master");
}
/* *** End of sketch *** */

 次に、受ける側の標準ソフトウェアシリアルによるスケッチです。仮にSlaveと呼びますが、受けた文字をそのまま印字するようにハードウェアシリアルに出します。

/* Software serial - exapmle 
 * Receiver (Slave) V.00 July 2020 (c)Akira Tominaga
*/
#include "SoftwareSerial.h"
#define sRx 7                     // sS-Slave Rx (MOSI) = sS Master Tx
#define sTx 8                     // sS-Slave Tx (MISO) = sS Master Rx
char rBt;                         // receiving char from software Serial
#define sSbaud 9600               // SW-serial baud ratea
SoftwareSerial sS(sRx, sTx);      // create SW-serial sS as symbol

void setup() { // ***** Arduino setup *****
  Serial.begin(sSbaud);           // start hardware serial
  sS.begin(sSbaud);               // start SW-serial sS
  Serial.println("Receiver ready");
}

void loop() {  // ***** Arduino Loop *****
  if (sS.available()) {
    rBt = sS.read();
    Serial.print(rBt);
    if (rBt == 0x0A) {
      Serial.println("was received by Slave");
    }
  }
}
/* *** End of Sketch *** */

この2つのペアによる送受をオシロで観察しても、中身が分り難いのが非同期通信の特徴。手がかりのクロック信号がないですからね。次の写真で下がMasterから出たもの、上がSlaveから返したもの(2020.8.6追加記述:失礼、上に示したレシーバーのソースに1行追加して送り返しています)。スタートビットはH(シリアル通信ではHをスペースという)からL(マーク)になるところを指し、ストップビットは最後に信号線をH(スペース)に戻すだけです。その間に8ビットの列があり、ビットの順番は普通のバイトとは逆に(つまりLSb=Least Significant bit から)送られます。コンピュータによってメモリー内への収容が逆順の機種(Little Endian)も多いのでまあ一理あるわけですが。

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

オシロではなく、ロジックアナライザーなら少しわかりやすいですが、時間計算する以外にタイミングの手がかりが少ないもの。そこで、勝手な電子工作の知恵では、分析のためにはU(0x55)という文字を含めるようにします。Uならオンオフのビットが交互になりますからタイミングが確実にわかりますね。次はUAUと打ち込み送信をロジックアナライザーで見たもの。

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

Sはスタートビットで、Pはストップビット。間のビットの並びを前後逆転すれば0x55、0x41、0x55、0x0Aと簡単に読めます。送信側ではUSBキーボードを打ち込んでハードウェアシリアルに渡されたものをDLE記号が来る前までそのままを送っていますから、最後に0x0AのLF(改行つまりエンターキー)がくっついてます。

ロジックアナライザーは次を使っていますが、今どきは中国製のコンパチモデルというよりも、形状を変えたコピー機が超廉価でずいぶん出回ってるようです。

 

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

 

 ロジックアナライザーで使わないチャネルのプローブ先端はグランドに落としますから、次のようなアナライザー専用のブレッドボードを常備しておくと測定が手短かに行え、かつ、刺し間違いも減ります。

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

これは普通のブレッドボードの一部を強引に切り取ってヤスリをかけただけ。小さくて必要な穴が十分なので、場所を取らず迅速に使え、さしっぱなしで保管できます。

 

とはいえ、シリアル通信の信号解析が圧倒的に楽な方法は、プログラムで分析することです。さきほど紹介した受信側のロジックを使えば簡単です。

 

標準のソフトウェアシリアルはUNOの容量の1割近くを占拠するので、一般的な工作で小マイコンに組み込むために手で書く例(RYO=Roll-Your-Ownプログラム)が次です。

まず送る側。Uの文字を含んで測定しながら定数(sTuS)を設定するだけでよいものです。

/*  Roll-Your-Own software-serial-communication; Sender
 *  Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define sRx 7                     // Sender sRx (MISO) = Receiver sTx
#define sTx 8                     // Sender sTx (MOSI) = Receiver sRx
#define sTuS 104-3                // time unit (9600bps) for sending
#define sTuhS 51                  // time unit half for sending                  
char tByte;                       // typed-in character
char sByte;                       // character work area to be sent

void setup() {  // ***** Arduino setlup ***** 
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx,INPUT_PULLUP);      // soft Rx pulled-up
  pinMode(sTx,OUTPUT);            // soft Tx as output
  digitalWrite(sTx,HIGH);         // set it HIGH
  Serial.println("Snd ready");
}

void loop() {  // ***** Arduino loop *****
  while (Serial.available() < 1) {} // wait until type-in
  tByte = Serial.read();          // read typed-in character
  sByte = tByte;                  // set it into work byte to be sent
  sSend();                        // send it via software serial
  Serial.print(tByte);            // print char sent(unchanged byte)
  delayMicroseconds(sTuS*3);        // *** for receiver printing time
}

/**************************************
      User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
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+sTuhS);  // consume time-unit x 1.5
}

/* End of program */

そして次が受ける側のRYOプログラム。こちらはタイミング調整の勝手な生活の知恵として、読取タイミングをどこかのピン(tmChk)に出力します。定数(sTuR)の値を減らしながら、検査すべきビットの真ん中に読取タイミングが来るようにすれば、それだけで以後はもう大丈夫です。

/*  Roll-Your-Own software-serial-communication Receiver
    Sample sketch V.00  July 31, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for measurement
#define sRx 7                     // Receiver sRx (MOSI) = Sender sTx
#define sTx 8                     // Receiver sTx (MISO) = Sender sRx
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(sRx, INPUT_PULLUP);     // soft Rx 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
  Serial.println();               // LF for repeated testing
  Serial.println("Rcv ready");
}

void loop() {  // ***** Arduino loop *****
  sRcv();                         // receive data into rByte
  Serial.print(rByte, HEX);
  Serial.print(" ");              // insert blank as a separator
}

/**************************************
      User defined functions
* *********************************** */
// *** sRcv *** receive a charater in rByte
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 */

 受信のタイミング信号を含めて記録したものは次です。

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

送る側のRYOプログラムを標準ソフトウェアシリアルの受信側で受けると同じ動きをしますし、逆の組みあわせ、つまり標準ソフトウェアシリアルの送り側を今回のRYOの受け側を組み合わせても、同じ動きをします。つまり他の相手ともつながるということが分ります。

ついでにビットの解析を書けば次のようになります。

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

以上のように、必要部分だけを勝手なRYOプログラムにすると、Arduinoの容量はずいぶん節約されて、UNOでは通常は難しいことも、余裕でできてしまいます。1例としては次の記事などをご参照ください。

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

 

資源の減らすだけでなく、このようなRYOプログラムなら通信のトレースも簡単にできます。

次の例は、マイコンとデバイスの間のシリアル通信でのやりとりを、Arduinoのプログラムで外部から観察したものです。

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

 プログラムは分析相手に合うように適当に加工すればよいものですが、ここで用いたスケッチは次のものです。

/*  Roll-Your-Own software-serial-communication Receiver
    Initial version V.00  Aug. 12, 2020 (c) Akira Tominaga
*/
#define tmChk 6                   // timing checker for adjustment use
#define Line1 7                   // Line1 to analyze
#define Line2 8                   // Line2 to analyze
#define sTuR 104-10               // time unit (9600bps) for receive
#define sTuhR 51-21               // time unit / 2 for receive
byte rByte;                       // character receiving byte
#define numB  128                 // number of bytes to be analyzed
byte rB[numB * 2 + 5];            // analyzed data accumulation area
uint16_t rBi = 0;                 // accumulation area index

void setup() {  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial
  pinMode(tmChk, OUTPUT);         // timing checker as output
  digitalWrite(tmChk, LOW);       // set it LOW
  pinMode(Line1, INPUT_PULLUP);   // Line1 pulled-up
  pinMode(Line2, INPUT_PULLUP);   // Line1 pulled-up

  Serial.println("Analyzing...");
}

void loop() {  // ***** Arduino loop *****
  if (digitalRead(Line1) == LOW) sRcv1();
  if (digitalRead(Line2) == LOW) sRcv2();
  if (rBi > numB * 2) {
    byte Lnosv = 0x03; // set temp value (not 01 or 02)
    for (rBi = 0; rBi < numB * 2 - 1; rBi = rBi + 2) {
      byte Lno = rB[rBi];
      byte Dat = rB[rBi + 1];
      if (Lnosv == Lno) {
        Serial.print(" "); Serial.print(Dat, HEX);
      } else {
        Serial.println(); Serial.print("Line=");
        Serial.print(Lno, HEX); Serial.print(" ");
        Serial.print(Dat, HEX);
        Lnosv = Lno;
      }
    }
    while (1) {}
  }
}
/**************************************
      User defined functions
* *********************************** */
#define MSb B10000000             // Most-Significant-bit
#define nMSb B01111111            // not Most-Significant-bit

// *** sRcv1 *** receive Line1 charater in rByte
void sRcv1(void) {
  // while(digitalRead(Line1)==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(Line1) == HIGH) { // if Line1 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(Line1) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x01; rBi++;
  rB[rBi] = rByte; rBi++;
}

// *** sRcv2 *** receive Line2 charater in rByte
void sRcv2(void) {
  // while(digitalRead(Line2)==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(Line2) == HIGH) { // if Line1 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(Line2) == LOW) {}; // wait until HIGH (stop bit)
  rB[rBi] = 0x02; rBi++;
  rB[rBi] = rByte; rBi++;
}

/* End of program */

アナライザープログラムとしてのこの使い方は、ピン7、ピン8を調べたいシリアルラインに接続するだけです。もちろんグランドの接続は前提ですし、クロックスピードは相手に合わせられる範囲だけですが。

 

以上で簡単な説明を終わりたいと思いますが、他のマイコンでも似たようなものですね。ご質問いただいた方のお役に立てば幸いです。

 

©2010 Akira Tominaga, All rights reserved.

 

テレワークは換気に注意ーArduinoでCO2ロガー(その2):赤外線吸収型

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

テレワークで部屋に籠ることが多い場合は換気が必要です。実際に2酸化炭素が1000PPMを超えると眠くなったり飽きたり・・・。前々回作った測定装置でそうなるのを体感しました。

梅雨明けした暑い夏は、部屋にエアコンかけて閉じっぱなしにすると益々危険そう。今回は赤外線吸収型(NDIR=nondispersive infrared )のCO2センサーを使って室内の炭酸ガス濃度ロガーを作ってみました。

2酸化炭素が4260nmの赤外線を吸収するという原理を使う古典的な方法のセンサーですが、長年にわたって改良されてきたもので小型化や高性能化が色々行われています。上の写真は今回使うMH-Z19Bという安価なセンサーモジュールです。

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

前々回の記事に金属酸化物を使ったCCS811半導体センサーで作ったロガーを紹介しました。なぜ、新たにNDIRセンサーでロガーを作ってみるかと言えば、半導体センサー(動作の殆どは内蔵ソフトウェアの機能)は比較的新しいセンサーで鋭敏なのですが、長時間記録するロガーとするには結構手間のかかるキャリブレーション(Baseline registerの設定)が度々必要です。

ArduinoのCCS811標準ライブラリーでは、なんと2019年1月にその設定機能が追加されたばかりです。現在それを別プログラムで行っていますが、校正がしばしば必要なのは、扱いが結構手間です。2台作った測定値の間にはこれまで大きな狂いはないですが、正しい数値かどうかを別原理のロガーと比較してみたいという興味もあります。

NDIRセンサーは多くのメーカーから工夫された安定製品がでていますが、どれも比較的高価です。調べた限りは、長期間の使用では当然キャリブレーションを必要としますが、方法も色々あります。MH-Z19Bではシンプルな方法3つが提供されていますし、なによりも安価(海外ネットでは15ドル程ですから、CCS811の倍ぐらい)なので、これを2個入手して挑戦しました。

測定値のとりかたには次の3通りあります。

①シリアル(UART)通信でコマンドを送り、値をデジタルで直接得る

②PWMのDuty幅(アナログ値)を介して得る

③アナログ出力値を読み取る

まず一番簡単な③をやってみたら旨く測れるのですが、なんと!周期的に特定ノイズが載ってきます。2個とも同じ現象があり、配線の問題ではなくこのセンサーモジュール自体の性格のようです。これを無視するようにプログラムすればできそうですが、この際は③を使わないことにしました。ついでに②もけっこう面倒そうなので、一番確実な①でやってみることにしました。

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

試しに値をとりプロットすると、おお!長時間忠実にアウトプットされるではないですか!

上は測定開始1010秒(約17分)後から2610秒(約44分)後の間のグラフです。エアコンの風があたっている間は測定値がギザギザになることがわかりました。空気取り入れ口にフィルターがあるからそういう現象になるかも。風のあたらない場所で測るとスムーズです。しかし、風が当たる場所でもプログラムでスムージングすれば問題ない感じです。

このセンサーは約1秒強の間隔でデータが得られますが、ロガーとしては安全のために2秒間隔で測定し毎分1度記録するとします。たとえば測定値を指数平滑法を使って、測定値x0.25+前回保存値x0.75でスムージングするとした場合、仮に500ppmの急激な変化があっても、記録する1分(つまり測定29回)以内で追従できます(計算してみました)ので問題ないわけです。

さて、UART接続したArduinoスケッチは次のように短い超簡単なものです。

/* CO2 plotter example with NDIR MH-Z19b via serial interface  */
#include "MHZ19.h"
#include "SoftwareSerial.h"
#define Rx 8                      // Master Rx (MISO) = MHZ19 Tx
#define Tx 9                      // Master Tx (MOSI) = MHZ19 Rx
#define swSerB 9600               // MH-Z19 spec. serial baud rate
MHZ19 NDIR;                       // MH-Z19 symbol to be NDIR
SoftwareSerial swSer(Rx, Tx);     // (Uno example) create device to MH-Z19 serial
void setup(){ // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial to PC
  swSer.begin(swSerB);            // start software serial to MH-Z19
  delay(100);
  NDIR.begin(swSer);              // begin NDIR CO2 sensor MHZ19
  NDIR.autoCalibration();         // set auto caliration TRUE
}
void loop(){ // ***** Arduino loop *****
  int CO2 = NDIR.getCO2();        // get CO2 PPM
  int lL = 400;  int hL = 1000;   // set scale lines
  Serial.print(lL);  Serial.print(",");  Serial.print(CO2);
  Serial.print(",");  Serial.println(hL);
  delay(2000);                    // measure every 2 seconds
}
// *** end of sketch ***

 このスケッチをためして気づいた重要なことは、次の2つです。

①これにArduino標準ライブラリーを使うとマイコン容量をかなり消耗する。ロガーには他のデバイスも複数使用するため、そのやりかたではUNOではとても困難になる。

②MH-Z19というセンサーモジュールは4ピンと5ピンの列があるが、列間距離の仕様が異常。このままブレッドボードなどに刺すとモジュールを壊す恐れがある。

①についてはコンパイル結果が次のとおりで、グローバル変数で実に34%消耗!

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

 ②については、サイズの仕様が次のとおりです。

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

29.54mmという間隔には、ホントにまいってしまいます!端数にわざわざ0.54mmをつけたりして。30.48mmとか、27.94mmとかなら適切で納得できますが。・・・きっと過去に仕様をきめたときの誰かの間違いが長年ずっとひきずっているのでしょうかね。

とにかくこのままブレッドボードなどに刺してはだめ(データシートにはPCBに無理な力をかけないようにと注意書きがあります、ということはそういうことで壊す例が多い?)。とにかく気を付けましょう。

さて、標準ライブラリーが大きすぎるなら、UNOをあきらめて別のモデルを使う方法もありますが、UNOにこだわるなら手で書けばよいわけです。このセンサーとのやりとりも、ソフトウェアSerialも、複雑なものではありませんから。次のようにRYO(Roll-Your-Own)プログラムに書き換えれば問題なく動作をしますし、容量もあまり食いません。

/* CO2 (PPM) testing plotter with MH-Z19b NDIR sensor,
 * w/RYO serial-com. & RYO handling V00 Aug.2000 (c) Akira Tominaga */
#define sRx 8                     // Master Rx (MISO) = MHZ19 Tx
#define sTx 9                     // Master Tx (MOSI) = MHZ19 Rx
// for serial 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 serial 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 MH-Z19b, preset & auto-calib since July 2015, hence no setup required doday.
//  Ref:Datasheet v1.0 (by Zhengzhou Winsen Electronics Technology Co.,Ltd)
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//               flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
byte hV;                          // response byte2 high value
byte lV;                          // response byte3 low value
uint16_t CO2;                     // CO2=hV*256+lV

void setup(){  // ***** Arduino setup *****
  Serial.begin(9600);             // start hardware serial to PC
  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
}

void loop(){  // ***** Arduino loop *****
  // send "Read CO2" command
  for (uint8_t j=0; j<9; j++){
    sByte=rCO2cmd[j];
    sSend();
  }
  // receive CO2 value
    sRcv();                     // byte0 is ignored (0x86 response) 
    sRcv();                     // byte1 is ignored (sensor number)
    sRcv();                     // byte2 is CO2 value high
    hV=rByte;                   // set it to highValue
    sRcv();                     // byte3 is CO2 value low
    lV=rByte;                   // set it to lowValue
    for (uint8_t j=0; j<5; j++){ // read and ignore residual 5 bytes
      sRcv();
    }
    CO2=hV*256+lV;              // calculate CO2 PPM value
  // plot scales and CO2 via hardware serial to PC
  int lowL = 400;  int highL = 1000; // set scales
  Serial.print(lowL);   Serial.print(",");  Serial.print(CO2);
  Serial.print(",");  Serial.println(highL);
  delay(2000);                 // measure every 2 seconds
}

/**************************************
      User defined functions
* *********************************** */
// *** sSend *** send a charater in sByte
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 + sTuhS); // consume time-unit x 1.5
}

// *** sRcv *** receive a charater in rByte
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 */

標準ライブラリーではSetupでABC設定(Automatic Baseline calibration)コマンドを送りますが、こちらでは出しません。このデバイスの最近の仕様は出荷時にABC設定済で設定不要となっており、設定の必要性がありません。このスケッチで標準ライブラリーと同じく長時間安定した値が測定されます。

これならArduino Mega2560とかESPなどを持ち出さなくとも、Arduino UNO(ATmega328P)単体で余裕をもってできそうです。なお、タイミングが重要なシリアル通信などをどうやって簡単にRYOで作ったり観察したりするかの方法は、少し長くなりそうなので回を改めてご紹介したいと思います。

 ロガーの回路は次のようにすれば、5.0V供給だけですむので前回の半導体センサーよりだいぶ単純です。毎度汚い手書きですみませんが。じつはこれを考えてからプロッターでのテストをしてみて、ソフトのRYO化が必要なことに気づいたのですが。

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

次はブレッドボードで試作したロガーです。

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

今回のセンサーはまだキャリブレーションをしていませんが、数値は前回作ったロガー(下の写真の下側)とあまり大きくは違いません。キャリブレーションはこのセンサーに接続したスライドスイッチをオンに(接地)するだけでできるようにしました。CO2が400ppm以下の環境を必要とするので、緑の多いところで光合成の盛んな午後に行うのが妥当かと思います。(あるいはCO2ゼロで赤外線吸収のない窒素ガスかアルゴンガスの中でやればよいのですが、そういうのはなかなかね)。

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

 

今回のロガーのプログラムでは、比較的複雑なSD入出力、SPI通信、I2C通信に標準ライブラリーを使い、単純なRTC(リアルタイムクロック)、TM1637表示、このMH-Z19センサー処理、およびソフトウェアシリアル通信をRYOにすることで、十分小さく収まりました。そういうわけでプログラムの記述は少し長いですがArduinoスケッチは次のとおりです。

(2020.8.11 下のスケッチで2行を直接直しました。①回路図に合わせてピン8と9を逆にしました。②Setupの最後の待ち時間を1000ミリ秒に直しました。前の100ミリ秒の場合にはMH-Z19センサーがReadyになってないことがまれにありますので。最近作った基板とともに使い心地の比較等を含めて、今後別記事で最新プログラムとともに改めて紹介するつもりです。)

/*
   CO2 Logger with MH-Z19b NDIR sensor,
   with RYO coding for RTC, TM1637, MHZ19, and Soft-Serial
        Initial version V.00 Aug.1, 2020 (c) Akira Tominaga
*/
#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
byte rCO2cmd[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
//               flags, req, rCO2,  0,    0,    0,    0,    0,  chksum
// Auto-calib & preset since Jul.2015, hence no setup required as of doday.
// 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
int 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(".txt");           // complete file name
  aqLog = SD.open(s, FILE_WRITE);
  aqLog.println("MMDDhhmm,CO2"); // write column hdr
  mmSv = vI[mdm];             // save current minute value
  delay(1000);                // enough time for stability
}

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 for 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 + sTuhS); // consume time-unit x 1.5
}

// *** 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%台にしっかり収まっています。

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

測定値はいままでのところ長時間使っても安定しており、数値には換気がしっかり反映されています。とりあえず、めでたしめでたし^^

 

では今回はこの辺で。

 

©2010 Akira Tominaga, All rights reserved.

TM1637-LEDをライブラリーなしで簡単表示

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

4桁の7セグメントLEDを直接ダイナミック点灯する場合は、少なくとも11本以上のピンを使います。このTM1637というICを用いたディスプレイなら2ピンしか使わずにすみ、見やすく廉価。色々な機会にこれを使っていますが、使い方を質問されることが結構多い部品です。

外出自粛の週末ですが、おまけにすごい雨です。こういう機会に、ごく簡単ですがこれの要点と簡単なプログラムの例を書いておこうと思います。

次の写真は10年ほど前にPCの元電源オン表示の代わりとして、ちょこっと作ってみた温湿度計です。8ピンマイコンで作りましたが今でも明るく動いています。8ピンなのに温湿度センサーの他に、明るさ表示変更用のボタンなどもつけています。

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

TM1637というICはずいぶん前に出たものですが、最初の頃はとくにライブラリーもなく、中国語のデータシートしかなくて苦労しましたが、その後は文書類もLED用のライブラリーなども整ったので世界で普及したと思います。

第一に4桁LED+TM1637IC込みで値段が1ドル程度と圧倒的に安いし、Duty比が可変で電流消費も少なくできたり、接続やプロトコルがシンプルだったり。7セグメントが自由自在(これはあたりまえか^^;)。

とはいえ、デバイス1つ毎にIOピンを2つも(!?)使うので、本当はI2C接続のものが欲しかったわけですが、最近になってHT16K33+4桁LEDのモジュールが出回り始めたので早速注文中。単価はこの約3倍ほどします。そのうちに記事で紹介したいと思いますが。

TM1637を用いた4桁LED表示器の種類(大きさ、表示色、コロンとピリオドの有無など)は色々あります。

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

今eBayを覗いてみると、現時点はコロナの影響もあって送料がかかるものが多く少々高いかもしれませんが、それなりに経済的。

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

Arduinoとは試作では例えば次のようにつなぎます。

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

各7セグメント表示用のデータは次の形のバイナリー形式です。

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

pで示したものはピリオドだけの場合もコロンだけの場合もありますが、両方表示できても予めハードウェア回路で固定され、変更できないものが殆どです。ちなみにピリオドだけのは次のもの。写真がちょっとぼけていてすみませんが。

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

Arduinoでプログラムを組むときは、今ではTM1637用ライブラリー(例えばTM1637Display)が数種類揃っています。しかし、ライブラリーの使い方を確認するのが面倒なら、必要な機能だけを自分でコーディングしてもこのデバイスなら比較的単純にできます。例えば次です。

/* *********************************************************
   Simple roll-your-own TM1637 operations without library
      Sample sketch V00  July 25, 2020 (c) Akira Tominaga 
 *  ********************************************************/
#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

void setup() { // ***** Arduino setup *****
  pinMode(lCLK, OUTPUT);
  pinMode(lDIO, OUTPUT);
  digitalWrite(lCLK, HIGH);
  digitalWrite(lDIO, HIGH);
  lDispData();              	// display 0000
  delay(1000);
}

void loop() { // ****** Arduino Loop *****
  // example 
  for (int k = 0; k < 4; k++) {
    uint8_t Rand = random(0, 15);
    Data[k] = Rand;
  }
  lDispData();
  delay(1000);
}

/**************************************
      User defined functions
* *********************************** */
// *** lDispData *** display data to 4dig LED
void lDispData(void) {
#define showDat 0x40          	// show this is data
#define showAd0 0xC0          	// LED data 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 0x0A: lChr = 0x77; break; // A
      case 0x0B: lChr = 0x7C; break; // b
      case 0x0C: lChr = 0x39; break; // C
      case 0x0D: lChr = 0x5E; break; // d
      case 0x0E: lChr = 0x79; break; // E
      case 0x0F: lChr = 0x71; break; // F
      case 0x10: lChr = 0x00; break; // blank
      default:   lChr = 0x7B;        // 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
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 
void lStart(void) {
  digitalWrite(lDIO, LOW);
  delayMicroseconds(lTu);
  digitalWrite(lCLK, LOW);
  delayMicroseconds(lTu);
}

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

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

 上では表示するコードを7セグに変えるためswitch-case-defaultのロジックで行っていますが、固定byte列をコード値でインデクシングしてもよいでしょうね。その場合は、インデクシング前に0x0Fでandをとるなどしてコード値を制限しないといけませんね。

こういう勝手で単純な”Roll-Your-Own"と呼ばれるプログラムは、ライブラリーの使い方を調べなくてよいのと、マイコン容量消費の最小化ができる利点があるかも^^;  上のプログラムは、pのセグメントを使う場合には適当に加工して応用いただくと良いかと思います。

この場合、CLKとDIOは次の図のように動かしています。下は0000を表示している時のロジックアナライザー表示です。簡便法としてI2Cとして分析していますが、I2Cとは逆に、TM1637での約束は各バイトを下位ビット(つまりLSb)から順に送るので、各バイトごとにビットの並びが逆転しているので注意がいります。またスレーブアドレスなどはありません。

 

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

TM1637側は受信バイトに対してAck以外の応答をしませんから、DIOピンのモードをいちいち送信から受信に切り替えなくとも、早めのタイミングで自分でLOWにすれば送信モードのまま変えずにいけます。もしLOWにするタイミングが遅すぎて、スレーブが先にLOWを出す場合は正負で引きあうので注意。もし仮にそういう間違いをしても数マイクロセカンドだけですが心配ならDIOへの接続に数百オームの抵抗をいれれば安心でしょう。上のコーディングの場合は不要です。

 

その他もろもろの情報が必要な場合、TM1637のDatasheetをググってご確認いただくと良いと思います。

 

今回は超簡単な解説記事でしたが、このへんまでにしたいと思います。TM1637をお知りになりたい方々へ、少しでもお役に立つなら幸いです。

 

©2020 Akira Tominaga, All rights reserved.

室内CO2濃度の簡単なロガー

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

前回の「室内炭酸ガスの濃度測定」で少しご紹介したロガーの製作についてです。前の記事から少し間があいてしまいましたため、ここで急いで簡単にご紹介することにします。

ロガーでは、センサーが空気と接するよう、上の写真のようにケースの右側にとりつけてセンサーの前面に小さな穴を設けておきました。換気している部屋の机の隅において、窓を閉めたり全開したりすれば、次のような測定値が記録できます。

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

前回は、ロガーをブレッドボードで試作したところまででしたが、その後すぐに基板に組んで1号機を作りました。次の写真の下の基板です。

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

このロガーの回路は前回のとほぼ変わりませんが、都合のよいようにピンを変えたところがありますので、ここに改めて掲載します。

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

Arduinoプログラムはこの記事の最後の方につけておきます。

最初の基板では電源スイッチをつけましたが、ケースに収めにくいので、「不要なものはとにかく外す」という主義で他の位置関係などを含めて少し改善をしたのが次の元図。

これを作るには、過去の工作で作った部品ごとのパターンを切り貼りしてまずは部品面の図を作り、左右逆転します。毎度ながら時間節約のため、ランドなどは勝手に省略する基板です。基板の大きさは105mm x 78mm です。

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

いつものことですが、ツールパス・データに変換してCNCルーターで削り20分程でできます。

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

次が基板です。

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

加工の前にまずはジャンパー線で補完、今回は10本と少し多めですが。グラウンドは0.6φスズメッキ線5本、その他は細い銅線5本です。

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

最初に3V用レギュレータだけを配線して、センサーの予定電圧に正確に合わせておきます。

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

多回転ポテンシオメーターなので上図のように精密に合わせられます。また生活の知恵として他の抵抗器を混ぜないので、温度補正が不要になり常に安定した電圧になります。

ここまで行けば後はさっさと部品を付けて完成。

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

ブレークアウトモジュールは試作用にL型ヘダーピンが付いたものが多いですが、無理に外して付け替えずにソケットで対応します。なにせハンダを吸い取る場合はパターンを傷つけてしまうことが多いので、必要悪ですね。ハンダ吸い取り線だと時間がかかるし、「はんだシュッ太郎」ではパターンを剥がしやすく、ヒートガンではパーツにダメージを与えることがあるので、とにかくそのまま使うようにしています。

センサー部品(SMD)を直接使うほうが小さくて良いですが、購入数量が少ないと、ブレークアウトモジュールのほうが圧倒的に安いうえ扱いやすい。もしピンヘダーが予め着いてなければ言うことないのですが、そういうものは多くはありません。

上の例では上部のマイクロSDライターと、下部のTM1637はブレークアウトモジュールそのままです。左にあるRTC(リアルタイムクロック)DS3231モジュールは、空いているI2C用4穴に新たな平型ヘダーピンをつけて使います(L型ピンは整形し樹脂粘土をかぶせて脚にしているがカットしてもよい)。中央のロジックレベルコンバーターはコストも安いので必要な穴にだけスズメッキ線を通して直接ハンダづけします(底部がジャンパー線に当たらないよう少し浮かせてつけます)。

組み立て上がりを銅箔面から見ると次です。

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

1号機と並べると次です。下が今回の2号機。大して変わりませんが。

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

さて、2つ用意した目的は、もちろん長時間ロギングした値の変化を比較しようというものです。なぜなら、48時間のバーンインの最中に値が上昇する状況を詳しく見たかったからです。そのため、CCS811モジュールを改めて2個入手し、最初から行いました。

バーンイン24時間後時点で再スタートし、残りの24時間の途中で次のように値の上昇がみられます。

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

驚くべきことに2つの独立したセンサーの出す異常値は同じ変化をしています。つまり環境の僅かな変化が増幅されて値に反映されているように見えます。このセンサーの内蔵ソフトウェアはいったいどのような処理をしているのでしょうか。

とにかくそれからしばらくすると何事もなかったかのように落ち着きますから、このセンサーは凡人の理解を超えています(・・?。

バーンインの終わった状態でケースにとりつけました。このケースはだいぶ前に秋月で購入したポリカーボネートの丈夫なケースで、加工も簡単です。

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

センサー取り付け部分は、後から側面を適当に加工したため次の写真のように少し曲がってとりつけてしまいましたが、まあよしとします。

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

これで室内を記録した結果、換気状態に応じた値を敏感に示し異常なく動作します。なお、始まりの20分間はこのセンサーの「Run-in時間」とされ、測定値を無視すべき時間です(下のグラフでは09:22以降が測定値として有効です)。

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

今のところ1秒単位で計測・表示し、SDへは分単位で(分替わりの最初の測定結果)1つを記録していますが、記録としては秒単位の移動平均をとればスムーズかもしれません。あるいは複数回の平均をとるのもよいかもしれません。とはいえセンサーの提供する個々の値も既に多数回測定し平均した結果だろうとは思いますが。

また、この測定は秒単位の「モード1」なのでセンサーを構成しているヒーターは常時オンになっています。オンオフするモードでどうなるかも今後調べたいところです、時間のあるときにマイペースでなのですが。

しばらく使いこんだら「ベースライン設定」、つまりキャリブレーションが望ましいとのこと。少し手間のかかるセンサーですが、それほど正確なのでしょうか。マニュアルは次のサイトにまとめてリストアップされています。

www.sciosense.com

今回のプログラムは次です。毎度のことですが、Arduino-UnoにIDEで書き込み、ATmega328pの単体を取り出して使います。いつもそういう使い方をするため、UNOの本体には無圧(Zero Pressure)ソケットを装填しておきます。

/* *********************************************************
   CO2 Logger V03                    (c)2020 Akira Tominaga
   Function:
    Measure CO2 and TVOC using CCS811, with RTC & TM1637.
   Revisions:
    V00: Original devloped on 6/25/'20 
    V02: Show sensor error status on display        6/28/'20
    V03: SD-close-switch from D2 to D4              7/05/'20
 *  ********************************************************/
#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 4              // 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
byte Data[] = { 0x3F, 0x3F, 0x3F, 0x3F }; // data for TM1637 init 0000
// *** 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
#define erCd CO2            // error code instead of CO2, when error 

void setup() { // ***** Arduino setup *****
  Serial.begin(9600);
  Wire.begin();
  pinMode(SDsw, INPUT_PULLUP);
  disp.setBrightness(0x0A);
  if (!SD.begin(Cs)) {
    //Serial.println("SD E");
    Data[2] = disp.encodeDigit(0x02); // 0020=SD Error
    disp.setSegments(Data);
    while (1) {}
  }
  if (!AQ.begin()) {
    //Serial.println("AQ E");
    Data[2] = disp.encodeDigit(0x03); // 0030=AQ Error
    disp.setSegments(Data);
    while (1) {}
  }
  if ( !AQ.start(CCS811_MODE_1SEC) ) {
    //Serial.println("Am E");
    Data[2] = disp.encodeDigit(0x04); // 0040=AQ mode-set Error
    disp.setSegments(Data);
    while (1) {}
  }
  // set SD file name MMDDhhmm
  getTime();
  String s = strMDhm;
  s.concat(".csv");      // complete Comma-separated-value 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 ) {
    // for serial plotter 
    Serial.print("400,");Serial.print(CO2); Serial.println(",1000"); 
   } else if ( errSt == CCS811_ERRSTAT_OK_NODATA ) { // if no data
    erCd = 9000;          // show waiting-data
  } else if ( errSt & CCS811_ERRSTAT_I2CFAIL ) {    // if I2C error
    erCd = 9100;          // show I2C-error
  } else {                // for all other erros,
    erCd = 9200 + errSt;  // add error status to erCd 92ee
  }
  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 */

 Arsuino-Unoにどのように無圧ソケットをつけて使うかは次の記事に詳しく書いてありますので、必要な方は次のリンクをクリックしてご参照ください。

Zoom用かんたん操作ボタンをArduino-UNOで作る(その1) - 勝手な電子工作・・

 

さて、思い出したのでついでに書いておきます。この測定値を従来のNDIRセンサーと比較してみたいところです。最近世界中へ多く出ているのは中国製の次のものらしい。取り寄せたらすぐに来ました。

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

試しにUART接続で簡単に使ってみると次のように安定して測定されます。記録は分単位で、ピーク2つは息をかけたところです、だいたい2分後(!)に応答します。長時間の記録にも安定して使えるようにみえます。走り出しの2分間はRun-inタイムです。

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

赤外線吸収タイプなので価格は15ドルほどしますが、ずいぶん低価格になったものです。微細なほこりなどが入らないように空気の出入り口にはフィルターがありますから、反応もそのぶんは遅いでしょうね。

こちらのセンサーも長時間の間にはキャリブレーションを要しますがその操作は簡単です。そのうちにこちらを試してみようかと思いますが、うまく行けば半導体センサーと併用する測定がよいのではないかとふと感じます。

 

では今回はこの辺で。

 

©2020 Akira Tominaga, All rights reserved.

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

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.

Arduinoマウスでスピログラフを描く

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

Windows Paintにマウスで書いた?スピログラフ

スピログラフは半世紀も前に流行ったデザイン用のおもちゃで、色んな模様が描けます。その定規は百均でも売られていますが、もはやこれが何であるか知っている人の方が少ないかもしれません。

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

小さな円を大きな円に内接させながら穴に鉛筆やボールペンなどを差し込んでぐるぐる回せば模様が描けます。器用な人だと、この模様をいくつか組み合わせて次のようなデザインをします。

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

 

上の写真は次に載っています。

2020 3D Spirograph Drawing Toys Set Interlocking Gears Wheels Painting Drawing Accessories Creative Educational Toy Children For Creativity From Kidcostume, $2.52 | DHgate.Com

この最初の品は1965年に発売され、以後10年以上も世界で大ヒットしていたらしいです。

Denys Fisher original Spirograph 1965 | ralph stephenson | Flickr

実際使ってみればわかりますが、不器用な人はどこかで失敗して線が違う方向へとずれたりします、私もですが^^; やり直すのもたいへん。

そこで、PCで描けばずっと簡単にできそうです。そのために、まず式を考えます。汚いメモ書きですみませんが。

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

円Bの中心PがΘ回れば、円B自体はがa/b倍だけ逆に回るので、座標軸からみた傾き(Pから穴Qへの向き)としては Θー(a/b)Θ 回ることになります。なので上の式になるわけです。

これさえわかると後は簡単。プログラムで確認すると次のようになります。

youtu.be

画面をカメラで撮ったので少し傾いていてすみません<(_ _)>。

驚くような模様が次々に現れるから不思議です。このプログラム自体は記事の最後の方に載せておきますね。今回はMicrosoft Small Basicでちょこっと作ったシンプルなプログラムです。

さて、これでは当たり前で全然面白くないし、第一に電子工作とは言えませんね。電子工作として子供たちにも見せるには、マウスで描いて見せればよいかなと、忙しいときに余計なことをつい考えついてしまいました。

これならArduinoをマウスとして動かせばすぐにもできそうです。早速作るとしましょう。

前にキーボードを作るために余分に買ったArduino Pro Microが余っていますので、これを使ってみます。このモデルはコンパチ機が流通の主体で、海外だと1つ4ドル前後です。最新のIDEではLeonardoとしてプログラミングします。

考えてみたらこれには特別な回路など殆ど要りませんね。ここでは、単にディジタルピン8にタクトスイッチをつなぐだけにしました。

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

配線はグランドから右へ出した1本だけです。これにタクトスイッチの片側を接続。ハードウェアはたったのこれだけです。

Arduino IDEでプログラミングする際、ボードはLeonardoを指定。プログラム・スケッチは次のとおりで50行ほどの小さなものです。

/* Spirograph drawing machine. Arduino Pro-Micro as a Mouse
   (c)Akira Tominaga  June 15th, 2020                      */
#include "Mouse.h"
#define Button 8      // Button pin
#define bChatT 100     // chattering time
float rA = 300.0;
float rB = 211.0;
float rC = rB * 0.90;
float KakuHi = (rB - rA) / rB;
float Pi = 3.141593;
float gyakuPi = (1.0 / Pi) / 100.0;
int xX;
int yY;
#define pX 0
#define pY 0
#define rBdecr 0.9
#define rCrate 0.8

void setup() {
  pinMode(Button, INPUT_PULLUP);
  Serial.begin(9600);
  Mouse.begin();
  while (digitalRead(Button) == HIGH) {}
  delay(bChatT);
  while (digitalRead(Button) == LOW) {}
}
void loop() {
  int xS = pX;
  int yS = pY;
  int i = 0;
  for (float Theta = 0 ; Theta < 300 * Pi ; Theta = Theta + gyakuPi) {
    if (i > 0) {
      Mouse.press();
    }
    xX = (float)( (rA - rB) * cos(Theta) + rC * cos(KakuHi * Theta));
    yY = (float)( (rA - rB) * sin(Theta) + rC * sin(KakuHi * Theta));
    int Xmove = xX - xS;
    int Ymove = yY - yS;
    Mouse.move(Xmove, Ymove, 0);
    if (digitalRead(Button) == LOW) break;
    xS = xX;
    yS = yY;
    i = 1;
  }
  while (digitalRead(Button) == LOW) {}
  delay(bChatT);
  Mouse.release();
  while (digitalRead(Button) == HIGH) {}
  while (digitalRead(Button) == LOW) {}
  delay(bChatT);
  rB = rB * rBdecr;
  rC = rB * rCrate;
  KakuHi = (rB - rA) / rB;
  delay(300);
}

 操作は次のようにしますが単純です。

お絵描きソフトとして、Windows Paint(マウスで描けるソフトならほかに何でもよいです)を立ち上げ、

①書き始めの地点(出来上がる図形の上下中央の右端になります)にカーソルをもっていく。

②タクトスイッチを押す。するとスピログラフを描き出します。

止めたいところで再び

③タクトスイッチを押せばとまります。

youtu.be

止まっている間にブラシや鉛筆、色などを選んでからカーソルの場所を移動して①②③を繰り返せば色や太さが選べます。

 

これなら子供達の前で見せたり、どや顔もできそう。色々工夫すれば好きなことができますが、何も役立ちませんね^^

最後にWindowsの一番簡単な言語と思われるSmall Basicで書いたプログラムを入れておきます。outputは最初のほうに出した動画です。もちろん言語は何でもよいです。

myTitle="Automatic Spirograph V00  (c)2020 A.Tominaga "

' GraphicsWindow definition
Gt=10 ' top edge position
Gl=50 ' left edge position
Gw=900 ' window width
Gh=900 ' window height
Ds=1.25 'display scaling rate is 125% in this case
GraphicsWindow.top=Gt
GraphicsWindow.Left=Gl
GraphicsWindow.Width=Gw
GraphicsWindow.Height=Gh
GraphicsWindow.Title=myTitle

' TextWindow definition
Tt=(Gt+Gh)*Ds-377 'adjusted text window's top edge position
Tl=(Gl+Gw)*Ds+2   ' set clearance with graphic window
TextWindow.top=Tt
TextWindow.left=Tl
TextWindow.Title=myTitle

' Radii of Circles
rA=400 'A circle radius
rBdefault=171
Inq:
TextWindow.Write("*** rA=400. Input rB initial size -> ")
rBinit=TextWindow.Read() 'B circle radius specified
If rBinit="" Then
  rBinit=rBdefault
EndIf
If rB>rA-1 Then
  goto Inq
EndIf 
rCdefault=0.7 ' default C radius ratio to rB

' Main processes
For rB= rBinit To 350
  rC=Math.Round(rB*rCdefault)
  ' define pen colors
  cR=Math.GetRandomNumber(255)
  getcG:
  cG=Math.GetRandomNumber(255)
  If cR+cG>320 Then
    Goto getcG
  EndIf
  cB=Math.Round(255-(cR+cG)/1.2)
  GraphicsWindow.PenColor=GraphicsWindow.GetColorFromRGB(cR,cG,cB)
  GraphicsWindow.PenWidth=1
  
  'draw Spirograph
  GraphicsWindow.Clear()
  XS=Gw/2 ' initial X position
  YS=Gh/2 ' initial Y position
  ' Loop to draw graph
  for theta= 0 To 500 Step 0.2
    XX=(rA-rB)*Math.Cos(theta)+rC*Math.Cos(theta*(rB-rA)/rB)+Gw/2
    YY=(rA-rB)*Math.Sin(theta)+rC*Math.Sin(theta*(rB-rA)/rB)+Gh/2
    If theta>0 then ' exclude initial position
      GraphicsWindow.DrawLine(XS,YS,XX,YY)
    EndIf
    XS=XX
    YS=YY
  EndFor

  ' Draw legend to show radius values of  A, B, and C
  fontSize=20
  GraphicsWindow.FontSize=fontSize 
  fontW=fontSize/2 ' font width average to be
  Lx=150 ' legend characters' X-position
  Ly=65  ' legend characters' Y-position
  Legend="Radii   A=400   B="
  LegLen=Text.GetLength(Legend)
  GraphicsWindow.DrawText(Lx,Ly,Legend)
  GraphicsWindow.DrawText(Lx+LegLen*fontW+2,Ly,rB)
  LegLen2p=Lx+(LegLen+4)*fontW 'Len2 as pixel number
  GraphicsWindow.DrawText(LegLen2p,Ly,"  C=")
  GraphicsWindow.DrawText(LegLen2p+4*fontW+2,Ly,rC)
  Name="Programmmed by A. Tominaga"
  NameLen=Text.GetLength(Name)
  fontSize=fontSize*0.6
  GraphicsWindow.FontSize=fontSize
  fontW=fontSize/2
  Lx=Gw-(NameLen*fontW+Lx)
  GraphicsWindow.DrawText(Lx,Ly+fontW,Name)
  Program.Delay(1000)
EndFor

Sound.PlayBellRing()
Program.Delay(2000)
Program.End()

今回は単純で「どこが工作?」という感じではありますが、このへんで失礼したいと思います。

後で気づいたので補足します:三角関数は中学校で習うものと思っていたら、今は高校のようです。このIT時代に不思議なことですね。

 

©2020 Akira Tominaga, All rights reserved.