Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
- 1. NTP là gì và ESP32 dùng nó thế nào?
- 2. Linh kiện cần chuẩn bị
- 3. Lấy giờ và in ra Serial Monitor
- 4. Giải thích các lệnh chính
- 5. Sơ đồ nối màn hình OLED 0.96 inch
- 6. Cài thư viện cho màn hình
- 7. Chương trình đồng hồ trên màn hình OLED
- 8. Điều gì xảy ra khi mất Wi-Fi?
- 9. Một số tùy chỉnh
- 10. Xử lý lỗi thường gặp
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.
- 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ị
- ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340) và cáp USB có truyền dữ liệu.
- Màn hình OLED 0.96 inch SSD1306 giao tiếp I2C (dùng từ mục 5).
- Breadboard MB-102 và dây cắm test board.
- Mạng Wi-Fi 2,4 GHz có kết nối Internet. ESP32 không dùng được mạng 5 GHz.
- Máy tính đã cài Arduino IDE 2.x và gói esp32 by Espressif Systems.
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:
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ểustruct tm. Hàm trả vềfalsenế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_yearlà số năm tính từ 1900 vàtm_wdaybằ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.
| Ký hiệu | Ý nghĩa | Ví dụ |
|---|---|---|
%d | Ngày trong tháng, 2 chữ số | 11 |
%m | Tháng, 2 chữ số | 10 |
%Y / %y | Năm 4 chữ số / 2 chữ số | 2026 / 26 |
%H / %I | Giờ kiểu 24 giờ / kiểu 12 giờ | 22 / 10 |
%M | Phút | 25 |
%S | Giây | 40 |
%p | AM hoặc PM | PM |
%A / %B | Tên thứ / tên tháng bằng tiếng Anh | Sunday / 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
| OLED 0.96 inch | ESP32 DevKit V1 | Ghi chú |
|---|---|---|
| VCC | 3V3 | Cấp 3,3 V để bus I2C cùng mức logic với ESP32. |
| GND | GND | Mass chung. |
| SCL | D22 / GPIO22 | Xung clock I2C. |
| SDA | D21 / GPIO21 | Dữ liệu I2C. |
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
- Mở Tools > Manage Libraries, tìm Adafruit SSD1306 và bấm Install.
- 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);
}
- 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
giayDaVenhớ 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_GIAYthà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 theot.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.
Xem thêm