/* MH-Z19C CO2ガスモニタ 20210802MH-Z19C_CO2meter-VS V0.94 機能:測定結果をOLEDに表示、グラフ表示、ゼロ調の自動/マニュアル切り替え グラフレンジ切替・設定、スクリーンセーバー、アナログ表示、 アナログ指針移動でLED点滅(ヒステリシスアルゴリズム修正) ラジオペンチ 2021/8/2 http://radiopench.blog96.fc2.com/ スケッチ:20738使用,グローバル変数813バイト,ローカル変数1235バイトフリー */ #include #include // 0.96インチOLED使用 #include // グラフの記録レンジの保存に使用 #include // 測定周期の設定に使用 #include // CO2センサとのインターフェイスに使用 #include // 時計の針の駆動パルス作成に使用 #include #define MODE_PIN 2 // モード指定ピン #define ENT_PIN 3 // enterボタン #define SEL_PIN 4 // selectボタン #define COIL_P 5 // 時計のモーターのコイル-P #define COIL_N 6 // 時計のモーターのコイル-N #define G_LED_PIN 8 // CO2濃度増加表示(緑色LED) #define R_LED_PIN 9 // CO2濃度増加表示(赤色LED) #define RX_PIN 10 // MH-Z19Cとのシリアル通信 RX #define TX_PIN 11 // 同上 TX #define BUILTIN_LED 13 // ボード内蔵LED #define BAUDRATE 9600 // MH-Z19の通信ボーレート(固定値) #define T0 1000 // 測定インターバル(ms単位で指定、最小値は1000=1sec) #define DISP_ON_TIME 3600 // 表示点灯時間(消灯までの時間) #define SHORT_ON_TIME 10 // 短時間表示点灯時間(秒)アナログ指針の位置が変化した時に #define WAKEUP_VALUE 1000 // 表示を強制ONにするCO2濃度(ppm) #define T1 20000 // 順回転パルス幅(us) #define T2 60000 // 順回転パルス間隔 #define T3 5800 // 逆回転第一パルス幅 #define T4 8000 // 逆回転第二パルス幅 #define T5 120000 // 逆回転パルス間隔 #define DEAD_BAND 10 // 運針の不感帯幅のパルス数(=OLED消灯条件)(CO2 ppm濃度の1/1.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 mesV; // CO2の測定結果の生データ unsigned int calib; // キャリブレーションモード int dispTimer = DISP_ON_TIME; // 消灯までの残り時間 int dddTimer = SHORT_ON_TIME; // 変化検知表示タイマー(Diffarence Detect Display timer) 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個) char chrBuff[6]; // 文字列操作バッファ int latestData; int dataMin = 400; // 最初のグラフ作図まではこの値で目盛を表示 int dataMax = 400; // int tCount = 0; // グラフ更新タイマー unsigned int rNum = 0; // レンジ番号(rangeNumber) 0-11 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(ENT_PIN, INPUT_PULLUP); // ENTERボタン pinMode(SEL_PIN, INPUT_PULLUP); // SELECTボタン pinMode(COIL_P, OUTPUT); // 時計のコイル ポジ pinMode(COIL_N, OUTPUT); // 時計のコイル ネガ pinMode(R_LED_PIN, OUTPUT); // 赤色LED pinMode(G_LED_PIN, OUTPUT); // 緑色LED pinMode(BUILTIN_LED, OUTPUT); // LEDピン pinMode(RX_PIN, INPUT); // ソフトシリアル用 pinMode(TX_PIN, OUTPUT); // ソフトシリアル用 mySerial.begin(9600); // ソフトウエアシリアルのボーレート(MH-Z19Cの固定値) digitalWrite(BUILTIN_LED, 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(ENT_PIN) == LOW) { // 起動時にENTボタンが押されていたら rangeSet(); // レンジ設定を実行 } rNum = EEPROM.read(0); // EEPROMからレンジ番号を読み出す OLED.setCursor(10, 12); OLED.print(F("CO2 monitor V0.94")); // 開始メッセージ OLED.display(); modeSet(); // ゼロ調モード設定 measure(); // 1回読み飛ばす pv = clockPosition(400); // 起動時は指針が400ppmの位置にあるものと見なす sv = pv; // 針の位置は合っていることにする MsTimer2::set(T0, timer2_IRQ); // 測定インターバル割り込み設定 MsTimer2::start(); // 測定インターバル割り込み開始 Timer1.initialize(T1); // 適当な時間を空けて Timer1.attachInterrupt(pm_Home); // パルスモーター制御のホームポジション起動 digitalWrite(BUILTIN_LED, LOW); // setup処理が正常終了 } void loop() { while (timerFlag != true) { // タイマー割込みフラグが立つまで待機 if (digitalRead(ENT_PIN) == LOW) { // 待機中にENTERボタンが押されていたら entPushed = true; // ENTERボタンが押されたフラグを立てる } } timerFlag = false; // 次回割り込み用にフラグを戻しておく if (entPushed == true) { // 途中でENTボタンが押されて、 if (dispTimer > 1) { // まだ表示中だったら dispTimer = 0; // 表示を消す } else { // 表示中では無かったら(消灯中なら) dispTimer = DISP_ON_TIME; // 表示タイマーを元に戻す(点灯させる) } } entPushed = false; // ENTボタンフラグを戻す measure(); // CO2濃度測定(エラーチェックは省略),測定結果はmesVに入る co2Val = ave4(mesV); // 測定結果(mesV)をアベレージした値をCO2の濃度として使う cpx = clockPosition(co2Val); // 時計の針の位置を計算 cli(); // 不可分処理のために割り込み禁止 sv = cpx; // パルスモーターの目標位置に設定 sei(); // 設定完了したので割込み許可 latestData = co2Val; if (co2Val > WAKEUP_VALUE) { // CO2濃度が指定値以上だったら dispTimer = DISP_ON_TIME; // 表示時間タイマーをリセット(表示させる) } tCount++; // ログインターバルタイマーをインクリメント if (tCount == hRange[rNum].gInterval) { // ログの指定時刻だったら saveBuff(); // 測定結果をバッファに保存 tCount = 0; } if (dispTimer > 0 || dddTimer > 0) { // 表示タイマーと変動検知表示タイマーのどちらかがゼロでなければ disp(); // OLEDにグラフを表示 } else { // 両方ゼロだったら、 OLED.clearDisplay(); // OLED消灯(スクリーンセーバー) OLED.display(); } dispTimer--; // 表示タイマーの値をデクリメントして、 if (dispTimer < 0) { // 負の値になっていたら dispTimer = 0; // ゼロに戻す } dddTimer--; // 変動検知表示タイマーの値をデクリメントして、 if (dddTimer < 0) { // 負の値になっていたら dddTimer = 0; // ゼロに戻す } } void modeSet() { if (digitalRead(MODE_PIN) == LOW) { // スタート時にモードピンがLOWだったら calib = 0; // マニュアルモード(Autoゼロ調無し) } else { // そうでなければ calib = 1; // ゼロ調はAutoモード(デフォルト状態) } setCalMode(calib); // ゼロ調モードを設定 0:手動、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) { // 正常に読めていたら mesV = RetVal[2] * 256 + RetVal[3]; // CO2濃度を計算 err = 0; } else { mesV = 399; // 正常でなければこの値を返して誤魔化す err = 1; } return err; } int ave4(int x) { // 過去4回のデーターの平均値を計算して返す static int d[] = {400, 400, 400, 400}; // 配列の初期値は400、要素は4つ(0-3) int av = 0; d[3] = d[2]; // 配列の中身を1つ後ろにずらす(変数節約のためにベタに記述) d[2] = d[1]; d[1] = d[0]; d[0] = x; // 先頭には最新値を保存 av = (d[0] + d[1] + d[2] + d[3]) / 4; // 平均値を求め return av; // 戻り値として返す } 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後は再度リセットしないと測定結果が読めない 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); // 2倍角文字で 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); // OLED.setCursor(102, 8); OLED.print(sv); // アナログ指針の位置を表示 } 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倍して文字列3文字に変換 OLED.print(chrBuff); // OLEDに転送 OLED.print(hRange[rNum].scaleC); // 単位文字 OLED.setCursor(49, 57); // 時間スケール中央 sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -2); // -2倍して文字列3文字に変換 OLED.print(chrBuff); OLED.print(hRange[rNum].scaleC); OLED.setCursor(79, 57); // 時間スケール右 sprintf(chrBuff, "%+3d", (hRange[rNum].scaleV) * -1); // -1倍して文字列3文字に変換 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); // fromのプロット座標へ変換 y2 = map(dataBuff[i + 1], dataMin, dataMax, 55, 16); // forのプロット座標へ変換 OLED.drawLine(120 - i, y1, 119 - i, y2, WHITE); // 折れ線でCO2濃度グラフを描画 } } void rangeSet() { // グラフレンジの設定 unsigned int d; d = EEPROM.read(0); // EEPROM二保存しておいたレンジ番号を読み出し、 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(ENT_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(ENT_PIN) == HIGH) { // ENTERボタンが押されるまで if (digitalRead(SEL_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(SEL_PIN) == LOW) { // SELECTボタンが離されるまで待つ } delay(30); } } EEPROM.write(0, d); // レンジ番号をEEPROMに保存 OLED.clearDisplay(); OLED.setCursor(0, 0); OLED.setTextSize(1); OLED.display(); } // 時計のコイルを指定されたパルスで駆動(TimerOneの割込みでパルスを作成) void pm_Home() { // パルスモータードライブのホームポジション static int dx = 0; // 誤差量の設定値(初期値は誤差ゼロモード) static boolean greenLED = false; // 緑色LED点灯フラグ static boolean redLED = false; // 赤色LED点灯フラグ digitalWrite(R_LED_PIN, redLED); // 現在のフラグの状態を反映させて赤LED点灯 digitalWrite(G_LED_PIN, greenLED); //                緑 // デッドバンドを動的に変更 if (abs(sv - pv) > DEAD_BAND) { // 差がデッドバンドの範囲外だったら dx = 0; // デッドバンド無しで運転 } else if (sv == pv) { // 誤差ゼロなら(注:範囲内でも誤差ゼロ以外はこれまでのモードを継続) dx = DEAD_BAND; // デッドバンド付きで運転 } // 指定モードで駆動パルス発生 if ((pv + dx) < sv) { // 現在値+デッドバンドが設定値より小さかったら、CW方向に回転 greenLED = false; // 緑LED点灯 redLED = true; // 赤LED消灯 cw = !cw; // 駆動フラグを反転 digitalWrite(COIL_P, cw); // コイル駆動、ポジ側 digitalWrite(COIL_N, !cw); // コイル駆動、ネガ側 digitalWrite(R_LED_PIN, redLED); // UP-LED(赤)点灯 digitalWrite(G_LED_PIN, greenLED); // Down-LED(緑)消灯 digitalWrite(BUILTIN_LED, HIGH); // 動作確認用に基板のLEDを点灯 dddTimer = SHORT_ON_TIME; // 表示短時間点灯タイマー設定 Timer1.setPeriod(T1); // T1の時間経過後、 Timer1.attachInterrupt(pm_T2); // T2の処理を起動 } else if ((pv - dx) > sv) { // 現在値-デッドバンドが設定値より多きかったら、CCW方向に回転 greenLED = true; redLED = false; cw = !cw; // 駆動フラグを反転 digitalWrite(COIL_P, cw); // コイル駆動、ポジ側 digitalWrite(COIL_N, !cw); // コイル駆動、ネガ側 digitalWrite(R_LED_PIN, redLED); // UP LED消灯(T2の頭で消灯) digitalWrite(G_LED_PIN, greenLED); // Down LED点灯(T5の頭で消灯) digitalWrite(BUILTIN_LED, HIGH); // 動作確認用に基板のLEDを点灯 dddTimer = SHORT_ON_TIME; // 短時間点灯タイマー設定 Timer1.setPeriod(T3); // T3の時間経過後、 Timer1.attachInterrupt(pm_T4); // T4の処理開始 } else { // 誤差がデッドバンド内もしくはゼロだったら、 Timer1.setPeriod(20000); // 何もしないでちょっと待って Timer1.attachInterrupt(pm_Nop); // Nop経由でホームポジションに戻る } } void pm_T2() { // T2の処理(CWパルスoff期間) digitalWrite(COIL_P, LOW); // コイル駆動停止 digitalWrite(COIL_N, LOW); // コイル駆動停止 digitalWrite(R_LED_PIN, LOW); // LED消灯 digitalWrite(BUILTIN_LED, LOW); // 動作確認LED消灯 Timer1.setPeriod(T2); // T2の時間経過後、 Timer1.attachInterrupt(pm_Home); // ホームポジションへ戻る pv++; // 移動完了したので現在値を+1 } void pm_T4() { // T4の処理(CCWの第二パルス期間) digitalWrite(COIL_P, !cw); // 指定極性で駆動 digitalWrite(COIL_N, cw); // 指定極性で駆動 Timer1.setPeriod(T4); // T4の時間経過後、 Timer1.attachInterrupt(pm_T5); // T5の処理開始 } void pm_T5() { // T5の処理(CCWパルスのoff期間) digitalWrite(COIL_P, LOW); // コイル駆動停止 digitalWrite(COIL_N, LOW); // コイル駆動停止 digitalWrite(G_LED_PIN, LOW); // LED消灯 digitalWrite(BUILTIN_LED, 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; // 割込みが入ったのでフラグをセット }