ESP32のNTP時刻取得:タイムゾーン、OLED、RTCバックアップ付きインターネット時計
ESP32でNTP時刻を取得する方法を解説します。configTzTimeとタイムゾーン、getLocalTime、strftime、OLED時計、DS3231によるバックアップ、毎日の定時タスク、Pico Wのntptimeを紹介します。
ESP32 NTPで時刻を取得するとは、ボードをインターネットの時刻サーバーに接続し、時計モジュールを使わずに数十ミリ秒以内の精度で日時を取得することです。ESP32がWi-Fiに接続したら、configTzTime()を1回呼び出すだけで時刻を取得し、タイムゾーンを適用して、その後はボード自身で時刻を刻み続けます。置き時計、定時の水やり、データ記録用のタイムスタンプ、朝のアラーム、決まった時刻に点灯する照明などに、別のRTCモジュールは必要ありません。
このガイドでは、NTPの仕組み、正しいタイムゾーンの設定、getLocalTimeとstrftimeによる任意の書式での日付出力、OLEDディスプレイへの時刻表示、Wi-Fiの切断や停電時にDS3231モジュールで時刻を保持する方法、毎日の指定時刻にタスクを実行する方法を順に解説します。最後に、Raspberry Pi Pico WでMicroPythonのntptimeモジュールを使い、同じ処理を行います。

