小さなPM2.5記録計を作りました。最近、仕事に関連した製作が多いため、工作室の空気の汚れが気になっています。とくにオリジナル基板のほとんどはCNCルーターで削るため、細かい塵がさぞやたくさん出ているかなと。削るときはもちろろんN95マスクをつけ窓を開けて換気していますが。
この機械をブレッドボードで試作してすぐに測ってみると、外気の値は「リアルタイムPM2.5マップ」の当地の値とよく一致。その日は花粉が多くとんでいる日でした。そして気になっていた測定を。。。ところが、CNCで基板を切削する際には、想像していたほどは室内の微小粒子状物質が増えないのがわかりました。部屋のよごれからみて、意外に大きな埃だけを出すようです。その代わり、電子工作では他の作業がもっとひどく汚すことが間もなくわかり、驚きました!
PM2.5センサーのうち、小型で手ごろなのはレーザー散乱光計測方式のものです。ミニファンで外部の空気を吸い込み続け、内部のダクトを通る際に前方特定角度にあるレーザーからの散乱光の強さを測定することで、通過する微小物質の個々の大きさを識別するしかけになっています。
ここではWinsen社のZH03Bを使いましたがArduinoで至極楽ちんに短時間でできました。このセンサーでの測定データを得るのには複数のプロトコル(いずれもUART)が用意されています。その中で、センサー側から一定間隔で一方的に送ってくるデータを素直に受け取る方式、Sensor Initiative Uploadが一番楽です。その場合に必要な接続はセンサー側のTxラインとGndだけです。
接続はごく簡単ですので、この記事の最後につけるArduinoスケッチを参照してください。ロガーとするために必要なデバイスは、他に①リアルタイムクロック(I2C接続)と ②マイクロSDカードモジュール(SPI接続)をつなぎ、またPM2.5の数値をリアルタイム表示するための③4桁LED(TM1637、2線接続)も接続します。これだけ使っても、速度の上でも容量の上でもUnoで十分な余裕があります。
ZH03Bは立てて設置する必要がありますが、それだけで50mm強の高さがあります。そこで、この際はRTCモジュールもSDカードモジュールもピンの着いたまま立てて、メスソケットに挿しこむことにしました。こうすると基板の作成もとても楽。
え?ランドがないうえ、角が多いひどい基板だ?--> これでいいんです^^; それを言うとブレッドボードはこの何十倍も問題。そんな環境で動くデジタルものは何ら問題ないです、アナログでも高周波でもありませんからね。そういうものは全てCNCルーターの高価な超硬エンドミル寿命を優先、つまり削る個所を極端に減らすパターンにしてるわけです。
センサーの空気の流入口と吸出口をふさいではいけないので、前後を開放。アクリル板を加工した簡単な屋根を付けることにしました。マイクロSDカードを取り出す際は屋根を倒して開ける構造にすることで、幅76x奥行60x高さ60のコンパクトな機械になりました。
なお、2号機ではスイッチを1つ追加しましたが、これはマイクロSDへの物理的書き込み中に電源オフが重ならないようにするためのもの(重なる確率はおよそ数十万分の1ですが)なので、なくても問題ないかとは思いますが。とにかく長時間記録したものを絶対失いたくないので^^; 電源を切るときだけこのスイッチをオンにするわけです。この記事の最後につけるプログラムはそのスイッチもつけたものです。無視されても良いかと思います。
この機械は忙しいさなかに考えながら少しずつの時間を使って作りましたが、めでたく2台が完成!
右が1号機、左が2号機でスイッチが1つ追加され、高さ、幅ともに5mmほど小さくできました。2台作ったのはもちろん別々に測ったデータがどれほど一致するかを見たかったからです。
そして2号機の製作中に1号機で記録をつづけていたら、ビックリすることがわかりました。
この日は花粉もなく外気は綺麗な日です。なんとはんだ付けで微小粒子状物質が大量にまき散らされるのがわかりました。たしかに松脂の煙を含む蒸気をだしてますし臭います。
しかし数メートルはなれたこのセンサーの位置(窓に近い)で、こんなに直ちにばっちり記録されるとは驚き・・・。
そして2号機が完成したので、同時に測定を開始。その結果2台の数値は常に非常によく一致することがわかりました!
別個の独立した測定装置でこんなによく一致するとは!、これもまた今回の製作の驚きでした。
今回は簡単な記事で図面等も省略させていただきますが、最後にプログラムをつけておきます。これをご覧いただいた方が接続などはわかりやすいかもしれません。
/***************************************************************
Air-dust Density Logger for Arduino-Uno (ATmega328P)
Initial Version V.00 Mar.26 2022 (c)Akira Tominaga
Functions:
- Measure PM1.0, PM2.5 and PM10 densities (μg/m^3).
- Display current density of PM2.5 to TM1637 LED.
- Record each minuite's average data to micro SD card.
Pin connections:
Air-dust sensor ZH03B; TX = software Rx pin 2.
Micro SD card; MOSI=11, MISO=12, CLK=13, CS=10.
Real time clock DS3231; SDA=A4, SCL=A5.
TM1637 4digit LED display: DIO=6, CLK=7.
Optional switch: pin9 to avoid pwr-off during
SD-write timing (though extremely rare).
***************************************************************/
#include "SoftwareSerial.h"
#define sRx 2 // sS-Rx = sensor Tx pin
#define sTx -1 // sS-Tx = sensor Rx not connected
#define sSbaud 9600 // software-serial baud rate
#include "Wire.h" // for RTC interface
#include "SPI.h" // for SD interface
#include "SD.h" // for micro SD drive
byte rBt[24]; // rcv bytes from software-Serial
// *** for air-dust sensor ***
SoftwareSerial sS(sRx, sTx); // sS as SW-serial symbol
uint16_t PM1_0, PM2_5, PM10_; // measured values
#define iuLen 24 // length of sensor-Initiative-Upload
uint16_t cSum; // checksum
uint8_t dC = 0; // data counter to calculate averages
uint16_t tPM1_0 = 0; // total PM1.0 value
uint16_t tPM2_5 = 0; // total PM2.5 value
uint16_t tPM10_ = 0; // total PM10. value
// *** for Real Time Clock ***
#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
char MMDD_hhmm[14]; // editing area for calendar and clock
char hhmmss[10]; // editing area for hh:mm:ss
uint8_t vI[8] = { 0, 0, 0, 0, 0, 0, 0, 0}; // integers for RTC data
// *** for micro SD drive ***
#define cS 10 // chip select
String fName; // CSV file name
String Rec; // contents of record
uint8_t minSave; // save area for minute value
// *** SD disable switch to avoid power-off during SD writing
#define SDsw 9 // SD enable/disable switch pin
// *** for TM1637 display to diplay real time PM2.5 μg/m^3
#include "TM1637Display.h"
#define tmDIO 6 // TM1637 DIO
#define tmCLK 7 // TM1637 CLK
TM1637Display TM(tmCLK, tmDIO); // class TM for TM1637
void setup() { // ***** Arduino setup *****
Serial.begin(sSbaud); // start hardware serial
sS.begin(sSbaud); // start SW-serial sS
Wire.begin(); // start I2C
// *** start TM1337 display
TM.setBrightness(2); // *** set LED brightness(low0-high7)
const byte segs[] = {0x40, 0x40, 0x40, 0x40}; // "----"
TM.setSegments(segs, 4, 0); // displsy "----"
// *** set SD file name as MMDDhhmm.csv, using start-time
getTime();
minSave = vI[mdm]; // save minute value for later use
sprintf(MMDD_hhmm, "%02d%02d%02d%02d.csv", vI[mdM], vI[mdD], vI[mdh], vI[mdm]);
fName = String(MMDD_hhmm);
pinMode(SDsw, INPUT_PULLUP); // set SD enable/disable Sw HIGH(enable)
// *** check SD card
if (!SD.begin(cS)) {
Serial.println(F("SD err"));
while (true) {}
}
File aqLog = SD.open(fName, FILE_WRITE); // make the file for data
// ***** write column header
aqLog.println("MM/DD-hh:mm,PM1.0,PM2.5, PM10");
Serial.print(fName); Serial.println(F(" created"));
aqLog.close();
delay(200);
}
void loop() { // ***** Arduino Loop *****
while (sS.available() < iuLen) {} // wait if not data ready
// *** read data from sensor
for (int j = 0; j < iuLen; j++) {
rBt[j] = sS.read();
} // got data
// *** check validity of the data
#define okD 0x00
#define ngD 0x01
byte ckD = okD;
// *** confirm correct sensor-initiative-upload
if (rBt[0] != 0x42) { // if invalid I.U. header
ckD = ngD; // do not use error data
flushData();
}
// ***** check Checksum *****
cSum = 0;
for (uint8_t j = 0; j < iuLen - 2; j++) {
cSum += rBt[j];
}
byte cSh = cSum / 256;
byte cSl = cSum % 256;
if ((cSh != rBt[22]) | (cSl != rBt[23])) { // if error,
Serial.println(F("*Cksum err")); // then
ckD = ngD; // do not use error data
flushData();
}
// *** process for valid data only
if (ckD == okD) {
getTime();
sprintf(MMDD_hhmm, "%02d/%02d-%02d:%02d", vI[mdM], vI[mdD], vI[mdh], vI[mdm], vI[mds]);
sprintf(hhmmss, "%02d:%02d:%02d ", vI[mdh], vI[mdm], vI[mds]);
PM1_0 = rBt[10] * 256 + rBt[11];
PM2_5 = rBt[12] * 256 + rBt[13];
PM10_ = rBt[14] * 256 + rBt[15];
Serial.print(hhmmss); Serial.print(PM1_0);
Serial.print("-"); Serial.print(PM2_5);
Serial.print("-"); Serial.println(PM10_);
TM.showNumberDec(PM2_5, false, 4, 0); // show PM2.5 to LED
// *** if minute changed record log, else accumulate data
if ((minSave != vI[mdm]) & (dC > 0)) { // if minute value changed,
minSave = vI[mdm]; // save new minute to check
float fPM1_0 = (float)tPM1_0 / (float)dC;
float fPM2_5 = (float)tPM2_5 / (float)dC;
float fPM10_ = (float)tPM10_ / (float)dC;
Rec = String(MMDD_hhmm) + "," + String(fPM1_0, 2) + "," + String(fPM2_5, 2) + "," + String(fPM10_ , 2);
putLog();
Serial.println(Rec);
dC = 0; tPM1_0 = 0; tPM2_5 = 0; tPM10_ = 0; // reset accumulated values
} else { // if not timing, accumulate totals
dC++; tPM1_0 += PM1_0; tPM2_5 += PM2_5; tPM10_ += PM10_;
}
}
// no delay allowed in the sensor-initiative-upload mode
} // end of loop()
/***********************************************************
User defined functions
***********************************************************/
// ***** get time *** getTime() *****
void getTime(void) {
byte vR[8]; // values in RTC registers
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
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);
}
// ***** check and flush if broken data exists
void flushData(void) {
int resBt = sS.available();
if (resBt > 0) { // if rest of data exists
for (int k = resBt; k <= 0; k--) {
rBt[0] = sS.read(); // dummy read for flush
} // end for
Serial.println(F("Flushed"));
} // end if
}
// ***** put log Rec *** putLog() *****
void putLog(void) {
if (digitalRead(SDsw) == HIGH) { // do only when SDsw is HIGH
File aqLog = SD.open(fName, FILE_WRITE);
aqLog.println(Rec);
aqLog.close();
}
}
// ***** End of Sketch **
以上、短時間でご紹介しましたが、この記事が今後どなたかのお役に立てば幸いです。
©2022 Akira Tominaga, All rights reserved.