0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 NTP 網路時鐘 + OLED 顯示完整教學:從 NTP 協定到 Arduino 實作

2026 年 6 月 11 日 12點熱度 0人點贊 0條評論

為什麼需要 NTP?

ESP32 NTP 網路時鐘是 IoT 專案中最實用的時間同步方案。ESP32 沒有板載 RTC(即時時鐘)電池備援,每次斷電重啟後時間都會歸零。在 IoT 專案中,精確的時間戳記對以下場景至關重要:

  • 感測器數據記錄:知道資料是何時採集的
  • 排程控制:定時開關裝置、鬧鐘功能
  • 安全認證:TLS 憑證驗證、Token 時效
  • 分散式同步:多個 ESP32 節點時間一致

NTP(Network Time Protocol)是最廣泛使用的網路時間同步協定,能在區域網路內達到 sub-millisecond 精度,在網際網路上達到數毫秒精度。

ESP32 NTP 網路時鐘系統架構

NTP 協定工作原理

NTP 使用用戶端-伺服器架構,透過 UDP port 123 交換時間戳記。核心是四時間戳計算模型:

NTP 協定時序圖 — 四時間戳偏移計算

ESP32 發送 NTP 請求時記錄 T1(本地時間),NTP 伺服器收到請求記錄 T2,準備回覆時記錄 T3,ESP32 收到回覆記錄 T4。有了這四個時間戳,就能計算:

偏移量 θ = (T2 − T1 + T3 − T4) / 2
往返延遲 δ = (T4 − T1) − (T3 − T2)

校正後時間 = T1 + θ

這個演算法假設網路延遲是對稱的(上下行時間相等),實際誤差通常小於 50ms。

NTP 時間同步流程

NTP 封包格式

NTP v4 封包固定 48 bytes,結構如下:

NTP 封包格式 v4 48 bytes

  • LI (Leap Indicator, 2b):宣告閏秒(最後一分鐘 59/61 秒)
  • VN (Version Number, 3b):NTP 版本,v4 = 100
  • Mode (3b):3 = 用戶端,4 = 伺服器
  • Stratum (8b):0 = 未同步,1 = 主時鐘(原子鐘/GPS),2-15 = 層級
  • Root Delay / Dispersion:同步路徑總延遲與最大誤差
  • Reference Timestamp:最後一次同步的時間
  • Originate / Receive / Transmit Timestamp:四個時間戳中的三個(T1、T2、T3)

時間戳使用 64-bit 定點數:前 32 bit 為秒數(從 1900-01-01 起算),後 32 bit 為小數部分,理論精度約 232 皮秒。

ESP32 + OLED SSD1306 硬體接線

OLED SSD1306 ESP32 說明
VCC 3.3V 電源 (3.3V ~ 5V 皆可)
GND GND 共地
SCL GPIO 22 I²C 時脈
SDA GPIO 21 I²C 資料

SSD1306 使用 I²C 通訊,預設位址為 0x3C(或 0x3D,依模組而定)。透過送出指令設定頁面、欄位位置後,寫入 128×64 = 1024 bytes 的顯示緩衝區。

SSD1306 OLED I2C 指令寫入時序

ESP32 NTP 客戶端:Arduino 實作

// ESP32 NTP 時間同步(使用 WiFiUDP)
#include 
#include 

const char* ssid     = "YourSSID";
const char* password = "YourPassword";

WiFiUDP udp;
const char* ntpServer = "pool.ntp.org";  // NTP 伺服器池
const int localPort  = 8888;             // 本地 UDP 埠

// NTP 封包(48 bytes)
byte packetBuffer[48];

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) delay(500);

    udp.begin(localPort);
    sendNTPpacket(ntpServer);
}

