使ったもの
RTCモジュールは高精度のものではありませんが、どのみち一日数回NTPと時刻合わせするので、問題ないでしょう。
配線図
D1 mini ESP32 のピンは内側だけを使っています。RTCモジュールもTFT LCDも3.3Vで駆動できるので楽ですね。なお、ディスプレイからの配線は、下で記述するライブラリのヘッダファイルの変更とセットです。
プログラム
RTC用のライブラリは adafruit RTC lib を、TFT LCD用のライブラリは TFT_eSPI lib を利用しました。
TFT_eSPI の方は、インストール後にUser_Setup.h の201行目付近を、下記のように変更しています。
// ###### EDIT THE PIN NUMBERS IN THE LINES FOLLOWING TO SUIT YOUR ESP32 SETUP ######
// For ESP32 Dev board (only tested with ILI9341 display)
// The hardware SPI can be mapped to any pins#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 5 // Chip select control pin
#define TFT_DC 16 // Data Command control pin
//#define TFT_RST 4 // Reset pin (could connect to RST pin)
#define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST
プログラムの方はといえば、
- 約1/10秒毎にRTCから時刻を取得し、変化があったら表示を更新
- 起動時と0時、4時、8時、12時、16時、20時にNTPサーバで時刻合わせ
- NTP時刻合わせは、時刻合わせ中の表示と、時刻合わせをした時刻の表示をする
- デバッグ用に時刻合わせ前のRTCの値とNTPサーバから取得した時刻の表示をする
というものです。
#include <WiFi.h> #include "time.h" #include "sntp.h" #include "RTClib.h" #include "SPI.h" #include "TFT_eSPI.h" // NTP関連定義 const char* ssid = "(SSID)"; // SSID const char* password = "(PASSWORD)"; // パスワード const char* ntpServer1 = "ntp.nict.jp"; // NTPサーバ1 const char* ntpServer2 = "ntp.jst.mfeed.ad.jp"; // NPTサーバ2 const char* time_zone = "JST-9"; // タイムゾーン // RTCオブジェクト RTC_PCF8523 rtc; // TFT LCDオブジェクト TFT_eSPI tft = TFT_eSPI(); // グローバル変数 int old_sec = -1, old_day = -1; // 前回の日と秒 // 時刻合わせ後のコールバック関数 void timeavailable(struct timeval *t) { char buf[255]; struct tm timeinfo; // RTC時刻合わせ前の時刻表示(デバッグ用) DateTime rtctime=rtc.now(); tft.setCursor(10, 260); tft.setTextFont(1); tft.setTextSize(3); tft.setTextColor(TFT_WHITE, TFT_BLACK); sprintf(buf, "RTC=%02d:%02d:%02d", rtctime.hour(), rtctime.minute(),rtctime.second()); tft.print(buf); // RTCの時刻合わせ getLocalTime(&timeinfo); rtc.adjust(DateTime(timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)); // ローカルタイムの表示(デバッグ用) tft.setCursor(240, 260); tft.setTextFont(1); tft.setTextSize(3); tft.setTextColor(TFT_WHITE, TFT_BLACK); sprintf(buf, "NTP=%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); tft.print(buf); // 時刻合わせをした時刻の表示 DateTime now=rtc.now(); tft.fillRect (445,140,10,10,TFT_BLACK); tft.fillTriangle (435,150,465,150,450,170,TFT_BLACK); tft.fillCircle (450,150,10,TFT_WHITE); tft.setCursor(437, 170); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextFont(1); tft.setTextSize(1); sprintf(buf, "%02d:%02d", now.hour(), now.minute()); tft.print(buf); } // NTPサーバを用いた時刻合わせ関数 void call_ntp() { tft.fillCircle (450,150,10,TFT_BLACK); tft.fillRect (445,140,10,10,TFT_WHITE); tft.fillTriangle (435,150,465,150,450,170,TFT_WHITE); configTzTime(time_zone, ntpServer1, ntpServer2); } void setup () { // 初期設定 Serial.begin(57600); delay(5000); while (!Serial); // wait for serial port to connect. Needed for native USB // RTC初期設定 if (! rtc.begin()) { Serial.println("Couldn't find RTC"); Serial.flush(); while (1) delay(10); } if (! rtc.initialized() || rtc.lostPower()) { Serial.println("RTC is NOT initialized, let's set the time!"); } rtc.start(); // RTCの更正値計算(42時間で16秒進んでいた場合) float drift = 16; // 観察期間での誤差(単位は秒で+が進んだ場合) float period_sec = (42 * 3600); // 観察期間(秒) float deviation_ppm = (drift / period_sec * 1000000); // 偏差(μ秒) float drift_unit = 4.069; // 毎分修正する場合の単位(2時間毎の場合は4.34) int offset = round(deviation_ppm / drift_unit); // 最終的なオフセット値 // RTCキャリブレーション設定(1分毎に修正する場合) rtc.calibrate(PCF8523_OneMinute, offset); // TFT LCD 初期設定 tft.init(); tft.fillScreen(TFT_BLACK); tft.setRotation(3); tft.setTextFont(7); tft.fillRect (10,148,400,4,TFT_WHITE); // NTP時刻合わせ後のコールバック関数の設定 sntp_set_time_sync_notification_cb( timeavailable ); // Wi-Fi接続 Serial.printf("Connecting to %s ", ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(" CONNECTED"); // NTPを用いた時刻合わせ call_ntp(); } void loop () { char buf[255]; // RTCから時刻を取得 DateTime now = rtc.now(); // 前回取得から秒が変わっていたら時刻表示を変更する if (now.second() != old_sec) { tft.setCursor(10, 20); tft.setTextFont(7); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); sprintf(buf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); tft.print(buf); old_sec=now.second(); } // 前回取得から日が変わっていたら年月日表示を変更する if (now.day() != old_day) { tft.setCursor(80, 180); tft.setTextFont(7); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(1); sprintf(buf, "%04d-%02d-%02d", now.year(), now.month(), now.day()); tft.print(buf); old_day = now.day(); } // 0時、4時、8時、12時、16時、20時にNTPによる時刻合わせを行う if (now.minute()==0 && now.second()==0) { int hour = now.hour(); if (hour == 0 || hour == 4 || hour == 8 || hour == 12 || hour == 16 || hour == 20) { call_ntp(); } } // 0.1秒の待機 delay(100); }
22行目のグローバル変数は、現在表示している秒と日を保管するためのものです。これは、1/10秒毎にいちいち表示を変えると画面がちらつきそうだったので、秒と日の値に変化があった時のみ表示を変えるようするためのもの。
26行目の timeavailable関数が時刻合わせ後のコールバック関数です。41,42行目で内蔵RTCの時刻を用いてRTCモジュールの時刻を合わせています。
66行目の call_ntp関数が、時刻合わせ用の関数。70行目の configTzTimeで内蔵RTC時刻が更新されます。つまり70行目でNTPから内蔵RTC時刻を設定、コールバック関数の41,42行目で内蔵RTCからRTCモジュールの時刻を設定するという構成になっています。NTP⇒内蔵RTC⇒RTCモジュールと受け渡しているわけですね。このあたり、これで良いのか今ひとつ自信はありません。
90~98行目がRTCのキャリブレーションです。補正無しで42時間ほど動かしたときに16秒ほど進んでいたので、それを元に計算しています。PCF8523は毎分の補正と2時間に1回の補正の2つのモードがあるようで、今回は毎分の補正として計算、設定しています。1週間程度の進み(遅れ)具合で補正する方が、より正確なのでしょうが、どのみちNTPで数時間毎に時刻合わせするので充分でしょう。もちろん補正の精度を上げれば、NTP時刻合わせの頻度は少なくできます。
実行結果
電源投入直後の画面が下記です。
説明するまでもなく上段が時刻、下段が年月日になります。真ん中右の下向き矢印はNTP時刻合わせ中表示。
時刻合わせがされると、下記のようになります。
真ん中右の矢印が●になり、その下に小さく時刻合わせした時分を表示しています。最下段はデバッグ用に時刻合わせ直前のRTC時刻とNTPサーバから取得した時刻を表示しています。
課題
今回はブレッドボード上で構築していますが、本格的に組み立ててリビングで使用しようと思っています。本格的といってもユニバーサル基板にはんだ付けしてケースに入れる程度ですけどね。
ただ、リビングで使う場合には夜中にまで表示しておく必要はありません。そこでESP32のディープスリープ機能を用いて、暗くなったらすべて停止し、明るくなったら再起動するように、光センサーと連携するように回路&プログラムの追加をしようと思っています。