NTPとは?ESP32はどのように時刻を取得する?
NTP(Network Time Protocol)は、インターネット経由でコンピューターや機器の時計を同期するために、1980年代から使われているプロトコルです。サーバーはstratumと呼ばれる階層に分かれています。stratum 1のサーバーは原子時計やGPSに直接接続され、stratum 2のサーバーはそこから時刻を取得し、その先も同様に続きます。ESP32はUDPポート123に小さなパケットを送って時刻を要求します。サーバーは、1900年からの経過秒数と1秒未満の端数を含むタイムスタンプを返します。ESP32はパケットの往復時間を考慮して、自身の時計を合わせます。家庭のインターネット回線では通常10〜50ミリ秒の精度が得られ、時計のプロジェクトには十分です。
ESP32内部のlwIPネットワークスタックは、NTPを簡略化したSNTPを使います。時刻を一度受信すると、ESP32はそれをシステム時刻として保持し、水晶発振器を基にしたタイマーで時刻を刻み続けます。毎秒インターネットにアクセスするわけではありません。バックグラウンドでは一定の間隔でサーバーに再問い合わせし、小さなずれを修正します。Arduino ESP32コアの標準設定では3時間ごとです。システム時刻はUTCで保持され、表示される現地時刻はタイムゾーンの規則を適用して計算されます。
| 項目 | 値・説明 |
|---|---|
| プロトコルとポート | SNTP, UDP 123 |
| タイムスタンプ | 1900年からの経過秒数(ESP32が1970年を基準とするUnix時刻に変換) |
| 一般的な精度 | 家庭の回線で10〜50 ms |
| 再同期 | Arduino ESP32では標準で3時間ごと。sntp_set_sync_interval()で変更可能(最短15 s) |
| サーバーの最大数 | 3(configTzTimeに渡すサーバー名) |
| インターネット通信量 | 要求と応答はそれぞれ48バイトのNTPパケット。多くても1日に数キロバイト |
| 必要なもの | ESP32、2.4 GHzのWi-Fiネットワーク、インターネット接続 |
configTimeとconfigTzTime:タイムゾーンの設定
Arduino ESP32コアでNTPを開始する方法は2つあります。configTime(gmtOffset_sec, daylightOffset_sec, server1, server2, server3)は、UTCからの時差と夏時間の補正値を秒単位で受け取ります。例えばトルコではconfigTime(10800, 0, "pool.ntp.org")と書きます。3時間は10,800秒で、トルコは2016年以降、夏時間による切り替えを行わず、通年でUTC+3を使っているためです。この方法は簡単ですが、地域の夏時間の開始日と終了日を認識しません。欧州や米国など夏時間を採用している地域では切り替え日が正しくならないため、configTzTimeを使ってください。
より適した方法はconfigTzTime(TZ, server1, server2, server3)です。この関数は夏時間の規則も含むPOSIX形式のタイムゾーン文字列を受け取り、ESP32が毎年の切り替えを自動で処理します。トルコの場合は"<+03>-3"です。山括弧の中の+03は画面に表示する略称で、-3は現地時刻から3時間を引くとUTCになることを表します。主な地域の文字列を次の表に示します。
| 地域 | TZ文字列(configTzTime) | configTimeでの指定 |
|---|---|---|
| トルコ(通年UTC+3) | <+03>-3 | configTime(10800, 0, ...) |
| ドイツ、フランス、イタリア、スペイン | CET-1CEST,M3.5.0,M10.5.0/3 | 不適切です。configTzTimeを使用 |
| 英国、ポルトガル | GMT0BST,M3.5.0/1,M10.5.0 | 不適切です。configTzTimeを使用 |
| 米国東部(ニューヨーク) | EST5EDT,M3.2.0,M11.1.0 | 不適切です。configTzTimeを使用 |
| アゼルバイジャン、ジョージア(UTC+4) | <+04>-4 | configTime(14400, 0, ...) |
| UTC(タイムスタンプ用) | UTC0 | configTime(0, 0, ...) |
必要なものと配線
シリアルモニタに時刻を表示するだけなら、ESP32とWi-Fiネットワークがあれば十分です。このガイドの後半では、時刻表示に0.96インチのSSD1306 OLEDディスプレイ、停電時の時刻保持にDS3231 RTCモジュール、定時タスクの例にリレーモジュールを使います。OLEDとDS3231は同じI2Cバスを共有するため、同じ2本のピンに接続します。ESP32のピン配置は「ピン配置」ツールで確認できます。
| 部品のピン | ESP32 | Raspberry Pi Pico W | 補足 |
|---|---|---|---|
| OLED / DS3231 VCC | 3V3 | 3V3 (OUT) | どちらも3.3 Vで動作 |
| OLED / DS3231 GND | GND | GND | 共通のGND |
| OLED / DS3231 SDA | GPIO 21 | GP4 | I2Cデータ線 |
| OLED / DS3231 SCL | GPIO 22 | GP5 | I2Cクロック線 |
| リレーモジュールのIN | GPIO 26 | GP15 | 定時タスクの例 |
| リレーモジュールのVCC | 5V (VIN) | VBUS | ほとんどのリレーのコイルには5 Vが必要 |
ステップごとに作る、初めてのESP32 NTPスケッチ
- Arduino IDEにESP32のボードパッケージがインストールされていることを確認し、「ツール」でESP32 Dev Moduleを選択してください。手順が分からない場合は、「ESP32とは?」の記事を参照してください。
- 追加のライブラリは不要です。WiFi.hとtime.hはESP32コアに含まれています。
- WIFI_SSIDとWIFI_PASSを使用するネットワークの値に置き換え、TZ_INFOには上の表から地域に合ったタイムゾーンを設定してください。
- スケッチを書き込み、シリアルモニタを115200ボーで開いてください。
- 最初にWi-Fi接続待ちの点が表示され、数秒後に日時が毎秒更新されるはずです。
1#include <WiFi.h>
2#include <time.h>
3
4const char* WIFI_SSID = "AgAdi"; // Wi-Fi adı / Wi-Fi name
5const char* WIFI_PASS = "Sifre123"; // Wi-Fi şifresi / Wi-Fi password
6const char* TZ_INFO = "<+03>-3"; // Türkiye: UTC+3, yaz saati yok / Turkey: UTC+3, no DST
7
8void setup() {
9 Serial.begin(115200);
10 WiFi.begin(WIFI_SSID, WIFI_PASS);
11 Serial.print("Wi-Fi");
12 while (WiFi.status() != WL_CONNECTED) { // bağlanana kadar bekle / wait until connected
13 delay(500);
14 Serial.print(".");
15 }
16 Serial.println(" OK");
17
18 // NTP sunucuları + saat dilimi / NTP servers + time zone
19 configTzTime(TZ_INFO, "pool.ntp.org", "time.cloudflare.com");
20
21 struct tm t;
22 while (!getLocalTime(&t)) { // ilk eşitlemeyi bekle (5 sn deneme) / wait for the first sync (5 s per try)
23 Serial.println("NTP bekleniyor / waiting for NTP");
24 }
25 Serial.println("Saat alindi / time received");
26}
27
28void loop() {
29 struct tm t;
30 if (getLocalTime(&t)) {
31 Serial.println(&t, "%d.%m.%Y %H:%M:%S"); // ör. 16.11.2026 14:05:09 / e.g. 16.11.2026 14:05:09
32 }
33 delay(1000);
34}
35| コード行 | 処理内容 |
|---|---|
| configTzTime(TZ_INFO, ...) | SNTPクライアントを開始し、サーバーとタイムゾーンを設定します。すぐに戻り、時刻はバックグラウンドで取得されます |
| getLocalTime(&t) | struct tmに現地時刻を格納します。まだ時刻がなければ最大5秒待ち、falseを返します |
| Serial.println(&t, "...") | ESP32のPrintクラスは、strftimeの書式でstruct tmを直接出力できます |
| time(nullptr) | 1970年からの経過秒数(Unix時刻、UTC)を返します |
ここには重要な点があります。getLocalTime()は年を見て、時刻が「有効」かどうかを判定します。ESP32の起動時、システム時刻は1970年1月1日で、年が2016年より後になるまで関数はfalseを返します。そのため、戻り値を確認すれば、NTP時刻の取得前にデータを記録したりタスクを実行したりするコードが、誤って1970年の日付で記録することを防げます。第2引数はミリ秒単位のタイムアウトです。loop()内で処理を止めたくない場合は、getLocalTime(&t, 0)と書くと1回だけ試行します。
struct tm:時刻を構成する各要素
getLocalTime()は、時刻を各要素に分けてstruct tmに書き込みます。定時タスクを作るときには、これらのフィールドで時・日・月を比較します。初心者がつまずきやすいのは2つのフィールドです。月は0から数え、年は1900年からの経過年数で表します。
| フィールド | 範囲 | 例(2026年11月16日月曜日、14:05:09) |
|---|---|---|
| tm_sec | 0〜59(うるう秒では60) | 9 |
| tm_min | 0〜59 | 5 |
| tm_hour | 0〜23 | 14 |
| tm_mday | 1〜31 | 16 |
| tm_mon | 0〜11(0=1月) | 10 |
| tm_year | 1900年からの経過年数 | 126(1900 + 126 = 2026) |
| tm_wday | 0〜6(0=日曜日) | 1 |
| tm_yday | 0〜365(年内の通算日、0から数える) | 319 |
strftimeで日時の書式を設定する
strftime()は、指定したパターンに従ってstruct tmの時刻を文字配列に書き込みます。パターン内のパーセント記号で始まるコードは対応する値に置き換えられ、それ以外の文字はそのまま残ります。配列のサイズは、最長の出力文字数に終端のゼロの分を1つ加えて確保してください。「14:05:09」は8文字なので、配列には最低9バイトが必要です。よく使うコードは次のとおりです。
| コード | 意味 | 出力例 |
|---|---|---|
| %H / %M / %S | 時(00〜23)/分/秒 | 14 / 05 / 09 |
| %Iと%p | 12時間表記とAM/PM | 02 PM |
| %d / %m / %Y | 日/月(01〜12)/4桁の年 | 16 / 11 / 2026 |
| %y | 2桁の年 | 26 |
| %A / %a | 曜日名、正式名/略称(英語) | Monday / Mon |
| %B / %b | 月名、正式名/略称(英語) | November / Nov |
| %j | 年内の通算日(001〜366) | 320 |
| %F / %T | %Y-%m-%d/%H:%M:%Sの短縮形 | 2026-11-16 / 14:05:09 |
| %Z | タイムゾーンの略称 | +03 |
ESP32のCライブラリが扱える曜日名と月名は英語だけで、%Aは常に「Monday」のように出力します。別の言語で日付を表示するには、曜日名と月名を自分で配列に用意し、tm_wdayとtm_monを配列のインデックスとして使います。次の例ではトルコ語の名前を使っています。使用したい言語に置き換えてください。シリアルモニタはUTF-8に対応しているため、アクセント付きの文字も正しく表示されます。このスケッチは、読みやすい日付と、記録用に並べ替えやすいISO形式(2026-11-16 14:05:09)の両方を出力します。
1#include <WiFi.h>
2#include <time.h>
3
4const char* WIFI_SSID = "AgAdi";
5const char* WIFI_PASS = "Sifre123";
6const char* TZ_INFO = "<+03>-3";
7
8// tm_wday: 0 = Pazar ... 6 = Cumartesi / 0 = Sunday ... 6 = Saturday
9const char* GUNLER[] = {"Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"};
10const char* AYLAR[] = {"Ocak", "Şubat", "Mart", "Nisan", "Mayıs", "Haziran",
11 "Temmuz", "Ağustos", "Eylül", "Ekim", "Kasım", "Aralık"};
12
13void setup() {
14 Serial.begin(115200);
15 WiFi.begin(WIFI_SSID, WIFI_PASS);
16 while (WiFi.status() != WL_CONNECTED) delay(500);
17 configTzTime(TZ_INFO, "pool.ntp.org");
18}
19
20void loop() {
21 struct tm t;
22 if (!getLocalTime(&t)) return; // saat henüz yok / time not set yet
23
24 char saat[9]; // "HH:MM:SS" + '\0'
25 char iso[20]; // "YYYY-MM-DD HH:MM:SS" + '\0'
26 strftime(saat, sizeof(saat), "%H:%M:%S", &t);
27 strftime(iso, sizeof(iso), "%Y-%m-%d %H:%M:%S", &t);
28
29 // Türkçe tarih: 16 Kasım 2026 Pazartesi / Turkish date
30 Serial.printf("%d %s %d %s %s\n", t.tm_mday, AYLAR[t.tm_mon], t.tm_year + 1900, GUNLER[t.tm_wday], saat);
31 Serial.printf("Kayit / log: %s epoch: %ld\n", iso, (long)time(nullptr));
32 delay(1000);
33}
34OLEDディスプレイに時刻と日付を表示する
シリアルモニタの出力を実際の置き時計にするには、0.96インチのSSD1306 OLEDディスプレイが適しています。安価で、2本の信号線で接続でき、大きな数字をくっきり表示できます。次のスケッチは、上部に曜日と日付、中央に大きな文字で時刻、下部にWi-Fiの状態を表示します。ループのたびではなく秒が変わったときだけ描き直すため、I2Cバスを無駄に占有しません。ライブラリマネージャからAdafruit SSD1306とAdafruit GFXライブラリをインストールしてください。ディスプレイ自体については「OLEDディスプレイ(SSD1306)」の記事で解説しています。
1#include <WiFi.h>
2#include <Wire.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5#include <time.h>
6
7const char* WIFI_SSID = "AgAdi";
8const char* WIFI_PASS = "Sifre123";
9const char* TZ_INFO = "<+03>-3";
10// OLED yazı tipinde Türkçe harf yok / the OLED font has no Turkish letters
11const char* GUNLER[] = {"Pazar", "Pazartesi", "Sali", "Carsamba", "Persembe", "Cuma", "Cumartesi"};
12
13Adafruit_SSD1306 display(128, 64, &Wire, -1);
14int sonSaniye = -1; // son çizilen saniye / last drawn second
15
16void yaz(const char* mesaj) { // ekrana tek satır / one line on screen
17 display.clearDisplay();
18 display.setTextSize(1);
19 display.setCursor(0, 0);
20 display.print(mesaj);
21 display.display();
22}
23
24void setup() {
25 Wire.begin(21, 22); // SDA = 21, SCL = 22
26 display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
27 display.setTextColor(SSD1306_WHITE);
28 yaz("Wi-Fi baglaniyor...");
29
30 WiFi.begin(WIFI_SSID, WIFI_PASS);
31 while (WiFi.status() != WL_CONNECTED) delay(300);
32 configTzTime(TZ_INFO, "pool.ntp.org", "time.cloudflare.com");
33 yaz("NTP bekleniyor...");
34}
35
36void loop() {
37 struct tm t;
38 if (!getLocalTime(&t, 0)) { // 0 = beklemeden dene / try without waiting
39 delay(200);
40 return;
41 }
42 if (t.tm_sec == sonSaniye) { // saniye değişmediyse çizme / redraw only on a new second
43 delay(20);
44 return;
45 }
46 sonSaniye = t.tm_sec;
47
48 char saat[9], tarih[11];
49 strftime(saat, sizeof(saat), "%H:%M:%S", &t);
50 strftime(tarih, sizeof(tarih), "%d.%m.%Y", &t);
51
52 display.clearDisplay();
53 display.setTextSize(1);
54 display.setCursor(0, 0);
55 display.print(GUNLER[t.tm_wday]); // gün adı / day name
56 display.setCursor(68, 0);
57 display.print(tarih); // 10 karakter = 60 piksel / 10 chars = 60 px
58 display.setTextSize(2);
59 display.setCursor(16, 26);
60 display.print(saat); // 8 x 12 = 96 piksel genişlik / 96 px wide
61 display.setTextSize(1);
62 display.setCursor(0, 56);
63 display.print(WiFi.status() == WL_CONNECTED ? "Wi-Fi: var" : "Wi-Fi: yok");
64 display.display();
65}
66曜日名にアクセント付きの文字を使っていない点に注目してください。Adafruit GFXの標準フォントにはş、ç、éなどの文字がなく、代わりに意味のない記号が表示されます。文字サイズ2では1文字の幅が12ピクセルなので、8文字の「14:05:09」は96ピクセルになります。16ピクセル目から描画すると、幅128ピクセルの画面のちょうど中央に配置できます。16x2文字のLCDを使う場合は、同じstrftimeの出力をlcd.print()で表示するだけです。配線は「I2C LCDディスプレイ」の記事で解説しています。
Wi-Fiが切れると時刻はどうなる?DS3231 RTCによるバックアップ
よくある質問は「Wi-Fiが切れると時刻も失われる?」です。答えは、いいえです。ESP32はNTPで一度時刻を取得すると、自身のタイマーで時刻を刻み続けるため、Wi-Fiが切れても時計は止まりません。このタイマーは40 MHzの水晶発振器を基に動作するため、1日に約1〜2秒ずれますが、Wi-Fiが復旧して次に同期すると修正されます。問題になるのは停電やENボタンによる再起動です。この場合はシステム時刻が1970年に戻り、Wi-FiがなければESP32は現在時刻を知ることができません。
| 状況 | 時刻は保持される? | 対処方法 |
|---|---|---|
| Wi-Fiが一時的に切断 | はい。ESP32が時刻を刻み続けます | 対応不要です。Wi-Fiの復旧後にNTPが修正します |
| ディープスリープ | はい。RTCタイマーが時刻を刻み続けます | 長時間スリープする場合は、何回かに1回の復帰時にNTPで再同期します |
| 停電またはENボタン | いいえ。時計が1970年に戻ります | DS3231などの電池バックアップ付きRTCモジュールを追加します |
| インターネットがまったくない場所(屋外の現場、倉庫) | NTP時刻を取得できません | RTCモジュールだけから時刻を取得します |
ディープスリープ中は、メインの水晶発振器ではなく、チップ内部の精度が低いRC発振器で時刻を刻みます。この発振器は温度の影響を受けるため、長時間のスリープで時計が数秒以上ずれることがあります。何時間もスリープする電池駆動のプロジェクトでは、数回の復帰ごとにNTPで同期するとよいでしょう。詳しくは「ESP32のディープスリープ」の記事を参照してください。