void loop() {
    int size = udp.parsePacket();
    if (size >= 48) {
        udp.read(packetBuffer, 48);

        // 提取 T4 時間戳(第 40-43 bytes = 秒數,第 44-47 = 小數)
        unsigned long secsSince1900 = 0;
        secsSince1900 |= packetBuffer[40] << 24;
        secsSince1900 |= packetBuffer[41] << 16;
        secsSince1900 |= packetBuffer[42] << 8; secsSince1900 |= packetBuffer[43]; // NTP 紀元 1900 → Unix 紀元 1970 const unsigned long seventyYears = 2208988800UL; unsigned long timestamp = secsSince1900 - seventyYears; Serial.printf("NTP 時間: %lu\n", timestamp); Serial.printf("本地時間: %s", ctime((time_t*)&timestamp)); delay(10000); // 每 10 秒同步一次 } } void sendNTPpacket(const char* server) { memset(packetBuffer, 0, 48); // LI=0, VN=4, Mode=3 (client) packetBuffer[0] = 0b11100011; // 0xE3 // 填入 T1(發送時間) unsigned long t1 = millis(); packetBuffer[40] = (t1 >> 24) & 0xFF;
    packetBuffer[41] = (t1 >> 16) & 0xFF;
    packetBuffer[42] = (t1 >> 8) & 0xFF;
    packetBuffer[43] = t1 & 0xFF;

    udp.beginPacket(server, 123);  // NTP port
    udp.write(packetBuffer, 48);
    udp.endPacket();
}

使用 ESP32 內建 time() 函式(更簡單)

Arduino-ESP32 封裝了 SNTP 客戶端,直接使用標準 C 的 time() 和 localtime() 即可:

// ESP32 SNTP — 最簡方式同步時間
#include 
#include 

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 28800;     // UTC+8 (台灣/北京)
const int   daylightOffset_sec = 0;    // 不考慮夏令時間

void setup() {
    Serial.begin(115200);
    WiFi.begin("YourSSID", "YourPassword");
    while (WiFi.status() != WL_CONNECTED) delay(500);

    // 初始化 SNTP
    configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

    // 等待時間同步完成
    struct tm timeinfo;
    while (!getLocalTime(&timeinfo)) {
        Serial.println("等待 NTP 同步...");
        delay(1000);
    }
    Serial.println("時間同步完成!");
}

void loop() {
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
        char buf[64];
        strftime(buf, sizeof(buf), "%F %T (%A)", &timeinfo);
        Serial.println(buf);
    }
    delay(1000);
}

OLED 顯示時鐘(完整範例)

結合 SNTP + SSD1306 OLED,做出真正的網路時鐘:

// ESP32 NTP 網路時鐘 + OLED 顯示
#include 
#include 
#include 
#include 
#include 

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const char* ssid     = "YourSSID";
const char* password = "YourPassword";
const char* ntpServer = "pool.ntp.org";
const long  gmtOffset = 28800;  // UTC+8

void setup() {
    Serial.begin(115200);

    // 初始化 OLED
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println("OLED 初始化失敗");
        while (1) delay(10);
    }
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);

    // 連 WiFi
    display.setCursor(0, 0);
    display.println("Connecting WiFi...");
    display.display();

    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) delay(500);

    display.println("WiFi OK");
    display.display();

    // NTP 同步
    display.println("Syncing NTP...");
    display.display();

    configTime(gmtOffset, 0, ntpServer);

    struct tm timeinfo;
    while (!getLocalTime(&timeinfo)) {
        delay(500);
        display.print(".");
        display.display();
    }

    display.println("\nTime synced!");
    display.display();
    delay(1000);
}

void loop() {
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) {
        display.clearDisplay();
        display.setCursor(0, 0);
        display.println("NTP Error!");
        display.display();
        delay(5000);
        return;
    }

    display.clearDisplay();

    // ---- 第 1 行:日期(置中) ----
    display.setTextSize(1);
    char dateBuf[32];
    strftime(dateBuf, sizeof(dateBuf), "%Y-%m-%d (%a)", &timeinfo);

    int16_t x1, y1;
    uint16_t w, h;
    display.getTextBounds(dateBuf, 0, 0, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w) / 2, 4);
    display.println(dateBuf);

    // ---- 第 2 行:時間(大字) ----
    display.setTextSize(3);
    char timeBuf[16];
    strftime(timeBuf, sizeof(timeBuf), "%H:%M:%S", &timeinfo);

    display.getTextBounds(timeBuf, 0, 20, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w) / 2, 22);
    display.println(timeBuf);

    // ---- 第 3 行:WiFi 狀態 + 運行時間 ----
    display.setTextSize(1);
    char wifiStr[32];
    snprintf(wifiStr, sizeof(wifiStr), "RSSI: %d dBm  UP: %lus",
             WiFi.RSSI(), millis() / 1000);
    display.getTextBounds(wifiStr, 0, 50, &x1, &y1, &w, &h);
    display.setCursor((SCREEN_WIDTH - w) / 2, 52);
    display.println(wifiStr);

    display.display();
    delay(500);  // 每 0.5 秒更新一次
}

