Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

ESP32 không có pin nuôi đồng hồ, nên mỗi lần cấp nguồn nó không biết bây giờ là mấy giờ. Khi đã có Wi-Fi, ESP32 có thể hỏi giờ từ các máy chủ thời gian trên Internet bằng giao thức NTP. Bài viết hướng dẫn lấy giờ Việt Nam bằng NTP trên Arduino IDE, in ra Serial Monitor, rồi làm một chiếc đồng hồ hiển thị trên màn hình OLED 0.96 inch.

1. NTP là gì và ESP32 dùng nó thế nào?

NTP (Network Time Protocol) là giao thức đồng bộ thời gian qua mạng. Thiết bị gửi một gói tin nhỏ tới máy chủ NTP và nhận về giờ chuẩn quốc tế UTC. Gói thư viện esp32 cho Arduino IDE đã có sẵn phần này, không cần cài thêm thư viện.

Sơ đồ ESP32 hỏi giờ máy chủ NTP qua router Wi-Fi, nhận giờ UTC rồi cộng múi giờ Việt Nam UTC+7
ESP32 hỏi giờ từ máy chủ NTP qua Wi-Fi, sau đó tự cộng múi giờ và đếm tiếp (hình vẽ minh họa).
  • Máy chủ trả về giờ UTC. Việt Nam ở múi giờ UTC+7 và không đổi giờ mùa hè, nên giờ Việt Nam bằng giờ UTC cộng 7 giờ.
  • Sau khi nhận giờ, đồng hồ bên trong ESP32 tự đếm tiếp. Không cần hỏi máy chủ mỗi giây.
  • ESP32 tự hỏi lại máy chủ theo chu kỳ để sửa sai lệch. Với gói esp32 phiên bản 3.3.10, chu kỳ mặc định là 3 giờ.
  • Khi mất điện hoặc cấp nguồn lại, đồng hồ quay về ngày 01/01/1970 cho đến lần đồng bộ kế tiếp.

2. Linh kiện cần chuẩn bị

3. Lấy giờ và in ra Serial Monitor

Phần này chưa cần màn hình: chỉ cần mạch ESP32 nối với máy tính. Sửa TEN_WIFI và MAT_KHAU_WIFI thành tên và mật khẩu Wi-Fi của bạn, chọn board DOIT ESP32 DEVKIT V1 rồi nạp chương trình:

/*
  PYWORLD.VN - ESP32 lấy giờ Internet bằng NTP và in ra Serial Monitor
  Chỉ cần mạch ESP32 và mạng Wi-Fi 2,4 GHz có Internet
  Không cần cài thêm thư viện
*/

#include <WiFi.h>
#include <time.h>

const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";

const long MUI_GIO_GIAY = 7 * 3600;   // Việt Nam: UTC+7
const int GIO_MUA_HE_GIAY = 0;        // Việt Nam không đổi giờ mùa hè

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

  Serial.printf("Dang ket noi WiFi %s", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nDa ket noi, IP: %s\n", WiFi.localIP().toString().c_str());

  // Khai báo múi giờ và máy chủ NTP; từ đây ESP32 tự hỏi giờ ở chế độ nền
  configTime(MUI_GIO_GIAY, GIO_MUA_HE_GIAY, "pool.ntp.org", "time.google.com");

  struct tm thoiGian;
  if (getLocalTime(&thoiGian, 10000)) {
    Serial.println("Da dong bo gio qua NTP.");
  } else {
    Serial.println("Chua lay duoc gio. Kiem tra ket noi Internet.");
  }
}

void loop() {
  struct tm thoiGian;
  if (getLocalTime(&thoiGian, 100)) {
    char chuoi[32];
    strftime(chuoi, sizeof(chuoi), "%d/%m/%Y %H:%M:%S", &thoiGian);
    Serial.println(chuoi);
  } else {
    Serial.println("Dang cho dong bo gio...");
  }
  delay(1000);
}

Mở Serial Monitor ở tốc độ 115200 baud. Sau vài giây kết nối, chương trình in ngày giờ mỗi giây:

Hình minh họa Serial Monitor khi ESP32 kết nối Wi-Fi, đồng bộ giờ qua NTP và in ngày giờ mỗi giây
Kết quả mong đợi trên Serial Monitor (hình minh họa, ngày giờ là số ví dụ).