停電しても時刻を失わない時計にするには、両方を組み合わせるのが最適です。起動時にはすぐにDS3231モジュールから時刻を取得し、Wi-Fi接続後にNTPで修正して、NTPで同期するたびにDS3231も更新します。次のスケッチでは、DS3231に現地時刻ではなくUTCを保存します。こうするとタイムゾーンの規則をTZ文字列の1か所にまとめられ、夏時間への切り替えでもRTCの時刻が崩れません。NTPの同期を検出するため、sntp_set_time_sync_notification_cb()でコールバックを登録します。このコールバックはネットワークタスク内で動くため、その中ではI2C処理を行いません。フラグだけを立て、DS3231への書き込みはloop()で処理します。スケッチにはRTClibライブラリを使います。
1#include <WiFi.h>
2#include <Wire.h>
3#include <RTClib.h>
4#include <sys/time.h>
5#include "esp_sntp.h"
6
7const char* WIFI_SSID = "AgAdi";
8const char* WIFI_PASS = "Sifre123";
9const char* TZ_INFO = "<+03>-3";
10
11RTC_DS3231 rtc;
12volatile bool ntpGeldi = false; // NTP eşitlemesi oldu mu? / did an NTP sync happen?
13
14void ntpCallback(struct timeval* tv) { // her NTP eşitlemesinde çağrılır / called on every NTP sync
15 ntpGeldi = true; // I2C işini loop() yapsın / let loop() do the I2C work
16}
17
18void setup() {
19 Serial.begin(115200);
20 Wire.begin(21, 22);
21 setenv("TZ", TZ_INFO, 1); // NTP gelmese de saat dilimi geçerli / TZ valid even without NTP
22 tzset();
23
24 // 1) Sistem saatini önce DS3231'den kur (DS3231'de UTC saklanır) / set system time from the DS3231 (stores UTC)
25 if (rtc.begin() && !rtc.lostPower()) {
26 struct timeval tv = { (time_t)rtc.now().unixtime(), 0 };
27 settimeofday(&tv, nullptr);
28 Serial.println("Saat DS3231'den / time from DS3231");
29 }
30
31 // 2) Wi-Fi gelince NTP saati düzeltir; bağlantı sonra gelse de SNTP yeniden dener
32 // NTP corrects the clock once Wi-Fi is up; SNTP keeps retrying if the link comes later
33 sntp_set_time_sync_notification_cb(ntpCallback);
34 WiFi.begin(WIFI_SSID, WIFI_PASS);
35 configTzTime(TZ_INFO, "pool.ntp.org", "time.cloudflare.com");
36}
37
38void loop() {
39 if (ntpGeldi) {
40 ntpGeldi = false;
41 rtc.adjust(DateTime((uint32_t)time(nullptr))); // DS3231'e UTC yaz / write UTC to the DS3231
42 Serial.println("DS3231 NTP ile guncellendi / DS3231 updated from NTP");
43 }
44
45 struct tm t;
46 if (getLocalTime(&t, 0)) {
47 Serial.println(&t, "%d.%m.%Y %H:%M:%S");
48 } else {
49 Serial.println("Saat yok: NTP ve DS3231 bekleniyor / no time yet");
50 }
51 delay(1000);
52}
53毎日の指定時刻にタスクを実行する
NTP時刻の代表的な用途は定時タスクです。07:30に水やりポンプを10分間動かす、19:00に庭の照明をつける、毎時ちょうどに測定値を送る、といった使い方があります。方法は簡単で、loop()で時刻を読み、時と分が指定値に一致したらタスクを開始します。注意が必要なのは、同じ1分間にタスクが何百回も実行されるのを防ぐことです。そのために、タスクを実行した日(tm_yday)を変数に保存し、同じ日に2回実行されないようにします。次のスケッチは、平日の毎朝07:30にGPIO 26のリレーを10分間オンにします。時間の計測にmillis()を使うため、コードの処理を止めません。
1#include <WiFi.h>
2#include <time.h>
3
4const char* WIFI_SSID = "AgAdi";
5const char* WIFI_PASS = "Sifre123";
6const char* TZ_INFO = "<+03>-3";
7
8const int ROLE_PIN = 26; // röle modülü IN / relay module IN
9const int GOREV_SAAT = 7; // her gün 07:30 / every day at 07:30
10const int GOREV_DAKIKA = 30;
11const unsigned long SURE_MS = 10UL * 60 * 1000; // 10 dakika çalış / run for 10 minutes
12
13int sonGun = -1; // görevin çalıştığı son gün (tm_yday) / last day the task ran
14bool roleAcik = false;
15unsigned long acilisMs = 0;
16
17void setup() {
18 Serial.begin(115200);
19 pinMode(ROLE_PIN, OUTPUT);
20 digitalWrite(ROLE_PIN, LOW);
21 WiFi.begin(WIFI_SSID, WIFI_PASS);
22 while (WiFi.status() != WL_CONNECTED) delay(500);
23 configTzTime(TZ_INFO, "pool.ntp.org");
24}
25
26void loop() {
27 struct tm t;
28 if (getLocalTime(&t, 0)) { // saat geçerliyse / only when the time is valid
29 bool haftaici = (t.tm_wday >= 1 && t.tm_wday <= 5); // Pzt-Cuma / Mon-Fri
30 if (haftaici && t.tm_hour == GOREV_SAAT && t.tm_min == GOREV_DAKIKA && t.tm_yday != sonGun) {
31 sonGun = t.tm_yday; // bugün bir kez / once per day
32 digitalWrite(ROLE_PIN, HIGH);
33 roleAcik = true;
34 acilisMs = millis();
35 Serial.println(&t, "%H:%M gorev basladi / task started");
36 }
37 }
38 if (roleAcik && millis() - acilisMs >= SURE_MS) { // süre dolunca kapat / switch off after the period
39 digitalWrite(ROLE_PIN, LOW);
40 roleAcik = false;
41 Serial.println("Gorev bitti / task finished");
42 }
43 delay(200);
44}
45- 秒での一致判定:t.tm_sec == 0のような条件では、Wi-Fiの再接続中や長いdelayの実行中などにループがその秒を逃すと、タスクが実行されません。分と日を確認する方が確実です。
- 時刻が有効か確認:getLocalTime()がfalseを返している間はタスクを実行しないでください。起動直後の1970年を基に誤った判断をしてしまいます。
- 実行し損ねたタスク:07:29に停電し、07:35に復旧すると、その日のタスクは実行されません。必要な場合は、「07:30を過ぎていて、今日はまだ実行していない」という条件にし、PreferencesでsonGunをフラッシュメモリに保存してください。
- 複数のタスク:時・分・実行時間を配列に保存し、各タスクに個別の最終実行日変数を用意してください。
- 大電流の負荷:ポンプやランプはリレーモジュールまたはMOSFETを介して駆動し、ピンから直接駆動しないでください。
同期間隔の変更とNTPの監視
標準の3時間ごとの同期間隔は、ほとんどのプロジェクトに適しています。例えば温度変化の大きい屋外で同期を増やしたい場合や、通信量を減らしたい場合は、sntp_set_sync_interval()に新しい間隔をミリ秒単位で渡してください。新しい間隔は次回の開始時か現在の間隔が終了した時点で有効になるため、configTzTime()より前に呼び出してください。プロトコルの規則により、間隔は15秒未満にはできません。sntp_get_sync_status()は同期完了後に1回だけSNTP_SYNC_STATUS_COMPLETEDを返し、その後は自動でリセットされるため、新しい同期を毎回検出できます。
1#include <WiFi.h>
2#include <time.h>
3#include "esp_sntp.h"
4
5const char* TZ_INFO = "<+03>-3";
6
7void setup() {
8 Serial.begin(115200);
9 WiFi.begin("AgAdi", "Sifre123");
10 while (WiFi.status() != WL_CONNECTED) delay(500);
11
12 sntp_set_sync_interval(6UL * 60 * 60 * 1000); // 6 saatte bir eşitle (en az 15 sn) / sync every 6 hours (min 15 s)
13 configTzTime(TZ_INFO, "pool.ntp.org"); // ayar sntp_init() öncesi verilir / set before sntp_init()
14}
15
16void loop() {
17 if (sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) {
18 Serial.println("Yeni NTP esitlemesi / new NTP sync");
19 }
20 delay(1000);
21}
22どのNTPサーバーを使う?
最もよく使われるのはpool.ntp.orgです。世界中のボランティアが運用する数千台のサーバーの集合で、要求するたびに近くのサーバーのアドレスが返されます。国別のサブドメイン(uk.pool.ntp.org、de.pool.ntp.org)を使えば、さらに近いサーバーを選べます。configTzTime()には最大3つのサーバー名を指定でき、最初のサーバーが応答しなければ次に進みます。
| サーバー | 特徴 | 補足 |
|---|---|---|
| pool.ntp.org (NTPサーバーのアドレス) | ボランティアによるプール、最寄りのサーバー | 一般用途での第一候補 |
| uk.pool.ntp.org / europe.pool.ntp.org (国・大陸別NTPサーバーのアドレス) | 国別・大陸別のプール | 遅延がやや小さい |
| time.cloudflare.com (CloudflareのNTPサーバー) | 大企業のインフラ | バックアップ用サーバーに適しています |
| time.google.com (GoogleのNTPサーバー) | うるう秒を分散して調整(リープスメア) | 他のサーバーと混在させないでください |
| 使用するルーターのIPアドレス | 一部のルーターはローカルNTPを提供 | インターネットのないローカルネットワークで便利です |
Raspberry Pi Pico WのNTP:ntptime
Raspberry Pi Pico WとPico 2 Wでは、MicroPythonのntptimeモジュールで同じ処理を行います。ntptime.settime()はNTPサーバーに問い合わせ、Picoの内部RTCをUTCに設定します。MicroPythonにはESP32のようなタイムゾーン文字列がないため、現地時刻を求めるにはUTCに時差を秒単位で加えます。例ではUTC+3に対して3 × 3600を加えます。ntptimeは時刻を1回取得するだけで、自動では再同期しません。長時間動作するプロジェクトでは、1日に1〜2回settime()を呼び出すとずれを修正できます。Pico WのRTCも電源断でリセットされるため、DS3231による同じバックアップ方法が使えます。Pico WのWi-Fi接続は、「Pico WのWi-FiとWebサーバー」の記事で詳しく解説しています。
1import network
2import ntptime
3import time
4
5SSID = "AgAdi"
6PASS = "Sifre123"
7UTC_OFFSET = 3 * 3600 # Türkiye UTC+3 / Turkey UTC+3
8# localtime(): 0 = Pazartesi / 0 = Monday
9GUNLER = ("Pazartesi", "Sali", "Carsamba", "Persembe", "Cuma", "Cumartesi", "Pazar")
10
11wlan = network.WLAN(network.STA_IF)
12wlan.active(True)
13wlan.connect(SSID, PASS)
14while not wlan.isconnected(): # bağlanana kadar bekle / wait until connected
15 time.sleep(0.5)
16
17ntptime.host = "pool.ntp.org"
18for deneme in range(5): # NTP bazen zaman aşımına uğrar / NTP may time out
19 try:
20 ntptime.settime() # dahili RTC'yi UTC'ye ayarlar / sets the internal RTC to UTC
21 break
22 except OSError:
23 time.sleep(2)
24
25def yerel_saat(): # UTC + 3 saat / UTC + 3 hours
26 return time.localtime(time.time() + UTC_OFFSET)
27
28son_gun = -1
29while True:
30 yil, ay, gun, saat, dk, sn, hgun, ygun = yerel_saat()
31 print("{:02d}.{:02d}.{} {} {:02d}:{:02d}:{:02d}".format(gun, ay, yil, GUNLER[hgun], saat, dk, sn))
32 if saat == 7 and dk == 30 and ygun != son_gun: # her gün 07:30'da bir kez / once a day at 07:30
33 son_gun = ygun
34 print("Gorev calisti / task ran")
35 time.sleep(1)
36MicroPythonのtime.localtime()は、年、月(1〜12)、日、時、分、秒、曜日、年内の通算日という8要素のタプルを返します。ESP32のstruct tmとの違いは2点です。月は1から始まり、曜日は0=月曜日として数えます。ntptime.settime()はネットワークがタイムアウトするとOSErrorを発生させるため、スケッチには5回試行するループを入れています。
ESP32のNTP時計プロジェクトのアイデア
- Wi-Fi置き時計:1枚のOLEDディスプレイに時刻・日付・室温を表示し、夜間は暗くします。
- 大型LED時計:MAX7219 LEDマトリクスに、NTP時刻をスクロールする文字で表示します。
- 定時の植物への水やり:毎朝土壌の水分を確認し、必要に応じてミニウォーターポンプを一定時間動かします。
- タイムスタンプ付きデータ記録:センサーの測定値をISO形式の日付とともにSDカードへ書き込みます。
- 天気時計:時刻の横にインターネットから取得した天気を表示します。「ESP32の天気ステーション」の記事で、この先を解説しています。
- 朝のレポート:毎日08:00に、家の温度と夜間に検知した動きをTelegramでスマートフォンに送ります。
よくある間違いと対処方法
| 症状 | 考えられる原因 | 対処方法 |
|---|---|---|
| 時刻が1970年になる、または取得できない | Wi-Fiに接続していないか、NTPポート(UDP 123)が遮断されています | まずWiFi.status()を確認してください。学校や職場のネットワークでは、別のネットワークやスマートフォンのテザリングを試します |
| UTCからの時差と同じ時間だけずれる | タイムゾーンが未設定で、UTCが表示されています | configTzTime("<+03>-3", ...)または使用する地域の文字列を指定します |
| 時差の2倍だけずれる | TZ文字列の符号が逆です("UTC+3") | UTC+3には"<+03>-3"または"UTC-3"を使います |
| 月が1つ少なく、年が126になる | tm_monは0から、tm_yearは1900から数えます | tm_mon + 1、tm_year + 1900、またはstrftimeを使います |
| 同じ1分間にタスクが何度も実行される | 繰り返し実行を防ぐ処理がありません | 実行した日(tm_yday)を保存して確認します |
| ループが毎回5秒間止まる | 有効な時刻がないと、getLocalTime()は標準で5000 ms待機します | loop()内ではgetLocalTime(&t, 0)を使います |
| OLEDの曜日名に意味のない記号が表示される | フォントにアクセント付きの文字がありません | 曜日名をASCII文字だけで書くか、U8g2ライブラリに切り替えます |
ESP32のNTPは、プロジェクトに正確な時刻を加える最も安価で手間の少ない方法です。数行のコードで秒単位の正確な時刻を取得し、タイムゾーンと夏時間を自動で処理できます。DS3231を追加すれば、停電にも対応できます。BlueGraysアプリには、ESP32とNTPによるリアルタイム時計、DS1307 RTCを使ったデジタル時計、OLEDへの天気表示、タイムスタンプ付き温度ロガーなど、回路図とコード付きで順を追って作れるプロジェクトがあります。
よくある質問
ESP32はインターネットなしで時刻を保持できますか?
はい。NTPで一度時刻を取得すれば、ESP32は自身のタイマーでシステム時刻を刻み続け、Wi-Fiが切れても時計は止まりません。ただし、電源が切れたりボードが再起動したりすると、時刻は1970年に戻ります。インターネットのない場所や、停電しても時刻を失わない時計には、DS3231などの電池バックアップ付きRTCモジュールを追加してください。
ESP32のNTP時刻が数時間ずれるのはなぜですか?
タイムゾーンを設定していない場合、ESP32はUTCを表示するため、UTCからの時差と同じ時間だけずれます。configTzTimeに正しいPOSIX文字列を指定してください。例えば中央ヨーロッパなら"CET-1CEST,M3.5.0,M10.5.0/3"、英国なら"GMT0BST,M3.5.0/1,M10.5.0"です。時差の2倍だけずれる場合は、TZ文字列の符号が逆です。
configTimeとconfigTzTimeの違いは何ですか?
configTimeはUTCからの時差と夏時間の補正値を秒単位で受け取りますが、その地域で夏時間がいつ始まり、いつ終わるかは認識しません。configTzTimeは夏時間の規則を含むPOSIX形式のタイムゾーン文字列を受け取るため、ESP32が夏時間と冬時間を自動で切り替えます。夏時間を採用する地域ではconfigTzTimeが適切です。
ESP32はどのくらいの間隔でNTPに同期しますか?
Arduino ESP32コアでは、SNTPは標準で3時間ごとにサーバーへ再問い合わせし、その間は自身のタイマーで時刻を保持します。sntp_set_sync_interval()で間隔をミリ秒単位で変更できますが、プロトコルの規則により15秒未満にはできません。
ESP32にRTCモジュールは必要ですか?
常に電源が供給され、Wi-Fiに接続できる機器なら不要です。NTPの方が正確な時刻を取得できます。停電後、インターネットが復旧する前に正しい時刻が必要な場合や、インターネットのない場所で動かす場合は、DS3231などの電池バックアップ付きRTCモジュールを追加してください。
Raspberry Pi Pico WでNTP時刻を取得するには?
MicroPythonではWi-Fiに接続した後、import ntptimeとntptime.settime()を実行するだけで、Picoの内部RTCをUTCに設定できます。現地時刻を求めるには時差を秒単位で加えます。例えばUTC+1ならtime.localtime(time.time() + 3600)です。ntptimeは自動で再同期しないため、1日に1〜2回settime()を再度呼び出してください。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32とNTPでインターネットから正確な時刻を取得
ESP32をWi-Fiに接続し、NTP(Network Time Protocol)サーバーからミリ秒精度の現在時刻を取得します。configTzTime()関数でタイムゾーンを自動適用し、夏時間を採用する国ではPOSIX TZ文字列で夏時間と標準時間の切り替えも自動管理します。時刻はシリアルモニタと、必要に応じて接続するI2C LCDに表示します。configTzTime()がSNTPサービスを開始し、ネットワークにアクセスできる間は定期的に再同期します。