進階:夏令時間自動切換

部分地區實施夏令時間(DST),可以動態調整:

// ESP32 自動偵測 DST(以歐洲/北美為例)
void setupTimeWithDST() {
    // 方法 1:使用 posix timezone string
    // 北美東部: EST5EDT,M3.2.0,M11.1.0
    // 歐洲中部: CET-1CEST,M3.5.0,M10.5.0/3
    // 台灣/中國: CST-8 (無 DST)

    setenv("TZ", "CST-8", 1);  // 台灣
    tzset();

    configTime(0, 0, "pool.ntp.org");
}

精度實測:NTP vs internal clock

計時方式 每日誤差 優點 缺點
ESP32 internal millis() ~10-30 秒 不需網路 隨溫度飄移
NTP 同步(每小時) < 10ms 精度極高 需 WiFi
DS3231 RTC 模組 ~1 秒/年 斷電保留 額外硬體成本
GPS PPS ~1μs 原子鐘級 需 GPS 模組 + 天線

建議實務做法:NTP 每小時同步一次,其餘時間用 ESP32 內建 micros() 或 esp_timer 維持計時。

NTP 伺服器選擇

  • pool.ntp.org:全球 DNS 輪詢,自動導向最近伺服器(推薦)
  • time.google.com:Google 公共 NTP(GPS 參考)
  • time.nist.gov:美國 NIST 官方(高精度)
  • ntp.aliyun.com:阿里雲(中國大陸速度快)
  • 自建 NTP Server:樹莓派 + 4G/GPS 模組
// 多伺服器備援
const char* ntpServers[] = {
    "pool.ntp.org",
    "time.google.com",
    "time.nist.gov"
};
configTime(gmtOffset, 0, ntpServers[0], ntpServers[1], ntpServers[2]);

搭配 Web Server 做配置頁面

結合之前介紹的 ESP32 Web Server,可以做出 WiFi 配置 + 時區設定網頁:

// Web Server + NTP 時鐘配置
#include 

WebServer server(80);

String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>NTP 時鐘設定</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>NTP 網路時鐘設定</h1>
<form action="/config" method="POST">
  SSID: <input name="ssid"><br>
  密碼: <input name="pass" type="password"><br>
  時區: <select name="tz">
    <option value="0">UTC</option>
    <option value="28800" selected>Asia/Taipei (UTC+8)</option>
    <option value="32400">Japan (UTC+9)</option>
  </select><br>
  <input type="submit" value="儲存">
</form>
<p id="clock"></p>
<script>
function updateClock() {
  fetch('/api/time')
    .then(r => r.json())
    .then(d => document.getElementById('clock').innerText = d.time);
}
setInterval(updateClock, 1000);
updateClock();
</script>
</body></html>
)rawliteral";

void handleConfig() {
    String ssid = server.arg("ssid");
    String pass = server.arg("pass");
    long tz = server.arg("tz").toInt();

    // 儲存到 Preferences / LittleFS
    // 重啟後套用新設定

    server.send(200, "text/plain", "設定已儲存,重新啟動中...");
    delay(1000);
    ESP.restart();
}

void setup() {
    server.on("/", handleRoot);
    server.on("/config", HTTP_POST, handleConfig);
    server.begin();
}

總結

NTP 是 IoT 專案中不可或缺的基礎服務。ESP32 的 SNTP 客戶端封裝了完整的 NTP v4 協定,只需 configTime() 一行就能同步時間。搭配 OLED 顯示器,短短幾十行程式就能做出實用的網路時鐘。

實務建議:每小時同步一次 NTP,平時使用 time() / getLocalTime() 讀取本地時間;若需要高精度計時(< 1ms),搭配 esp_timer 硬體定時器或 DS3231 RTC 模組。

標籤: 教學
最後更新:2026 年 6 月 11 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang