/* 水草水槽のCO2発生量コントロール V3.2 過去のショット数をEEPROMに保存するように変更  スケッチが22988バイト(71%)を使っています。  グローバル変数が802バイト(39%)を使っていて、ローカル変数で1246バイト使うことができます。 2019/5/6 版 */ #include // 時間はタイマー割り込みで刻む #include // adafruitのライブラリを使用 #include #include #include #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define STRING_L 4 // 合計表示文字数 #define AFTER_DECIMAL_L 0 // 小数点以下の表示桁数 #define UNIT_CHR "(mmAq)" #define k1 931.1 // 1V当たりの圧力(mmAqの値)、要調整 #define zeroPressV 0.5 // 気圧ゼロの時の電圧 #define graphIntervalShort 15 // 25分記録のインターバル #define SHORT_SCALE "25min" #define graphIntervalLong 1080 // 30時間記録のインターバル #define LONG_SCALE "30Hr " #define decPin 9 // -ボタン(disp Time length) #define incPin 10 // + ボタン(1-shot) #define entPin 11 // enter ボタン int secCount = 0; int tLong = 0; // ロングインターバル記録用経過時間 int tShort = 0; // ショートインターバル記録用経過時間 int pumpOnTime; // ポンプON時間 (単位:ms)(最小滴下量以上に設定すること) int pumpInterval; // ポンプ駆動周期 int nightTargetPress; // 夜間の圧力設定値(mmAq単位で設定) int lastDayNightFlag; // 前回の昼夜フラグ int dayNightFlag; // 現在の昼夜フラグ int ss; // ビルドアップショット数 int ssInterval; // ビルドアップショット間隔(単位:ms) int mmAq; // mmAq単位の気圧(表現出来る範囲は±3.2kg/cm2) int dayShotCount = 0; // 昼間のショット数 int nightShotCount = 0; // 夜間のショット数 int nightCheckCount = 0; // 夜間のショット要否チェックの回数 int pressLog[2][100]; // 圧力ログデーター(2種、100データ) // int pressLog[2][80]; // 圧力ログデーター(2種、100データ) int dataMin; int dataMax; int hScale = 1; // 水平スケール(0:25分、1:30Hrモード) char buff[5]; // 文字列操作バッファ(sprintfで使用) volatile int timeUpFlag = 0; // タイムアップフラグ(割込み) // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); void setup() { // Serial.begin(115200); // Serial.println(F("Auto CO2 injecter start v3.2")); // バージョン表示 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64 // Serial.println(F("SSD1306 allocation failed")); for (;;); // Don't proceed, loop forever } pinMode(6, OUTPUT); // ポンプ駆動信号 pinMode(7, OUTPUT); // 動作確認表示 pinMode(8, INPUT_PULLUP); // 昼夜信号(昼はLOW,夜はHIGH) pinMode(decPin, INPUT_PULLUP); pinMode(incPin, INPUT_PULLUP); pinMode(entPin, INPUT_PULLUP); pinMode(13, OUTPUT); display.setTextColor(WHITE); // 白文字で描く clearOLED(1); // 画面消去 startMessage(); // 開始画面表示 readEEPROM(); // EEPROMからパラメーターを読んで値を設定(ショットカウンタも可能な範囲で復元) delay(1000); if (digitalRead(entPin) == LOW) { // 起動時にEntボタンが押されていたら eraseShotLog(); // EEPROMに記録しているショット数ログをクリア } for (int i = 0; i <= 99; i++) { pressLog[0][i] = -9999; // バッファ0 を未定義フラグで埋める pressLog[1][i] = -9999; // バッファ1 を未定義フラグで埋める } shotCo2(); // 開始時に1ショット(リセット後は強制的に1ショット) mmAq = readPress(); // 読み捨て mmAq = readPress(); pressLog[0][0] = mmAq; // 開始時の値を記録 pressLog[1][0] = mmAq; setDayNight(); // 昼夜インターバルセット lastDayNightFlag = dayNightFlag; // フラグを記録 MsTimer2::set(1000, timeIntrp); // 1秒毎に timeIntrpをコール MsTimer2::start(); } void loop() { while (timeUpFlag == 0) { // 1秒間待つ間に下記を実行 if (digitalRead(incPin) == LOW) { // + ボタンが押されていたら shotCo2(); // CO2を1発発生 secCount = 0; delay(1000); } if (digitalRead(entPin) == LOW) { // entボタンが押されていたらパラメータ変更メニューを実行 paraSet(); delay(30); } if (digitalRead(decPin) == LOW) { // decボタンが押されていたら ショット時間、周期表示 logIntervalChange(); delay(30); } } // 1秒経過待ち中の処理end digitalWrite(7, HIGH); // 動作確認表示ON timeUpFlag = 0; // タイムアップフラグをクリア(割込みルーチンで1になっている) setDayNight(); // 昼夜情報を読んで必要なパラメーターを設定 if ((lastDayNightFlag == 1) && (dayNightFlag == 0)) { // 朝になった直後だったら(前回が夜で今回は昼だったら) buildUp(); // 内圧上昇させるために強制注入(ビルドアップ) EEPROM.write(22, nightShotCount >> 8); // 前夜のショット数をEEPROMに記録(上位) EEPROM.write(23, nightShotCount & 0x00FF); //                 (下位) EEPROM.write(24, nightCheckCount >> 8); // 前夜のチェック数をEEPROMに記録(上位) EEPROM.write(25, nightCheckCount & 0x00FF); //                 (下位) for (int i = 5; i >= 1; i--) { // ログの記録を1日後ろに転記 for (int j = 20; j <= 25; j++) { EEPROM.write(j + 6 * i, EEPROM.read(j + 6 * (i - 1))); } } for (int i = 20; i <= 25; i++) { // 日が変わったので本日の記録エリアをクリア EEPROM.write(i, 0); } dayShotCount = 0; nightShotCount = 0; nightCheckCount = 0; } if ((lastDayNightFlag == 0) && (dayNightFlag == 1)) { // 昼から夜になった直後なら EEPROM.write(20, dayShotCount >> 8); // 昼間のショット数をEEPROMに記録(上位) EEPROM.write(21, dayShotCount & 0x00FF); //              (下位) } lastDayNightFlag = dayNightFlag; // 次回のためにフラグを記録 secCount++; // 秒カウンタをインクリメント tLong++; tShort++; mmAq = readPress(); // 圧力を測定 dispInf(); // 画面表示 if (tShort >= graphIntervalShort) { // 短時間用データーを記録 saveLog(0); tShort = 0; } if (tLong >= graphIntervalLong) { // 長時間用データーを記録 saveLog(1); tLong = 0; } plotData(hScale); // 指定のトレンドグラフを表示 dispScaleValue(); // グラフの目盛りを表示 display.display(); if (secCount >= pumpInterval) { // 指定時間経過していたら secCount = 0; // 秒カウンタをリセット if (dayNightFlag == 0) { // 昼間なら shotCo2(); // ポンプ駆動 dayShotCount++; // 昼間用カウンタインクリメント } else { // 夜で、 nightCheckCount++; // 夜用圧力チェック回数カウンタインクリメント if ( mmAq < nightTargetPress ) { // 圧力が夜間目標値以下なら nightShotCount++; // 夜用ショットカウンタインクリメント shotCo2(); // 圧力維持のためにポンプ駆動 } } } delay(20); // LED点灯確認のために待つ digitalWrite(7, LOW); // 動作確認表示OFF } void dispInf() { // 1行目に各種情報を表示 clearOLED(0); // バッファだけ消す if (dayNightFlag == 1) { display.print(F("Night ")); // 夜表示 } else { display.print(F("Day ")); // 昼表示 } sprintf(buff, "%3d", pumpInterval - secCount); // 残り時間を3桁右詰め文字列に変換 display.print(buff); display.print(F("/")); sprintf(buff, "%3d", pumpInterval); display.print(buff); display.print(F("s ")); sprintf(buff, "%4d", mmAq); // 右詰め4文字で (マイナスは-nnn) display.print(buff); // 圧力を表示(mmAq単位) display.print(F("mm")); // } void plotData(int n) { // データーを折れ線グラフで表示 long y1, y2; int x; dataMin = 4000; dataMax = -500; writeScaleLines(); // 目盛り線書き込み for (int i = 0; i <= 99; i++) { // 最大と最小値の探索 x = pressLog[n][i]; if (x != -9999) { if (x > dataMax) { dataMax = x; } if (x < dataMin) { dataMin = x; } } } for (int i = 1; i <= 99; i++) { // 線で接続するために、先頭の次データから開始 if (pressLog[n][i] == -9999) { // データーが未定(-9999)なら break; // そこでプロット中止 } y1 = map(pressLog[n][i - 1], dataMin, dataMax, 63, 9); // y1をプロット座標へ変換(i番目のデーターが有効なのでその前は必ず有効) y2 = map(pressLog[n][i], dataMin, dataMax, 63, 9); // y2もプロット座標へ変換 display.drawLine(127 - i, y1, 126 - i, y2, WHITE); // 点間を線で結ぶ } } void dispScaleValue() { // グラフの目盛りを表示 float p1, p2, delta; p1 = dataMin; p2 = dataMax; delta = p2 - p1; dtostrf(p2, STRING_L, AFTER_DECIMAL_L, buff); // 書式設定 display.setCursor(0, 9); display.print(buff); // Max値表示 dtostrf(p1 + 2.0 * delta / 3.0, STRING_L, AFTER_DECIMAL_L, buff); // 書式設定 display.setCursor(0, 24); display.print(buff); // 2/3値表示 dtostrf(p1 + delta / 3.0, STRING_L, AFTER_DECIMAL_L, buff); // 書式設定 display.setCursor(0, 41); display.print(buff); // 1/3値表示 dtostrf(p1, STRING_L, AFTER_DECIMAL_L, buff); // 書式設定 display.setCursor(0, 57); display.print(buff); // Min値表示 display.fillRect(32, 48, 30, 9, BLACK); // 時間軸表示用に5文字分クリア display.setCursor(32, 49); if (hScale == 0 ) { display.print(F(SHORT_SCALE)); // 時間軸スケール表示 } else { display.print(F(LONG_SCALE)); } } void saveLog(int n) { // 指定チャンネルの圧力データを記録 int d; for (int i = 99; i >= 1; i--) { d = pressLog[n][i - 1]; // 配列からデーターを読んで、 pressLog[n][i] = d; // 一つ後ろにずらし } pressLog[n][0] = mmAq; // 先頭に最新データーを記録 } int readPress() { // 圧力を測定してmmAq単位の値で返す float p; // 圧力計算結果 long d; // ADC結果バッファ long sumd = 0; // ADC累積値 for (int n = 0; n < 892; n++) { // 892回で0.1秒(実測=100.06ms)、50Hz,60Hzで割り切れるので電源ノイズ低減が期待できる d = analogRead(0); // A0の値を読む sumd = sumd + d; // 平均処理のために累積 } // 圧力計算式 p = k1 * ((sumd * 5.0) / (892.0 * 1024.0) - zeroPressV ) + 0.5; return (int)p; // 結果をintで返す } void shotCo2() { // ポンプを指定時間駆動してCO2発生 clearOLED(1); display.setCursor(25, 25); display.print(F("Inject:")); sprintf(buff, "%3d", pumpOnTime); display.print(buff); // ポンプ時間表示 display.print(F("ms")); display.display(); // 実際に画面表示 digitalWrite(6, HIGH); // ポンプON digitalWrite(13, HIGH); delay(pumpOnTime); // 指定所間待って digitalWrite(6, LOW); // ポンプOFF digitalWrite(13, LOW); delay(50); // ちょっと待つ。表示が出るのは1秒後(次のサイクル) } void setDayNight() { if (digitalRead(8) == LOW) { // ピン8がLOWなら dayNightFlag = 0; // 昼間なのでフラグは 0 } else { dayNightFlag = 1; // 夜間なのでフラグは 1 } } void logIntervalChange() { clearOLED(1); display.setCursor(10, 20); display.print(F("Scale change")); display.display(); while (digitalRead(decPin) == LOW) { // Entボタンが離されるまで待つ } display.setCursor(10, 40); if (hScale == 0) { hScale = 1; display.print(F("to Long")); } else { display.print(F("to Short")); hScale = 0; } display.display(); delay(200); } void paraSet() { // 運転パラメーター設定(最後に現在のショット数を保存し、リセットに備える) int x; clearOLED(1); while (digitalRead(entPin) == LOW) { // Entボタンが離されるまで待つ } x = showShotLog(); delay(30); if (x != 1) { // ログ表示が強制終了されていなかったら以下を実行 setShotTime(); // ポンプの動作時間設定 setDayInterval(); // 昼間のインターバル setSShotN(); // モーニングスタートパルス数 setSShotI(); // モーニングスタートパルスの間隔 setNightTargetPress(); // 夜間圧力設定値 writeEEPROM(); // 設定値をEEPROMに書き込み。ついでに今日のショット数を保存 } } void setShotTime() { // ポンプ駆動時間設定 dispSettingsTitle(); display.print(F("Shot time")); display.setCursor(114, 16); display.print(F("ms")); //display.display(); pumpOnTime = oledRW(90, 16, pumpOnTime, 2, 4, 98); // step=5ms,min=5ms, max=95ms(2桁を超えると表示が壊れる) } void setDayInterval() { // 昼間のポンプ運転間隔設定 dispSettingsTitle(); display.print(F("Shot interval")); display.setCursor(114, 16); display.print(F("s")); //display.display(); pumpInterval = oledRW(90, 16, pumpInterval, 5, 10, 990); // step=5s,min=10s, max=999s } void setSShotN() { // モーニングスタートアップショット数設定 dispSettingsTitle(); display.print(F("Morning Shot")); ss = oledRW(90, 16, ss, 1, 0, 30); // 下限は0でこの場合ショットは無し } void setSShotI() { // モーニングスタートアップショットの時間間隔設定 dispSettingsTitle(); display.print(F("MS interval")); display.setCursor(114, 16); display.print(F("ms")); ssInterval = oledRW(90, 16, ssInterval, 100, 200, 2000); } void setNightTargetPress() { // 夜間圧力の設定値 dispSettingsTitle(); display.print(F("Night min.Press")); display.setCursor(114, 16); display.print(F("mm")); // mmAq単位 nightTargetPress = oledRW(90, 16, nightTargetPress, 20, 0, 500); // 20ステップで、最低1、最高500 } void dispSettingsTitle() { clearOLED(1); display.println(F("-Settings-")); display.println(); } int showShotLog() { int x; int t = 0; int flag = 0; clearOLED(1); display.println(F("-Shot log- ent. next")); // 1行目(タイトル) display.println(F(" Day Night")); display.print(F("Today ")); // 先頭はToday sprintf(buff, "%4d", dayShotCount); // 今日のショット数4桁 display.print(buff); display.print(F(" ")); // sprintf(buff, "%4d", nightShotCount); // 今日の夜のショット数4桁 display.print(buff); display.print(F("/")); sprintf(buff, "%4d", nightCheckCount); // 今日の夜の圧力チェック回数4桁 display.println(buff); for (int i = 1; i <= 5; i++) { display.print(F(" -")); // -i display.print(i); display.print(F(" ")); // 日付連番 x = EEPROM.read(20 + (i * 6)) * 256 + EEPROM.read(21 + (i * 6)); sprintf(buff, "%4d", x); // 昼間のショット数4桁 display.print(buff); display.print(F(" ")); // x = EEPROM.read(22 + (i * 6)) * 256 + EEPROM.read(23 + (i * 6)); sprintf(buff, "%4d", x); // 夜のショット数4桁 display.print(buff); display.print(F("/")); x = EEPROM.read(24 + (i * 6)) * 256 + EEPROM.read(25 + (i * 6)); sprintf(buff, "%4d", x); // 夜の圧力チェック回数4桁 display.println(buff); } display.display(); while (digitalRead(entPin) == HIGH) { // 表示したままEntボタンが押されるまで待つ delay(10); t++; if (t > 1000) { // 但し10秒間放置されたら、 flag = 1; // 戻り値を1にして break; // ループを抜ける } } while (digitalRead(entPin) == LOW) { // ボタンが離されるまで待つ } delay(30); return flag; } void eraseShotLog() { for (int i = 20; i < (20 + 6 * 6); i++) { // 36バイト分 EEPROM.write(i, 0); // クリア } clearOLED(1); // 画面を消して display.println(F("Shot Log erased")); // 確認用に画面表示 display.println(F("Reset to restart")); // 確認用に画面表示 display.display(); for (;;) {} } int oledRW(int x, int y, int d, int stepD, int minD, int maxD) { // OLEから値を入力 // OLEDの指定位置に4桁右詰めで変数の値を表示。ボタン操作で値を増減し、 // Ent入力で値を確定し戻り値として返す。表示位置の左上をx, y 座標で指定 // 引数:x座標、y座標、変更したい変数名、変更ステップ量、下限値、上限値 int t = 0; // 無操作タイマー while (digitalRead(entPin) == HIGH) { // enterボタンが押されるまで以下を実行 t++; delay(10); if (t >= 100) { // 操作無しが所定回数続いたら(約5秒) return d; // 入力中止 } display.fillRect(x, y, 24, 8, BLACK); // 指定座標から4文字分クリア display.setCursor(x, y); // カーソルを指定位置に合わせて sprintf(buff, "%4d", d); display.print(buff); display.display(); // 4桁右寄せで値を表示 if (digitalRead(incPin) == 0) { // + ボタンが押されていたら t = 0; // 操作があったのでタイマー延長 d = d + stepD; // x を指定ステップ増加 if (d > maxD) { d = maxD; } delay(30); while (digitalRead(incPin) == 0) { // ボタンが離されるまで待つ } delay(30); } if (digitalRead(decPin) == 0) { // - ボタンが押されていたら t = 0; // 操作があったのでタイマー延長 d = d - stepD; // x を指定ステップ減らす if (d < minD) { d = minD; } delay(30); while (digitalRead(decPin) == 0) { // ボタンが離されるまで待つ } delay(30); } } delay(30); while (digitalRead(entPin) == LOW) { // ent ボタンが離されるまで待つ } delay(30); return d; // 戻り値 } void clearOLED(int d) { // OLEDを消去してカーソルを先頭に戻す display.clearDisplay(); // VRAMだけクリア display.setCursor(0, 0); // カーソルを先頭に戻す if (d == 1) { display.display(); // 実際に画面を消す } } void writeScaleLines() { // グラフ用目盛り線の作図 display.drawFastVLine(26, 9, 55, WHITE); // 左縦線 display.drawFastHLine(25, 9, 3, WHITE); // 左端、Max値の補助マーク display.drawFastHLine(25, 63, 3, WHITE); display.drawFastHLine(46, 9, 3, WHITE); // 中間、Max値の補助マーク display.drawFastHLine(46, 63, 3, WHITE); display.drawFastHLine(66, 9, 3, WHITE); // 中間、Max値の補助マーク display.drawFastHLine(66, 63, 3, WHITE); display.drawFastHLine(86, 9, 3, WHITE); // 中間、Max値の補助マーク display.drawFastHLine(86, 63, 3, WHITE); display.drawFastHLine(106, 9, 3, WHITE); // 中間、Max値の補助マーク display.drawFastHLine(106, 63, 3, WHITE); display.drawFastHLine(126, 9, 2, WHITE); // 右端、Max値の補助マーク display.drawFastHLine(126, 63, 2, WHITE); for (int y = 26; y < 60; y += 19) { // 水平目盛り線を点線で2本書く for (int x = 25; x <= 128; x += 4) { display.drawFastHLine(x, y, 2, WHITE); } } for (int x = (127 - 20); x > 30; x -= 20) { // 縦目盛り線を点線で4本描く for (int y = 10; y < 63; y += 5) { display.drawFastVLine(x, y, 2, WHITE); } } } void startMessage() { // 開始画面表示 display.clearDisplay(); // 画面全消去(0.4ms) display.setTextSize(2); // 倍角表示(メモリーが足りれば別フォントにしたい) display.setCursor(0, 5); display.print(F(" CO2 Auto")); display.setCursor(0, 25); display.print(F("doser v3.2")); // バージョン表示 display.setCursor(0, 45); display.print(F(" start")); // バージョン表示 display.display(); display.setTextSize(1); //元のサイズに戻す } void readEEPROM() { // EEPROMからパラメーターを読む(異常値はデフォルトに置き換え) // 0,1:pumpOntime, 2,3:pumpInterval, 4,5:(未使用) // 6,7:SS、8,9:SSinterval, 10,11:nightTargetPress // 20,21:昼間のショット数、21,22:夜のショット数、22,23:夜の圧力チェック回数 int k; k = EEPROM.read(0) << 8 | EEPROM.read(1); // ポンプ運転時間 if ((k < 5) || ( 100 < k)) { // 5〜100の範囲外だったら k = 30; // 30msに設定 } pumpOnTime = k; k = EEPROM.read(2) << 8 | EEPROM.read(3); // 昼間のポンプ運転間隔 if ((k < 10) || ( 990 < k)) { // 10〜990の範囲外だったら k = 240; // 240秒に設定 } pumpInterval = k; k = EEPROM.read(6) << 8 | EEPROM.read(7); // モーニングショット数 if ((k < 0) || ( 30 < k)) { // 0〜30の範囲外だったら k = 10; // 10ショットに設定 } k = EEPROM.read(8) << 8 | EEPROM.read(9); // モーニングショット間隔 if ((k < 200) || ( 2000 < k)) { // 200〜2000の範囲外だったら k = 1500; // 1500msに設定 } ssInterval = k; k = EEPROM.read(10) << 8 | EEPROM.read(11); // 夜間圧力設定値 if ((k < 0) || ( 300 < k)) { // 0〜300mmAqの範囲外だったら k = 100; // 100mmAqに設定 } nightTargetPress = k; k = EEPROM.read(20) << 8 | EEPROM.read(21); // 本日昼間のショット数を復元 dayShotCount = k; k = EEPROM.read(22) << 8 | EEPROM.read(23); // 本日夜のショット数を復元 nightShotCount = k; k = EEPROM.read(24) << 8 | EEPROM.read(25); // 本日夜の圧力チェック回数を復元 nightCheckCount = k; } void writeEEPROM() { // EEPROMに設定値を保存 EEPROM.write(0, (pumpOnTime >> 8)); EEPROM.write(1, (pumpOnTime & 0xFF)); EEPROM.write(2, (pumpInterval >> 8)); EEPROM.write(3, (pumpInterval & 0xFF)); // 4.5は欠番 EEPROM.write(6, (ss >> 8)); EEPROM.write(7, (ss & 0xFF)); EEPROM.write(8, (ssInterval >> 8)); EEPROM.write(9, (ssInterval & 0xFF)); EEPROM.write(10, (nightTargetPress >> 8)); EEPROM.write(11, (nightTargetPress & 0xFF)); EEPROM.write(20, dayShotCount >> 8); // ついでに現時点のショット数を保存 EEPROM.write(21, dayShotCount & 0xFF); EEPROM.write(22, nightShotCount >> 8); EEPROM.write(23, nightShotCount & 0x0F); EEPROM.write(24, nightCheckCount >> 8); EEPROM.write(25, nightCheckCount & 0xFF); } void buildUp() { // ビルドアップショットを実行 clearOLED(1); display.print(F("Start Up")); // これ表示されていない for (int n = 0; n < ss; n++) { // 指定回数 shotCo2(); // ポンプ駆動 display.setCursor(3, 1); display.print(n + 1); display.print(F("/")); display.print(ss); display.print(F(" ")); delay(ssInterval); // 指定時間待つ } clearOLED(1); } void timeIntrp() { // MsTimer2 割込み処理 timeUpFlag = 1; // 1秒経過フラグON }