4. Giải thích các lệnh chính

  • configTime(MUI_GIO_GIAY, GIO_MUA_HE_GIAY, "pool.ntp.org", "time.google.com"): khai báo độ lệch múi giờ tính bằng giây (7 × 3600 = 25200), độ lệch giờ mùa hè (0) và tối đa ba máy chủ NTP. Lệnh này chỉ khởi động việc đồng bộ ở chế độ nền, không chờ kết quả.
  • getLocalTime(&thoiGian, 10000): chờ tối đa 10 giây cho đến khi có giờ hợp lệ rồi ghi vào biến kiểu struct tm. Hàm trả về false nếu hết thời gian chờ mà chưa đồng bộ được.
  • struct tm: chứa từng thành phần thời gian. Cần nhớ ba chỗ dễ nhầm: tm_mon đếm từ 0 (tháng 1 là 0), tm_year là số năm tính từ 1900 và tm_wday bằng 0 là Chủ nhật.
  • strftime(chuoi, sizeof(chuoi), "%d/%m/%Y %H:%M:%S", &thoiGian): ghép thời gian thành chuỗi theo mẫu định dạng.
Các ký hiệu định dạng thường dùng với strftime()
Ký hiệuÝ nghĩaVí dụ
%dNgày trong tháng, 2 chữ số11
%mTháng, 2 chữ số10
%Y / %yNăm 4 chữ số / 2 chữ số2026 / 26
%H / %IGiờ kiểu 24 giờ / kiểu 12 giờ22 / 10
%MPhút25
%SGiây40
%pAM hoặc PMPM
%A / %BTên thứ / tên tháng bằng tiếng AnhSunday / October

Vì strftime() chỉ cho tên thứ bằng tiếng Anh, chương trình đồng hồ ở mục 7 dùng một mảng tên thứ riêng.

5. Sơ đồ nối màn hình OLED 0.96 inch

Đấu nối màn hình OLED 0.96 inch SSD1306 với ESP32 DevKit V1
OLED 0.96 inchESP32 DevKit V1Ghi chú
VCC3V3Cấp 3,3 V để bus I2C cùng mức logic với ESP32.
GNDGNDMass chung.
SCLD22 / GPIO22Xung clock I2C.
SDAD21 / GPIO21Dữ liệu I2C.
Sơ đồ nối ESP32 DevKit V1 với màn hình OLED 0.96 inch SSD1306 qua I2C: VCC vào 3V3, SCL vào GPIO22, SDA vào GPIO21
Sơ đồ nối ESP32 DevKit V1 với màn hình OLED 0.96 inch (hình vẽ minh họa): VCC – 3V3, GND – GND, SCL – D22, SDA – D21.

Thứ tự bốn chân trên màn hình có thể khác nhau theo lô hàng (GND–VCC hoặc VCC–GND ở hai chân đầu), vì vậy luôn nối theo tên chân in trên mạch. Cắm ngược VCC và GND có thể làm hỏng màn hình.

Nếu dùng mạch ESP32 đã gắn sẵn màn hình OLED thì không cần nối dây, chỉ cần sửa hai số trong lệnh Wire.begin() theo chân I2C của mạch. Ví dụ mô đun ESP32 tích hợp màn hình OLED 0.96 inch và pin 18650 dùng SDA = GPIO5, SCL = GPIO4, tức là Wire.begin(5, 4).

6. Cài thư viện cho màn hình

  1. Mở Tools > Manage Libraries, tìm Adafruit SSD1306 và bấm Install.
  2. Khi IDE hỏi cài thêm thư viện phụ thuộc, chọn Install All để cài kèm Adafruit GFX Library và Adafruit BusIO.

Bài dùng gói esp32 phiên bản 3.3.10, thư viện Adafruit SSD1306 2.5.17, Adafruit GFX Library 1.12.6 và Adafruit BusIO 1.17.4.

7. Chương trình đồng hồ trên màn hình OLED

Chương trình kết nối Wi-Fi, khởi động đồng bộ NTP rồi vẽ lại màn hình mỗi khi sang giây mới:

