/* MH-Z19C CO2ガスモニタ 20210617_MH-Z19C_CO2meter V0.9 機能:測定結果をOLEDに表示、グラフ表示、ゼロ調の自動/マニュアル切り替え    グラフレンジ切替・設定、スクリーンセーバー、アナログ表示 ラジオペンチ 2021/6/17 http://radiopench.blog96.fc2.com/ */ #include #include #include // 0.96インチOLED使用 #include // CO2センサ都のインターフェイスに使用 #include // 時計の針の駆動パルス作成に使用 #include // 測定周期の設定に使用 #include #define MODE_PIN 2 #define ENTER_PIN 3 #define SELECT_PIN 4 #define COIL_P 5 #define COIL_N 6 #define RX_PIN 10 #define TX_PIN 11 #define LED_PIN 13 #define BAUDRATE 9600 // MH-Z19の通信ボーレート(固定値) #define T0 1000 // 測定インターバル(ms単位で指定、最小値は1000=1sec) #define DISP_ON_TIME 3600 // 表示点灯時間(消灯までの時間) #define DISP_WAKEUP_VALUE 1000 // 表示を強制ONにするCO2濃度 #define T1 20000 // 順回転パルス幅(us) #define T2 60000 // 順回転パルス間隔 #define T3 5800 // 逆回転第一パルス幅 #define T4 8000 // 逆回転第二パルス幅 #define T5 120000 // 逆回転パルス間隔 #define DEAD_BAND 5 // 制御の不感帯幅(の片側) #define SCREEN_WIDTH 128 // OLED Xサイズ #define SCREEN_HEIGHT 64 // OLED Yサイズ #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) Adafruit_SSD1306 OLED(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); unsigned int co2Val; // CO2の測定結果 unsigned int calib; // キャリブレーションモード int dispTimer = DISP_ON_TIME; // 消灯までの残り時間 byte ReadCO2[9] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79}; // read command byte SCalOn[9] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00, 0xE6}; // キャリブレーションONコマンド byte SCalOff[9] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x86}; // キャリブレーションOFFコマンド byte RetVal[9]; // レスポンス struct recipe { // レンジレシピの構造 int gInterval; // グラフインターバル byte scaleV; // 30画素(1Div.)当たりの時間 char scaleC[2]; // 時間の単位文字 }; struct recipe hRange[12] = { { 1, 30, "s"}, // range 0 プロット周期, 1div時間, 時間単位文字 { 2, 1, "m"}, // 1 { 4, 2, "m"}, // 2 { 10, 5, "m"}, // 3 { 20, 10, "m"}, // 4 { 40, 20, "m"}, // 5 { 120, 1, "h"}, // 6 { 240, 2, "h"}, // 7 { 480, 4, "h"}, // 8 { 960, 8, "h"}, // 9 {1440, 12, "h"}, // 10 {2880, 1, "d"}, // 11 }; int dataBuff[91]; // データーバッファ (0-90の91個) int latestData; int dataMin = 400; // 最初のグラフ作図まではこの値で目盛を表示 int dataMax = 400; // int tCount = 0; unsigned int rNum = 0; // レンジ番号(rangeNumber) 0-11 char chrBuff[6]; volatile boolean timerFlag = false; // タイマー割り込みフラグ boolean entPushed = false; volatile boolean cw = true; // 回転方向 cw:時計回り volatile int sv; // 設定値(set variable) int pv; // 現在値(process variable) int cpx; SoftwareSerial mySerial(RX_PIN, TX_PIN); // MH-Z19Cとのインターフェイスに使用 void setup() { pinMode(MODE_PIN, INPUT_PULLUP); // オートゼロモード指定 pinMode(ENTER_PIN, INPUT_PULLUP); // ENTERボタン pinMode(SELECT_PIN, INPUT_PULLUP); // SELECTボタン pinMode(COIL_P, OUTPUT); // 時計のコイル ポジ pinMode(COIL_N, OUTPUT); // 時計のコイル ネガ pinMode(LED_PIN, OUTPUT); // LEDピン pinMode(RX_PIN, INPUT); // ソフトシリアル用 pinMode(TX_PIN, OUTPUT); // ソフトシリアル用 mySerial.begin(9600); digitalWrite(LED_PIN, HIGH); // setupの処理中はLED点灯 for (int i = 0; i <= 90; i++) { dataBuff[i] = -1; // データバッファを未定義フラグ(-1)で埋める } if (!OLED.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x32 for (;;); // 開始できなかったらここで停止 } OLED.clearDisplay(); OLED.setTextColor(WHITE); if (digitalRead(ENTER_PIN) == LOW) { // 起動時にENTボタンが押されていたら rangeSet(); // レンジ設定を実行 } rNum = EEPROM.read(0); // EEPROMからレンジ番号を読み出す OLED.setCursor(10, 12); OLED.print(F("CO2 monitor V0.9")); // 開始メッセージ OLED.display(); if (digitalRead(MODE_PIN) == LOW) { // スタート時にモードピンがLOWだったら calib = 0; // マニュアルモード(Autoゼロ調無し) } else { // そうでなければ calib = 1; // ゼロ調はAutoモード(デフォルト状態) } setCalMode(calib); // ゼロ調モードを設定 0:手動、1:自動 measure(); // 1回読み飛ばす pv = clockPosition(400); // 起動時は指針が400ppmの位置にあるものと見なす sv = pv; // 針の位置は合っていることにする MsTimer2::set(T0, timer2_IRQ); // タイマー割り込み設定(測定周期を設定) MsTimer2::start(); Timer1.initialize(T1); // 適当な時間を空けて Timer1.attachInterrupt(pm_Home); // パルスモーター制御のホームポジション起動 digitalWrite(LED_PIN, LOW); // setup処理が正常終了 } void loop() { while (timerFlag != true) { // タイマーフラグが立つまで待つ if (digitalRead(ENTER_PIN) == LOW) { // 待機中にENTERボタンが押されたら entPushed = true; // ENTERボタンが押されたフラグを立てる } } timerFlag = false; // 次回割り込み用にフラグを戻しておく if (entPushed == true) { // 途中でENTボタンが押されて、 if (dispTimer > 1) { // まだ表示中だったら dispTimer = 1; // 表示を消す } else { // 表示中では無かったら(消灯中なら) dispTimer = DISP_ON_TIME; // 表示タイマーを元に戻す(点灯させる) } } entPushed = false; // ENTボタンフラグを戻す measure(); // CO2濃度測定(エラーチェックは省略) cpx = clockPosition(co2Val); // 時計の針の位置を計算 cli(); // 不可分処理のために割り込み禁止 sv = cpx; // パルスモーターの目標位置に設定 sei(); // 設定完了したので割込み許可 latestData = co2Val; if (co2Val > DISP_WAKEUP_VALUE) { // CO2濃度が指定値以上だったら dispTimer = DISP_ON_TIME; // 表示時間タイマーをリセット(表示させる) } tCount++; // ログインターバルタイマーをインクリメント if (tCount == hRange[rNum].gInterval) { // ログの指定時刻だったら saveBuff(); // 測定結果をバッファに保存 tCount = 0; } dispTimer--; // 表示タイマーの値をデクリメントして、 if (dispTimer > 0) { // ゼロになっていなければ disp(); // OLEDに表示 } else { // ゼロだったら、 OLED.clearDisplay(); // OLED消灯 OLED.display(); dispTimer = 1; // 次回も消灯される値に設定 } } int measure() { // MH-Z16CからCO2濃度の値を読む int err; mySerial.write(ReadCO2, sizeof ReadCO2); // 測定コマンド送信 memset(RetVal, 0x00, sizeof RetVal); // 受信バッファをクリアしておいて mySerial.readBytes((char *)RetVal, sizeof RetVal); // 測定結果を受信 if (RetVal[0] == 0xff && RetVal[1] == 0x86) { // 正常に読めていたら co2Val = RetVal[2] * 256 + RetVal[3]; // CO2濃度を計算 err = 0; } else { co2Val = 399; // 正常でなければこの値を返して誤魔化す err = 1; } return err; } int clockPosition(int x) { // アナログ表示の針の位置を計算 int y; if (x < 1000) { // 0-999ppmだったら、 y = map(x, 400, 1000, 600, 1500); // 30分の位置をゼロとして、5分=200ppmで計算 } else if (x < 2000) { // 1000-1999ppmだったら、 y = map(x, 1000, 2000, 1500, 2100); // 5分=500ppmで計算 } else { // 2000ppm以上だったら、 y = map(x, 2000, 5000, 2100, 3000); // 5分=1000ppmで計算 } return y; } void setCalMode(int m) { // ゼロ調モードを設定 if (m == 0) { mySerial.write(SCalOff, sizeof SCalOff); // キャリブレーションOFF } if (m == 1) { mySerial.write(SCalOn, sizeof SCalOn); // キャリブレーションON } mySerial.readBytes((char *)RetVal, sizeof RetVal); // ダミー受信。これが無いと電源ON後はresetしないと測定結果が読めない delay(100); } void saveBuff() { // データバッファの更新と最大・最小値の決定 int d; dataMin = 9999; // 最小値 dataMax = 0; // 最大値 for (int i = 90; i >= 1; i--) { // 配列に値を保存しながら最大と最小値を求める d = dataBuff[i - 1]; // 一つ手前の値を dataBuff[i] = d; // 後ろにずらし if (d != -1) { // ずらしたデータが有効値だったら、 if (d < dataMin) { // 最小と dataMin = d; } if (d > dataMax) { // 最大値を記録 dataMax = d; } } } dataBuff[0] = latestData; // 配列の先頭に最新データーを記録し、 if (latestData < dataMin) { // 最小と dataMin = latestData; } if (latestData > dataMax) { // 最大値を再確認 dataMax = latestData; } if (dataMin < 0) { dataMin = 0; // 但し下限は0 } if (dataMax > 5000) { dataMax = 5000; // 但し5000以上なら5000で抑える } } void disp() { // OLED画面表示 OLED.clearDisplay(); // 画面消去 headWrite(); // 上部を書き込み plotPlaneWrite(); // 背景書き込み graphPlot(); // 折れ線グラフ書き込み OLED.display(); // データー転送して画面表示 } void headWrite() { // ヘッダ部を表示 OLED.setCursor(0, 2); OLED.setTextSize(1); // 先頭行に小さな文字で、、 OLED.print(F("CO2")); OLED.setCursor(16 * 6, 0); // 右上にゼロ調モードを表示 if (calib == 0) { OLED.print(F("AZoff")); } else { OLED.print(F("AZon")); } OLED.setCursor(24, 0); OLED.setTextSize(2); // 3倍角文字で sprintf(chrBuff, "%4d", co2Val); // CO2の値を4桁で OLED.print(chrBuff); // 表示 OLED.setCursor(26 + 4 * 6 * 2, 7); // 文字数(4),文字幅(6pix),サイズ(2x)分移動 OLED.setTextSize(1); OLED.print(F("ppm")); // 単位表示 // OLED.print(F(" ")); // OLED.print(sizeof dataBuff / sizeof dataBuff[0]); // デバッグ表示例 // OLED.print(sizeof hRange / sizeof hRange[0]); // デバッグ表示例(レンジ構造体の数) sprintf(chrBuff, "%4d", (hRange[rNum].gInterval - tCount) - 1); // グラフ更新までの残り時間 OLED.setCursor(102, 8); OLED.print(chrBuff); } void plotPlaneWrite() { // グラフ背景作図 OLED.drawFastVLine(30, 16, 40, WHITE); // 左基準線(縦線) for (int x = 30; x <= 120; x += 4) { OLED.drawFastHLine(x, 36, 2, WHITE); // 中心線を点線で描く(横の点線) } for (int x = (120 - 30); x > 40; x -= 30) { for (int y = 16; y < 55; y += 4) { OLED.drawFastVLine(x, y, 2, WHITE); // 縦線を点線で2本描く } } // 左スケール(CO2濃度の値を表示) OLED.drawFastHLine(26, 16, 4, WHITE); // Max値の目盛マーク OLED.drawFastHLine(26, 36, 4, WHITE); // center OLED.drawFastHLine(26, 55, 4, WHITE); // Min OLED.setCursor(0, 16); // Max値表示 sprintf(chrBuff, "%4d", dataMax); OLED.print(chrBuff); OLED.setCursor(0, 32); // 中心値表示 sprintf(chrBuff, "%4d", (dataMax + dataMin) / 2); OLED.print(chrBuff); OLED.setCursor(0, 48); // Min値表示 sprintf(chrBuff, "%4d", dataMin); OLED.print(chrBuff); // 下スケール(時間表示) OLED.setCursor(19, 57); // 時間スケール左 sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -3); // 構造体から持って来て -3倍 OLED.print(chrBuff); // OLEDに転送 OLED.print(hRange[rNum].scaleC); // 単位文字 OLED.setCursor(49, 57); // 時間スケール中央 sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -2); // -2倍 OLED.print(chrBuff); OLED.print(hRange[rNum].scaleC); OLED.setCursor(79, 57); // 時間スケール右 sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -1); // -1倍 OLED.print(chrBuff); OLED.print(hRange[rNum].scaleC); OLED.setCursor(118, 57); // 時間ゼロ OLED.print(F("0")); } void graphPlot() { // 配列の値に基づきデーターを折れ線グラフで表示 long y1, y2; for (int i = 0; i <= 89; i++) { if (dataBuff[i + 1] == -1) { // y2のデーターが未定(-1)だったら break; // プロット中止 } y1 = map(dataBuff[i], dataMin, dataMax, 55, 16); // プロット座標へ変換 y2 = map(dataBuff[i + 1], dataMin, dataMax, 55, 16); // プロット座標へ変換 OLED.drawLine(120 - i, y1, 119 - i, y2, WHITE); // 折れ線でCO2濃度グラフを描画 } } void rangeSet() { // 記録レンジの設定 unsigned int d; d = EEPROM.read(0); if (d > 11) { // レンジ番号が範囲外だったら、 d = 0; // 0に設定 } OLED.setCursor(0, 40); // 小さな字を先に書いておく OLED.print(F("SEL:change value")); OLED.setCursor(0, 50); OLED.print(F("ENT:save and exit")); OLED.setCursor(0, 0); OLED.setTextSize(2); // 以下は2倍角文字で表示 OLED.print(F("Range set")); OLED.display(); while (digitalRead(ENTER_PIN) == LOW) { // ENTERボタンが離されるまで待つ } delay(30); OLED.setCursor(0, 20); OLED.print(F("Fs = ")); // フルスケール= sprintf(chrBuff, "%2d", 3 * (hRange[d].scaleV)); // レンジ定義から値を持って来て3倍 OLED.print(chrBuff); // OLEDに転送 OLED.print(hRange[d].scaleC); // 単位文字 OLED.display(); // 実際に表示 while (digitalRead(ENTER_PIN) == HIGH) { // ENTERボタンが押されるまで if (digitalRead(SELECT_PIN) == LOW) { d++; // レンジ番号をインクリメント if (d > 11) { // レンジ番号が上限だったら d = 0; // 先頭に戻す } OLED.fillRect(0, 20, 128, 16, BLACK); // 前の値を矩形で塗りつぶしで消す OLED.setCursor(0, 20); OLED.print(F("Fs = ")); sprintf(chrBuff, "%2d", 3 * (hRange[d].scaleV)); // レンジ定義から値を持って来て3倍して OLED.print(chrBuff); // OLEDに転送 OLED.print(hRange[d].scaleC); // 単位文字 OLED.display(); while (digitalRead(SELECT_PIN) == LOW) { // SELECTボタンが離されるまで待つ } delay(30); } } EEPROM.write(0, d); // レンジ番号をEEPROMに保存 OLED.clearDisplay(); OLED.setCursor(0, 0); OLED.setTextSize(1); OLED.display(); } // 時計のコイルを指定されたパルスで駆動 void pm_Home() { // パルスモータードライブのホームポジション if ((pv + DEAD_BAND) < sv) { // 設定値が現在値+デッドバンド以上だったら cw = !cw; // 駆動フラグを反転 digitalWrite(COIL_P, cw); // 指定された極性でコイルを駆動 digitalWrite(COIL_N, !cw); digitalWrite(LED_PIN, HIGH); // 針の移動中はLED点灯(T2の頭で消灯) Timer1.setPeriod(T1); // T1の時間経過後、 Timer1.attachInterrupt(pm_T2); // T2の処理を起動 } else if ((pv - DEAD_BAND) > sv) { // 設定値が現在値−デッドバンド以下だったら cw = !cw; // 駆動フラグを反転 digitalWrite(COIL_P, cw); // 指定された極性でコイルを駆動 digitalWrite(COIL_N, !cw); digitalWrite(LED_PIN, HIGH); // 針の移動中はLED点灯(T5の頭で消灯) Timer1.setPeriod(T3); // T3の時間経過後、 Timer1.attachInterrupt(pm_T4); // T4の処理開始 } else { // 誤差がデッドバンド内だったら、 Timer1.setPeriod(20000); // 何もしないでちょっと待って Timer1.attachInterrupt(pm_Nop); // Nop経由でホームポジションに戻る } } void pm_T2() { // T2の処理 digitalWrite(COIL_P, LOW); digitalWrite(COIL_N, LOW); digitalWrite(LED_PIN, LOW); // LED消灯 Timer1.setPeriod(T2); // T2の時間経過後、 Timer1.attachInterrupt(pm_Home); // ホームポジションへ戻る pv++; // 移動完了したので現在値を+1 } void pm_T4() { // T4の処理 digitalWrite(COIL_P, !cw); // 指定極性で駆動 digitalWrite(COIL_N, cw); Timer1.setPeriod(T4); // T4の時間経過後、 Timer1.attachInterrupt(pm_T5); // T5の処理開始 } void pm_T5() { // T5の処理 digitalWrite(COIL_P, LOW); // 指定極性で駆動 digitalWrite(COIL_N, LOW); digitalWrite(LED_PIN, LOW); // LED消灯 Timer1.setPeriod(T5); // T5の時間経過後、 Timer1.attachInterrupt(pm_Home); // ホームポジションへ戻る pv--; // 移動完了したので現在位置を-1 } void pm_Nop() { Timer1.setPeriod(20000); // ちょっと待って Timer1.attachInterrupt(pm_Home); // ホームポジションへ戻る } void timer2_IRQ() { // MsTimer2割込み timerFlag = true; // 割込みが入ったのでフラグをセット }