ESP32とDS1307 RTCのデジタル時計(I2C + LCD)
このプロジェクトでは、DS1307リアルタイムクロック(RTC)モジュールを使って16x2 I2C LCDに時刻と日付を表示します。DS1307は電池でバックアップされるRTCで、ESP32の電源を切っても時刻を保持します。両方のモジュールが同じI2Cバス(SDA/SCL)を共有するため、2本の信号線だけで複数の機器を制御する方法を学べます。I2C通信プロトコルやRTCモジュールの動作原理、LCDの表示データを随時更新する方法を実践的に学ぶプロジェクトです。

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。

ESP32 HTTPClient+天気API(SSD1306 OLED付き)
このプロジェクトでは、ESP32でHTTPClient.hを使い、インターネット経由で天気API(wttr.in)をREST APIとして呼び出します。前のI2C LCD版とは異なり、SSD1306 OLED(128×64)ディスプレイを使うため、大きな文字やベクターアイコンで、1つの画面に豊かな表現ができます。天気を表す7種類のベクターアイコン(晴れ、晴れ時々曇り、曇り、雨、雪、嵐、霧)を実行時に描画するので、ビットマップファイルは不要です。気温は中央に3倍の大きさの文字で表示します。

ESP32 HTTPClient+天気API(LCD付き)
このプロジェクトでは、ESP32でHTTPClient.hを使い、インターネット経由で天気API(wttr.in)をREST APIとして呼び出します。返されたJSONレスポンスをArduinoJson v7で解析し、都市名、気温、湿度、天気、風の情報をI2C LCDディスプレイにトルコ語と英語で表示します。wttr.inは無料で、APIキーも不要です。都市名を入力するだけで、その都市の現在の天気を取得できます。30分ごとに自動更新し、表示画面ごとに異なるLEDの光り方で状態を知らせます。

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)
ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。

ESP32温度データロガー(DHT22 + DS1307 RTC + microSD + OLED)
ESP32で日時付きの温度・湿度記録システムを作ります!DHT22で1分ごとに温度と湿度を読み取り、DS1307 RTCから取得した日時とともにmicroSDカードへCSV形式で記録します。OLEDには最新の温度を大きな文字で表示し、湿度のパーセント値、日時、総記録数も表示します。SDカードの/temp_log.csvファイルはExcel、Google Sheets、Pythonのpandasで開いて分析できます。温室の監視、家や部屋の温度管理、実験室のデータ収集、食品の保管環境や養蜂箱の監視に適しています。

ESP32 I2C LCDディスプレイ(16x2)
このプロジェクトでは、16x2文字のLCDをI2CでESP32に接続して文字を表示します。I2Cならデータ用の2本のピン(SDAとSCL)だけで制御でき、従来のパラレル接続のように6本以上のピンを使わずに済みます。コードでは1行目に固定メッセージ、2行目に毎秒増えるカウンターを表示します。また、起動時にI2Cスキャナーを実行し、ディスプレイのアドレス(通常は0x27または0x3F)をシリアルモニタに表示します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