/*
  PYWORLD.VN - Đồng hồ Internet: ESP32 + NTP + OLED 0.96 inch SSD1306 (I2C)
  OLED: VCC = 3V3, GND = GND, SDA = GPIO21, SCL = GPIO22, địa chỉ 0x3C
  Thư viện: Adafruit SSD1306, Adafruit GFX
*/

#include <WiFi.h>
#include <Wire.h>
#include <time.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const char *WIFI_SSID = "TEN_WIFI";
const char *WIFI_PASS = "MAT_KHAU_WIFI";

const long MUI_GIO_GIAY = 7 * 3600;   // Việt Nam: UTC+7
#define OLED_ADDR 0x3C

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const char *THU[7] = {"Chu nhat", "Thu hai", "Thu ba", "Thu tu", "Thu nam", "Thu sau", "Thu bay"};
int giayDaVe = -1;

void hienThongBao(const char *dong1, const char *dong2) {
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setCursor(0, 20);
  oled.print(dong1);
  oled.setCursor(0, 36);
  oled.print(dong2);
  oled.display();
}

void veDongHo(const struct tm &t) {
  char dong[24];
  oled.clearDisplay();

  // Dòng trên cùng: thứ, ngày/tháng/năm, canh giữa
  snprintf(dong, sizeof(dong), "%s %02d/%02d/%04d", THU[t.tm_wday], t.tm_mday, t.tm_mon + 1, t.tm_year + 1900);
  oled.setTextSize(1);
  oled.setCursor((128 - 6 * (int)strlen(dong)) / 2, 4);
  oled.print(dong);

  // Giờ:phút cỡ lớn, giây cỡ nhỏ hơn
  snprintf(dong, sizeof(dong), "%02d:%02d", t.tm_hour, t.tm_min);
  oled.setTextSize(3);
  oled.setCursor(0, 24);
  oled.print(dong);
  snprintf(dong, sizeof(dong), ":%02d", t.tm_sec);
  oled.setTextSize(2);
  oled.setCursor(92, 32);
  oled.print(dong);

  // Dòng dưới cùng: tình trạng Wi-Fi
  oled.setTextSize(1);
  oled.setCursor(0, 56);
  oled.print(WiFi.status() == WL_CONNECTED ? "WiFi: da ket noi" : "WiFi: mat ket noi");

  oled.display();
}

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

  Wire.begin(21, 22);
  if (!oled.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("Khong tim thay OLED! Kiem tra day noi va dia chi I2C.");
    while (true) delay(1000);
  }
  oled.setTextColor(SSD1306_WHITE);

  hienThongBao("Dang ket noi WiFi...", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  hienThongBao("Dang lay gio NTP...", "pool.ntp.org");
  configTime(MUI_GIO_GIAY, 0, "pool.ntp.org", "time.google.com");
}

void loop() {
  struct tm thoiGian;
  if (!getLocalTime(&thoiGian, 100)) {
    return;   // Chưa có giờ: giữ nguyên thông báo đang hiển thị
  }

  // Chỉ vẽ lại khi sang giây mới
  if (thoiGian.tm_sec != giayDaVe) {
    giayDaVe = thoiGian.tm_sec;
    veDongHo(thoiGian);
  }
  delay(50);
}
Hình mô phỏng màn hình OLED 128 x 64 hiển thị thứ, ngày tháng ở vùng vàng, giờ phút giây cỡ lớn và dòng trạng thái Wi-Fi
Bố cục đồng hồ trên màn hình OLED (hình mô phỏng theo toạ độ và bộ chữ của chương trình, giờ là số ví dụ).
  • Bố cục: màn hình rộng 128 và cao 64 điểm ảnh. Mỗi ký tự cỡ chữ 1 chiếm 6 × 8 điểm ảnh; cỡ 2 và cỡ 3 lớn gấp đôi và gấp ba. Năm ký tự giờ:phút cỡ 3 chiếm 90 điểm ảnh, ba ký tự :giây cỡ 2 chiếm 36 điểm ảnh, vừa đủ một hàng.
  • Màn hình hai màu: với loại vàng – xanh, 16 hàng điểm ảnh trên cùng luôn sáng màu vàng, phần còn lại màu xanh. Màu do vị trí trên tấm nền quyết định, chương trình không chọn được. Bài đặt dòng ngày tháng trong vùng vàng.
  • Canh giữa dòng ngày tháng: vị trí bắt đầu bằng (128 − 6 × số ký tự) / 2.
  • Chỉ vẽ lại khi cần: biến giayDaVe nhớ giây đã vẽ; chương trình chỉ gửi dữ liệu ra màn hình khi giây thay đổi.
  • Chữ không dấu: bộ chữ mặc định của thư viện không có dấu tiếng Việt, nên tên thứ viết không dấu.

8. Điều gì xảy ra khi mất Wi-Fi?

Đồng hồ không dừng. Sau lần đồng bộ đầu tiên, ESP32 tự đếm thời gian bằng bộ dao động thạch anh của nó; Wi-Fi chỉ cần cho những lần đồng bộ lại. Khi mất mạng, dòng dưới cùng đổi thành WiFi: mat ket noi, giờ vẫn chạy và ESP32 tự kết nối lại khi mạng có trở lại.

Nếu ESP32 bị mất nguồn, toàn bộ quá trình bắt đầu lại từ đầu và cần Internet để lấy giờ. Thiết bị phải giữ giờ qua những lần mất điện mà không có mạng thì cần thêm một module đồng hồ thời gian thực có pin riêng.

9. Một số tùy chỉnh

  • Đổi máy chủ NTP: thay "pool.ntp.org" bằng "asia.pool.ntp.org" hoặc máy chủ nội bộ của cơ quan. Nên khai báo ít nhất hai máy chủ để có phương án dự phòng.
  • Múi giờ khác: đổi MUI_GIO_GIAY thành số giờ lệch so với UTC nhân 3600.
  • Nơi có giờ mùa hè: dùng configTzTime() với chuỗi múi giờ kiểu POSIX để ESP32 tự đổi giờ, ví dụ configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org") cho Trung Âu. Chuỗi tương ứng của Việt Nam là "<+07>-7".
  • Kiểu 12 giờ: lấy giờ bằng t.tm_hour % 12, đổi 0 thành 12 và hiển thị thêm AM hoặc PM theo t.tm_hour < 12.

10. Xử lý lỗi thường gặp

  • Dừng mãi ở dòng "Dang ket noi WiFi": kiểm tra tên và mật khẩu Wi-Fi (có phân biệt chữ hoa, chữ thường), bảo đảm router đang phát băng tần 2,4 GHz và đặt mạch gần router hơn.
  • Báo "Chua lay duoc gio": Wi-Fi đã kết nối nhưng không ra được Internet, hoặc mạng chặn cổng UDP 123. Thử máy chủ khác hoặc thử bằng điểm phát Wi-Fi của điện thoại.
  • Hiển thị năm 1970: chương trình đã dùng giờ trước khi đồng bộ xong. Luôn kiểm tra giá trị trả về của getLocalTime().
  • Giờ lệch đúng 7 giờ hoặc 1 giờ: sai tham số của configTime(). Với Việt Nam, tham số thứ nhất là 25200 và tham số thứ hai là 0.
  • Serial Monitor báo "Khong tim thay OLED!": kiểm tra bốn dây, nhất là SCL và SDA có bị đổi chỗ không. Một số màn hình dùng địa chỉ 0x3D thay cho 0x3C.
  • Hình bị lệch, có dải nhiễu ở mép màn hình: màn hình dùng chip SH1106 (thường gặp ở loại 1.3 inch) chứ không phải SSD1306. Xem hướng dẫn dùng màn hình OLED 1.3 inch SH1106 với ESP32.
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: dùng màn hình OLED SSD1306 với ESP8266, dùng màn hình LCD 2004 I2C với ESP32 và ESP32 làm web server điều khiển relay qua Wi-Fi.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

3 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

4 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

4 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

4 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

4 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

4 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.

Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng
Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng

4 Lượt xem

Hướng dẫn các thao tác Wi-Fi cần dùng nhất trên ESP32 với Arduino IDE: quét mạng, kết nối có thời gian chờ, xem IP và cường độ sóng, tự kết nối lại, đặt IP tĩnh, phát Wi-Fi riêng.


Bình luận
Đã thêm vào giỏ hàng