/* ソーラータイマースイッチ (日の出・日の入りタイマー) リアルタイムクロックRTC8564BNと秋月のI2C液晶(16文字2行)を使用 日付と経緯度から日の出・日の入り時刻を計算 待機時はCPUをパワーダウンモードでスリープさせ省エネ。 RTCの時刻合わせ機能付き(RTC8564ライブラりは修正が必要) 2016/7/1 ラジオペンチ http://radiopench.blog96.fc2.com/ */ #include // 待機中はスリープさせるために使用 #include // 数学ライブラリ https://www.arduino.cc/en/Math/H #include // Wireライブラリ #include // N. MitsunagaさんのI2C液晶ライブラリ http://n.mtng.org/ele/arduino/i2c.html #include // なんでも作っちゃうかもさんのRTCライブラリ http://arms22.blog91.fc2.com/blog-entry-232.html #define M_PI 3.14159265358979323846 // πの値 #define DEG(a) ((a) * 180 / M_PI) // ラジアンを度に変換するマクロ #define RAD(a) ((a) * M_PI /180) // 度をラジアンに変換するマクロ I2CLiquidCrystal lcd(30, true); // コントラスト(0-63),液晶電源(true=5V, false=3.3V) char dtString[20] = "yyyy/mm/dd hh:mm:ss"; // テンプレート用文字列で初期化 byte RtcADR = 0x51; int selB = 8; // SelectボタンのピンNo. int entB = 9; // EnterボタンのピンNo. int nightOutPin = 10; // 夜間固定出力ピン float t1; // 日の入り時刻(単位:時) float t2; // 日の入り時刻(単位:時) float Longitude = 139.7414; // 経度(東経を入力、値は東京) float Latitude = 35.6581; // 緯度(北緯を入力) int mNumNow; // 分で数えた現在時刻 int mNumT1, mNumT2; // 分で数えた日の出・日の入り時刻 void setup() { pinMode(2, INPUT_PULLUP); // RTCの /INT信号を接続 RTC側はオープンドレインなのでプルアップ pinMode(10, OUTPUT); // Night On 出力 (日没から日の出までHigh) pinMode(11, OUTPUT); // プログラム出力 pinMode(13, OUTPUT); // 動作表示LED pinMode(selB, INPUT_PULLUP); // Selectボタン pin8 pinMode(entB, INPUT_PULLUP); // Enterボタン pin9 Serial.begin(115200); lcd.begin(16, 2); // 16文字2行のI2C液晶を使用 Rtc.begin(); if (digitalRead(entB) == LOW) { // リセット時にEnterボタンが押されていたら ClockSet(); // RTCの時刻を合わせる } if (digitalRead(selB) == LOW) { // リセット時にselectボタンが押されていたら dispLoc(); // 経緯度を表示 for (;;) { // 無限ループに入る(リセットで抜ける) } } // AdjustRtcTime(); // 時刻を決め打ちで合わせる時に使う SetRtcTimer(); // NB8564の設定(1秒周期でINT発生) set_sleep_mode(SLEEP_MODE_PWR_DOWN); // スリープする時はパワーダウンモードを使用 } void loop() { waitExtIRQ(); // RTCからの1秒パルスを待つ Rtc.available(); // RTC convString(); // RTCの時刻(BCD)を読んで文字列に変換しdtStringに格納 Serial.println(dtString); // シリアルに書き出し dispClockNorm(); // 液晶表示 1行目に mm/dd hh:mm:dd dispSunRiseSet(); // 日の出・日の入り時刻の計算と表示 fixTimer(); // 固定タイマー実行 progTimer(); // これから作成 Serial.flush(); } void waitExtIRQ() { ADCSRA &= ~(1 << ADEN); // ADENビットをクリアしてADCを停止(120μA節約) attachInterrupt(0, trigerd, FALLING); // Pin2のネガエッジで割込み発生するように指定 sleep_mode(); // ここで指定モードでスリープ開始 detachInterrupt(0); // スリープ終了 ADCSRA |= (1 << ADEN); // ADC動作再開 } void trigerd() { // 割込み発生時の処理 } void convString() { // RTCの2バイトのBCDを文字に変換し時刻の文字列を作る byte x; mNumNow = 0; dtString[0] = '2'; dtString[1] = '0'; x = Rtc.years(); dtString[2] = upper2chr(x); dtString[3] = lower2chr(x); x = Rtc.months(); dtString[5] = upper2chr(x); dtString[6] = lower2chr(x); x = Rtc.days(); dtString[8] = upper2chr(x); dtString[9] = lower2chr(x); x = Rtc.hours(); dtString[11] = upper2chr(x); dtString[12] = lower2chr(x); mNumNow = ((x & 0xF0) >> 4 ) * 600 + (x & 0x0F) * 60; // 分の連番計算 x = Rtc.minutes(); dtString[14] = upper2chr(x); dtString[15] = lower2chr(x); mNumNow += ((x & 0xF0) >> 4) * 10 + (x & 0x0F); // 分の連番計算 x = Rtc.seconds(); dtString[17] = upper2chr(x); dtString[18] = lower2chr(x); } void fixTimer() { // 固定タイマー(日没から日の出までON) // Serial.print(mNumNow);Serial.print(", ");Serial.print(mNumT1);Serial.print(", ");Serial.println(mNumT2); if( (mNumNow < mNumT1) || ( mNumT2 <= mNumNow) ){ // 現在時刻がt1以前、もしくはt2と同じか以降だったらON digitalWrite(nightOutPin, HIGH); } else { digitalWrite(nightOutPin, LOW); } } void progTimer(){ // プログラムタイマー } void dispClockNorm() { // 液晶にRTCの内容を表示 lcd.clear(); lcd.setCursor(0, 0); if (digitalRead(selB) == HIGH) { // selectボタンが押されていないと for (int n = 5; n <= 9; n++) { // mm/dd(通常表示) lcd.print(dtString[n]); } } else { // selectボタンが押されていたら for (int n = 0; n <= 4; n++) { // yyyy(西暦表示) lcd.print(dtString[n]); } } lcd.print(" "); // 1文字スペース for (int n = 11; n <= 18; n++) { // hh:mm:ss lcd.print(dtString[n]); } } void dispSunRiseSet() { // 日の出・日の入り時刻を計算して表示 int mm, dd, n, i; mm = (dtString[5] & 0xf) * 10 + (dtString[6] & 0xf); dd = (dtString[8] & 0xf) * 10 + (dtString[9] & 0xf); n = dayOfYear(mm, dd); // 日付連番を求める t1 = SunRiseTime(Longitude, Latitude, n); // 経緯度、日付連番から日の出時刻を求める t2 = SunSetTime(Longitude, Latitude, n); // 経緯度、日付連番から日の入り出時刻を求める lcd.setCursor(0, 1); lcd.print("Dtime "); // 2行目の先頭から書き始める i = (t1 * 60.0) + 0.5; // t1を分の値で計算(四捨五入) mNumT1 = i; lcd.print(i / 60); lcd.print(":"); // 時: lcd.print((i % 60) / 10); // 分の10の位 lcd.print((i % 60) % 10); // 分の1の位 lcd.print("-"); i = (t2 * 60.0) + 0.5; // t2を分の値で計算(四捨五入) mNumT2 = i; lcd.print(i / 60); lcd.print(":"); // 時: lcd.print((i % 60) / 10); // 分の10の位 lcd.print((i % 60) % 10); // 分の1の位 } char upper2chr(byte x) { // 上位4ビットをAsciiコードに変換 return (x >> 4) + 0x30; } char lower2chr(byte x) { // 下位4ビットをAsciiコードに変換 return (x & 0x0f) + 0x30; } void AdjustRtcTime() { // 時計の時刻決め打ち用。数値は修正要 byte date_and_time[7]; date_and_time[0] = 0x00; // 0秒 date_and_time[1] = 0x40; // 40分 date_and_time[2] = 0x11; // 11時 date_and_time[3] = 0x01; // 1日 date_and_time[4] = 0x04; // X曜日 date_and_time[5] = 0x02; // 2月 date_and_time[6] = 0x16; // 16年 Rtc.sync(date_and_time); } void SetRtcTimer() { // RTCの設定 Wire.beginTransmission(RtcADR); Wire.write(0x0e); // タイマー停止 Wire.write(0x00); Wire.endTransmission(); Wire.beginTransmission(RtcADR); Wire.write(0x01); // コントロールレジスタ-2の設定 Wire.write(0x11); // TI/TP=on, TIE=on Wire.endTransmission(); Wire.beginTransmission(RtcADR); Wire.write(0x0f); // タイマー設定値 Wire.write(0x01); // 1回毎 Wire.endTransmission(); Wire.beginTransmission(RtcADR); Wire.write(0x0e); // タイマースタート Wire.write(0x82); // TEビットon, カウンタソースクロックは1秒を設定 Wire.endTransmission(); } void ClockSet() { // 時計合わせ int nen, tsuki, dayCount; lcd.setCursor(0, 0); lcd.print("Clock set!"); // モード開始メッセージ lcd.setCursor(0, 1); lcd.print("Sel, Ent"); // SElectで選択、Enterで決定 while (digitalRead(entB) == LOW) { // EnterスイッチがOFFになるまで待つ } delay(30); Rtc.available(); // 現在の時刻を取得 convString(); // RTCの時刻(BCD)を文字列に変換してdtStringに格納 dtString[17] = '0'; dtString[18] = '0'; // 但し秒はゼロに設定 dispClockAdj(); // 時刻合わせモードで表示 //データ位置と値の範囲を指定して数値設定プログラムを呼ぶ setV(3, 15, 35); // 年(2015〜2035年まで) setV(6, 1, 12); // 月 // 月の大小と閏年を反映した日数で呼ぶ nen = (dtString[2] & 0x0f) * 10 + (dtString[3] & 0x0f); tsuki = (dtString[5] & 0x0f) * 10 + (dtString[6] & 0x0f); dayCount = 31; // 普通の月は31日まで if ((tsuki == 4) | (tsuki == 6) | (tsuki == 9) | (tsuki == 11)) { dayCount = 30; // 4,6,9,11月は30日まで } if (tsuki == 2) { // 2月は dayCount = 28; // 普通は28日まで if ((nen % 4) == 0) { // うるう年だったら dayCount = 29; // 29日まで } } setV(9, 1, dayCount); // 日 setV(12, 0, 23); // 時 setV(15, 0, 59); // 分を合わせたら秒はゼロで時計あわせ updateClock(); // RTCに日時を設定 } // Set ボタンが押される毎にdtStringの指定位置の値をインクリメント、液晶を更新 // Eent ボタンが押されたら値を確定して戻る void setV(int p, int minV, int maxV) { // p:ポインタ、minV:最小値、maxV:最大値 int xx; char upper; char lower; lcd.setCursor(p % 11, p / 11); // 液晶のカーソルを移動 lcd.cursor(); // カーソル表示 xx = (dtString[p - 1] & 0x0f) * 10 + (dtString[p] & 0x0f); //文字列から初期値を計算。BCDなので上位は10倍 while (digitalRead(entB) == LOW) { // entBがOFFになるまで待つ delay(30); } while (digitalRead(entB) == HIGH) { // entBがNOでなければ以下の処理行う if (digitalRead(selB) == LOW) { delay(30); // selBが押された! xx++; // xxをインクリメント if (xx >= maxV + 1) { // 上限超えてたらゼロに戻す xx = minV; } upper = (xx / 10) | 0x30; // 上位をASCIIへ変換 lower = (xx % 10) | 0x30; // 下位をASCIIへ変換 lcd.setCursor((p - 1) % 11, (p - 0) / 11); lcd.print(upper); // 表示更新 lcd.print(lower); lcd.setCursor((p - 0) % 11, (p - 0) / 11); // 液晶のカーソルを元の位置に戻す dtString[p - 1] = upper; // データ文字列更新 dtString[p] = lower; while (digitalRead(selB) == LOW) { // selBが離されるまで待つ } delay(30); } } delay(30); } void updateClock() { // RTCに値をセット byte data[7]; int yy, mm, dd, ww; yy = (dtString[2] & 0xf) * 10 + (dtString[3] & 0xf) + 2000; mm = (dtString[5] & 0xf) * 10 + (dtString[6] & 0xf); dd = (dtString[8] & 0xf) * 10 + (dtString[9] & 0xf); if ( mm <= 2) { // ツェラーの式で曜日を求める mm += 12; yy--; } ww = (yy + yy / 4 - yy / 100 + yy / 400 + (13 * mm + 8) / 5 + dd) % 7; for (int n = 0; n <= 15; n++) { // デバッグ用 Serial.print(dtString[n]); } Serial.print(" Week= "); Serial.println(ww); // print week No. delay(200); data[0] = 0x00; // 00秒にリセット data[1] = ((dtString[14] & 0xf) << 4) | (dtString[15] & 0x0f); // 分 data[2] = ((dtString[11] & 0xf) << 4) | (dtString[12] & 0x0f); // 時 data[3] = ((dtString[8] & 0xf) << 4) | (dtString[9] & 0x0f); // 日 data[4] = ww; // 曜日 data[5] = ((dtString[5] & 0xf) << 4) | (dtString[6] & 0x0f); // 月 data[6] = ((dtString[2] & 0xf) << 4) | (dtString[3] & 0x0f); // 年 Rtc.sync(data); } void dispClockAdj() { // 時刻合わせモードの表示 lcd.clear(); lcd.setCursor(0, 0); // 1行目に、 for (int n = 0; n <= 9; n++) { // yyyy/mm/dd lcd.print(dtString[n]); } lcd.setCursor(0, 1); // 2行目に、 for (int n = 11; n <= 16; n++) { // hh:mm lcd.print(dtString[n]); } lcd.print("**"); // ssにはゼロが入る } float SunRiseTime(float x, float y, int n) { // 日の出時刻を求める関数 float d, e, t; y = RAD(y); // 緯度をラジアンに変換 d = dCalc(n); // 太陽赤緯を求める e = eCalc(n); // 均時差を求める // 太陽の時角幅を求める(視半径、大気差などを補正 (-0.899度) ) t = DEG(acos( (sin(RAD(-0.899)) - sin(d) * sin(y)) / (cos(d) * cos(y)) ) ); return ( -t + 180.0 - x + 135.0) / 15.0 - e; // 日の出時刻を返す } float SunSetTime(float x, float y, int n) { // 日の入り時刻を求める関数 float d, e, t; y = RAD(y); // 緯度をラジアンに変換 d = dCalc(n); // 太陽赤緯を求める e = eCalc(n); // 均時差を求める // 太陽の時角幅を求める(視半径、大気差などを補正 (-0.899度) ) t = DEG(acos( (sin(RAD(-0.899)) - sin(d) * sin(y)) / (cos(d) * cos(y)) ) ); return ( t + 180.0 - x + 135.0) / 15.0 - e; // 日の入り時刻を返す } float dCalc(int n) { // 近似式で太陽赤緯を求める float d, w; w = (n + 0.5) * 2 * M_PI / 365; // 日付をラジアンに変換 d = + 0.33281 - 22.984 * cos(w) - 0.34990 * cos(2 * w) - 0.13980 * cos(3 * w) + 3.7872 * sin(w) + 0.03250 * sin(2 * w) + 0.07187 * sin(3 * w); return RAD(d); // 赤緯を返す(単位はラジアン) } float eCalc(int n) { // 近似式で均時差を求める float e, w; w = (n + 0.5) * 2 * M_PI / 365; // 日付をラジアンに換算 e = + 0.0072 * cos(w) - 0.0528 * cos(2 * w) - 0.0012 * cos(3 * w) - 0.1229 * sin(w) - 0.1565 * sin(2 * w) - 0.0041 * sin(3 * w); return e; // 均一時差を返す(単位は時) } int dayOfYear(int mm, int dd) { // 月日から年間の経過日数を求める int daySum[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; //前月末の通算日(うるう年は無視) return daySum[mm - 1] + dd - 1; // 経過日数を返す(1月1日=0) } void dispLoc() { // 経緯度表示 lcd.clear(); lcd.print("E "); lcd.print(Longitude, 4); // 東経の値 lcd.setCursor(0, 1); lcd.print("N "); lcd.print(Latitude, 4); // 北緯の値 }