Đồng hồ để bàn có báo thức và nhiệt độ (Arduino Nano + DS3231 + LCD1602)
Đồng hồ chạy chính xác (sai lệch chỉ vài giây mỗi tháng) nhờ module thời gian thực DS3231 có pin nuôi: mất điện vẫn giữ giờ. Màn hình LCD hiện giờ, thứ, ngày tháng và nhiệt độ phòng; có báo thức bằng còi, chỉnh giờ bằng 2 nút nhấn.
✅ Đã kiểm thử
Xem chi tiết 10 phép đo
Đang báo thức (còi kêu) và đang bấm nút
- Q1: dẫn 29.2mA, Vce=0.03V (bão hòa tốt).
- Chân D8 cấp 4.1mA (an toàn ≤ 20.0mA).
- Tổng dòng đỉnh từ USB 95.3mA (đủ với cục sạc 5V 1A).
- Dòng qua còi (transistor cấp): 29.2mA ✓
- Dòng chân D8 phải cấp: 4.1mA ✓
- Nút CHỈNH khi bấm đọc mức thấp: 0.00V ✓
- Đường I2C SDA ở mức nghỉ (kéo lên 5V): 5.00V ✓
Bình thường (còi tắt, không bấm nút)
- Tổng dòng đỉnh từ USB 62.0mA (đủ với cục sạc 5V 1A).
- Còi im lặng: 0.0mA ✓
- Nút TĂNG khi thả đọc mức cao: 4.98V ✓
Nguyên lý hoạt động
DS3231 là chip đồng hồ có thạch anh bù nhiệt bên trong, sai số khoảng ±2 phút mỗi năm, có pin CR2032 nuôi nên rút điện vẫn chạy. Arduino đọc giờ từ DS3231 và gửi chữ ra LCD qua cùng 2 dây I2C (SDA ở A4, SCL ở A5) — mỗi module có địa chỉ riêng nên dùng chung được. Còi chủ động ăn khoảng 30mA, nhiều hơn mức nên lấy từ chân Arduino, nên chân D8 chỉ cấp ~4mA mở transistor S8050 để transistor đóng điện cho còi. Hai nút nhấn dùng trở kéo lên bên trong chip.
Sơ đồ đấu dây
Mỗi màu là một dây. Chấm tròn là điểm hàn. Chạm vào tên dây trong bảng để làm nổi bật.
Đây là mô phỏng một chiều (DC) ở trạng thái tĩnh: dùng để kiểm tra mức áp, dòng và công suất. Hiệu ứng nhấp nháy, PWM hay giao tiếp số do code quyết định nên không chạy ở đây.
| # | Dây | Nối các điểm | Áp mô phỏng |
|---|---|---|---|
| 1 | đỏ 5V | U1 5V ⟷ U2 VCC ⟷ U3 VCC ⟷ BZ1 + | — |
| 2 | đen GND | U1 GND ⟷ U2 GND ⟷ U3 GND ⟷ SW1 2 ⟷ SW2 2 ⟷ Q1 E (phát) | — |
| 3 | vàng SDA | U1 A4 ⟷ U2 SDA ⟷ U3 SDA | — |
| 4 | xanh lá SCL | U1 A5 ⟷ U2 SCL ⟷ U3 SCL | — |
| 5 | xanh dương BTN_SET | U1 D2 ⟷ SW1 1 | — |
| 6 | tím BTN_UP | U1 D3 ⟷ SW2 1 | — |
| 7 | xanh ngọc BZ_DRV | U1 D8 ⟷ R1 1 | — |
| 8 | nâu BZ_B | R1 2 ⟷ Q1 B (gốc) | — |
| 9 | hồng BZ_C | BZ1 - ⟷ Q1 C (góp) | — |
Danh sách linh kiện
| Ký hiệu | Linh kiện | Giá tham khảo | |
|---|---|---|---|
| U1 | Arduino Nano V3 (ATmega328P, chip nạp CH340) kèm cáp USB | 85.000đ | Tìm mua |
| U2 | Module thời gian thực DS3231 (loại có hộc pin CR2032) + pin CR2032 | 35.000đ | Tìm mua |
| U3 | Màn hình LCD 1602 nền xanh kèm mạch chuyển I2C (PCF8574) hàn sẵn | 55.000đ | Tìm mua |
| SW1 | Nút nhấn 4 chân 6×6mm (nút CHỈNH) | 1.000đ | Tìm mua |
| SW2 | Nút nhấn 4 chân 6×6mm (nút TĂNG) | 1.000đ | Tìm mua |
| BZ1 | Còi chip chủ động 5V (loại có dán tem, tự kêu khi cấp điện) | 3.000đ | Tìm mua |
| Q1 | Transistor S8050 (TO-92) | 500đ | Tìm mua |
| R1 | Điện trở 1kΩ 1/4W (nâu-đen-đỏ) | 100đ | Tìm mua |
| Tổng (chưa gồm board hàn, dây, thiếc) | 180.600đ |
Các bước làm
Code nạp cho mạch
Cài thư viện: RTClib, LiquidCrystal I2C (Arduino IDE → Library Manager). Chọn board Arduino Nano.
/*
Mạch Gọn — Đồng hồ để bàn có báo thức và nhiệt độ
Board : Arduino Nano
Module: DS3231 (I2C), LCD1602 I2C (PCF8574, địa chỉ 0x27 hoặc 0x3F — tự dò)
Chân : A4 = SDA, A5 = SCL, D2 = nút CHỈNH, D3 = nút TĂNG, D8 -> 1k -> S8050 -> còi
Thư viện: RTClib (Adafruit), LiquidCrystal I2C (Frank de Brabander)
*/
#include <Wire.h>
#include <EEPROM.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
const uint8_t PIN_SET = 2, PIN_UP = 3, PIN_BUZZ = 8;
const unsigned long DEBOUNCE_MS = 30, REPEAT_DELAY = 600, REPEAT_RATE = 150;
const unsigned long SET_TIMEOUT = 15000, ALARM_LEN = 60000;
const int EE_MAGIC = 0, EE_AH = 1, EE_AM = 2, EE_AON = 3;
RTC_DS3231 rtc;
LiquidCrystal_I2C* lcd = nullptr;
bool rtcOk = false;
const char* DAYS[] = {"CN", "T2", "T3", "T4", "T5", "T6", "T7"};
const uint8_t DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
enum Mode { M_RUN, M_HOUR, M_MIN, M_DAY, M_MONTH, M_YEAR, M_AHOUR, M_AMIN, M_AON, M_COUNT };
Mode mode = M_RUN;
int eH, eM, eD, eMo, eY; // giá trị đang chỉnh
uint8_t alarmH = 6, alarmM = 30;
bool alarmOn = false;
bool ringing = false;
unsigned long ringStart = 0, lastTouch = 0;
int lastAlarmMinute = -1;
struct Button { uint8_t pin; bool stable, last; unsigned long changed, pressedAt, lastRepeat; };
Button bSet = {PIN_SET, HIGH, HIGH, 0, 0, 0}, bUp = {PIN_UP, HIGH, HIGH, 0, 0, 0};
// 1 = vừa bấm, 2 = giữ (lặp), 0 = không
uint8_t readButton(Button& b) {
bool r = digitalRead(b.pin);
unsigned long now = millis();
if (r != b.last) { b.last = r; b.changed = now; }
if (now - b.changed >= DEBOUNCE_MS && r != b.stable) {
b.stable = r;
if (r == LOW) { b.pressedAt = now; b.lastRepeat = now; return 1; }
}
if (b.stable == LOW && now - b.pressedAt > REPEAT_DELAY && now - b.lastRepeat > REPEAT_RATE) { b.lastRepeat = now; return 2; }
return 0;
}
uint8_t daysIn(int m, int y) {
if (m == 2 && ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)) return 29;
return DAYS_IN_MONTH[m - 1];
}
void loadAlarm() {
if (EEPROM.read(EE_MAGIC) != 0xA5) return;
alarmH = EEPROM.read(EE_AH) % 24;
alarmM = EEPROM.read(EE_AM) % 60;
alarmOn = EEPROM.read(EE_AON) == 1;
}
void saveAlarm() {
EEPROM.update(EE_MAGIC, 0xA5); EEPROM.update(EE_AH, alarmH);
EEPROM.update(EE_AM, alarmM); EEPROM.update(EE_AON, alarmOn ? 1 : 0);
}
uint8_t findLcd() {
for (uint8_t a : (const uint8_t[]){0x27, 0x3F, 0x20, 0x38}) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) return a;
}
return 0x27;
}
void startEdit() {
DateTime n = rtc.now();
eH = n.hour(); eM = n.minute(); eD = n.day(); eMo = n.month(); eY = n.year();
}
void finishEdit() {
if (rtcOk) rtc.adjust(DateTime(eY, eMo, eD, eH, eM, 0));
saveAlarm();
mode = M_RUN;
lcd->clear();
Serial.println(F("Da luu gio va bao thuc"));
}
void increment() {
switch (mode) {
case M_HOUR: eH = (eH + 1) % 24; break;
case M_MIN: eM = (eM + 1) % 60; break;
case M_DAY: eD = eD % daysIn(eMo, eY) + 1; break;
case M_MONTH: eMo = eMo % 12 + 1; if (eD > daysIn(eMo, eY)) eD = daysIn(eMo, eY); break;
case M_YEAR: eY = eY >= 2099 ? 2024 : eY + 1; if (eD > daysIn(eMo, eY)) eD = daysIn(eMo, eY); break;
case M_AHOUR: alarmH = (alarmH + 1) % 24; break;
case M_AMIN: alarmM = (alarmM + 1) % 60; break;
case M_AON: alarmOn = !alarmOn; break;
default: break;
}
}
void drawRun(const DateTime& n) {
char l1[17], l2[17];
int t10 = rtcOk ? (int)(rtc.getTemperature() * 10) : 0;
snprintf(l1, sizeof l1, "%02d:%02d:%02d %3d.%dC", n.hour(), n.minute(), n.second(), t10 / 10, abs(t10 % 10));
snprintf(l2, sizeof l2, "%s %02d/%02d/%04d %c", DAYS[n.dayOfTheWeek()], n.day(), n.month(), n.year(), alarmOn ? '*' : ' ');
if (ringing) snprintf(l2, sizeof l2, " ** BAO THUC ** ");
lcd->setCursor(0, 0); lcd->print(l1);
lcd->setCursor(0, 1); lcd->print(l2);
}
void drawEdit() {
static const char* const TITLES[] = {"", "Chinh GIO", "Chinh PHUT", "Chinh NGAY", "Chinh THANG", "Chinh NAM",
"Bao thuc: GIO", "Bao thuc: PHUT", "Bao thuc: BAT?"};
char l1[17], l2[17];
snprintf(l1, sizeof l1, "%-16s", TITLES[mode]);
bool blinkOff = (millis() / 400) % 2;
switch (mode) {
case M_HOUR: case M_MIN: snprintf(l2, sizeof l2, " %02d:%02d ", eH, eM); break;
case M_DAY: case M_MONTH: case M_YEAR: snprintf(l2, sizeof l2, " %02d/%02d/%04d ", eD, eMo, eY); break;
case M_AHOUR: case M_AMIN: snprintf(l2, sizeof l2, " %02d:%02d ", alarmH, alarmM); break;
default: snprintf(l2, sizeof l2, " %s ", alarmOn ? "BAT" : "TAT"); break;
}
// nháy phần đang chỉnh
if (blinkOff) {
int s = -1, len = 2;
if (mode == M_HOUR || mode == M_AHOUR) s = 5;
if (mode == M_MIN || mode == M_AMIN) s = 8;
if (mode == M_DAY) s = 3;
if (mode == M_MONTH) s = 6;
if (mode == M_YEAR) { s = 9; len = 4; }
if (mode == M_AON) { s = 6; len = 3; }
for (int i = 0; i < len && s >= 0; i++) l2[s + i] = ' ';
}
lcd->setCursor(0, 0); lcd->print(l1);
lcd->setCursor(0, 1); lcd->print(l2);
}
void setup() {
Serial.begin(115200);
pinMode(PIN_SET, INPUT_PULLUP);
pinMode(PIN_UP, INPUT_PULLUP);
pinMode(PIN_BUZZ, OUTPUT);
digitalWrite(PIN_BUZZ, LOW);
Wire.begin();
uint8_t addr = findLcd();
Serial.print(F("LCD dia chi 0x")); Serial.println(addr, HEX);
lcd = new LiquidCrystal_I2C(addr, 16, 2);
lcd->init();
lcd->backlight();
rtcOk = rtc.begin();
if (!rtcOk) {
lcd->print(F("Loi DS3231"));
lcd->setCursor(0, 1); lcd->print(F("Kiem tra A4/A5"));
Serial.println(F("Khong tim thay DS3231"));
} else if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // lấy giờ lúc biên dịch
Serial.println(F("DS3231 mat gio, da dat theo gio bien dich"));
}
loadAlarm();
}
void loop() {
uint8_t s = readButton(bSet), u = readButton(bUp);
unsigned long now = millis();
if (ringing && (s || u)) { ringing = false; digitalWrite(PIN_BUZZ, LOW); s = u = 0; lcd->clear(); }
if (s == 1) {
lastTouch = now;
if (mode == M_RUN) { if (rtcOk) { startEdit(); mode = M_HOUR; lcd->clear(); } }
else if (mode == M_AON) finishEdit();
else { mode = (Mode)(mode + 1); lcd->clear(); }
}
if (u && mode != M_RUN) { lastTouch = now; if (!(mode == M_AON && u == 2)) increment(); }
if (mode != M_RUN && now - lastTouch > SET_TIMEOUT) finishEdit();
static unsigned long lastDraw = 0;
if (now - lastDraw >= 200) {
lastDraw = now;
if (!rtcOk) return;
DateTime n = rtc.now();
if (mode == M_RUN) {
// kiểm tra báo thức (mỗi phút chỉ kích 1 lần)
if (alarmOn && n.hour() == alarmH && n.minute() == alarmM && n.minute() != lastAlarmMinute) {
lastAlarmMinute = n.minute();
ringing = true; ringStart = now;
Serial.println(F("BAO THUC!"));
}
if (n.minute() != alarmM) lastAlarmMinute = -1;
drawRun(n);
} else drawEdit();
}
// còi kêu ngắt quãng 4 tiếng/giây, tự tắt sau 1 phút
if (ringing) {
if (now - ringStart > ALARM_LEN) { ringing = false; lcd->clear(); }
digitalWrite(PIN_BUZZ, ringing && ((now / 125) % 2 == 0) && ((now / 1000) % 2 == 0));
} else digitalWrite(PIN_BUZZ, LOW);
}
🔌 Cắm thử trên breadboard
Cắm nháp trên board cắm trước cho chắc, chạy đúng rồi hãy làm mạch in. Sơ đồ này do máy tự xếp từ chính netlist ở trên và đã đối chiếu lại: hai chân cùng một cột (cùng nửa board) là thông nhau, không có net nào bị chập.
- Mỗi cột 5 lỗ trong cùng một nửa board thông nhau; hai nửa cách nhau bởi rãnh giữa.
- Hai đường kẻ dọc mép board là đường nguồn: hàng + (vạch đỏ) và hàng − (vạch xanh). Nhớ bắc dây nối đường nguồn trên với đường nguồn dưới nếu dùng cả hai nửa.
- Màu dây trên hình trùng với màu trong bảng nối dây ở mục Sơ đồ, cắm theo màu cho dễ dò.
🧲 Mạch in để ủi (PCB 1 mặt)
Bản mạch in một mặt đồng, tự xếp linh kiện và tự đi dây, đã kiểm quy tắc khoảng cách và đối chiếu đúng 100% với sơ đồ nguyên lý ở trên. In tỉ lệ 1:1 rồi ủi lên tấm đồng.
- net GND: SW1.2 → U2.GND
Hướng dẫn ủi – ăn mòn – khoan – hàn (đầy đủ)
Hướng dẫn làm mạch in bằng phương pháp ủi
Mạch: Đồng hồ để bàn có báo thức và nhiệt độ (Arduino Nano + DS3231 + LCD1602)
Kích thước board: 48,3×53,3 mm · 53 lỗ khoan (mũi 0,8, 0,9 và 1 mm) · 24 đoạn mạch, tổng 357,4 mm · 1 cầu nhảy.
Quy tắc đã kiểm: đường mạch rộng 0,8mm (nguồn và GND 1mm), khe cách tối thiểu 0,5mm. Mạch in đã được máy đối chiếu đúng 100% với sơ đồ nguyên lý.
1. In đúng tỉ lệ 1:1
- In trang "MẶT ĐỒNG" bằng máy in LASER, đặt tỉ lệ 100%, KHÔNG chọn "Fit to page" hay "Scale to fit".
- Dùng thước trên bản in đo lại đoạn 100mm: phải đúng 100mm mới dùng được.
- In trên giấy in ảnh bóng, giấy decal bóc, hoặc giấy tạp chí láng. Mực càng dày càng tốt.
- Trang "MẶT ĐỒNG" đã được LẬT GƯƠNG sẵn: úp mặt có mực vào tấm đồng là đúng chiều.
2. Chuẩn bị tấm đồng
- Cắt phíp đồng (board đồng một mặt) lớn hơn kích thước mạch mỗi bề khoảng 5mm.
- Chà mặt đồng bằng giấy nhám mịn (P800–P1200) rồi rửa sạch, lau khô. Mặt đồng phải sáng, không vết dầu mỡ, không chạm tay vào.
3. Ủi mực sang đồng
- Úp mặt có mực vào tấm đồng, dùng băng keo giấy dán cố định một cạnh.
- Bàn ủi để mức nóng nhất (cotton/linen), KHÔNG dùng chế độ phun hơi nước.
- Ủi đè mạnh và đều 3–5 phút, tập trung từng vùng nhỏ, chú ý các góc và mép.
- Để nguội hẳn rồi ngâm cả tấm vào nước ấm 5–10 phút, bóc giấy thật nhẹ. Giấy decal thì bóc khi còn ấm.
- Soi kỹ: chỗ nào mực bị thiếu hoặc đứt đường thì tô lại bằng bút lông dầu (bút CD) rồi để khô.
4. Ăn mòn (etching)
- Pha dung dịch sắt(III) clorua FeCl3 theo hướng dẫn trên bao bì, hoặc dung dịch oxy già + muối ăn + a-xít; dùng hộp nhựa, KHÔNG dùng đồ kim loại.
- Nhiệt độ dung dịch khoảng 40–50°C thì ăn nhanh hơn. Lắc nhẹ hộp cho đều, thường 10–25 phút là xong.
- An toàn: đeo găng tay cao su và kính bảo hộ, làm ở nơi thoáng. Dung dịch ăn mòn làm hỏng quần áo, bồn rửa và gây bỏng rát da.
- Xong thì rửa board bằng nước sạch, lau khô, dùng xăng thơm hoặc axeton lau sạch lớp mực.
- Dung dịch đã dùng KHÔNG đổ xuống bồn rửa hay cống; đựng vào can kín và bỏ đúng nơi thu gom chất thải.
5. Khoan lỗ
- Đối chiếu trang "BẢN VẼ LỖ KHOAN" để biết lỗ nào dùng mũi nào.
- Dùng mũi khoan 0,8mm, 0,9mm và 1mm, khoan từ MẶT ĐỒNG vào để mũi tự định tâm giữa pad.
- Khoan tốc độ cao, đẩy nhẹ; mũi nhỏ rất dễ gãy. Khoan xong chà nhẹ bavia hai mặt.
6. Hàn linh kiện
- Linh kiện cắm ở MẶT KHÔNG CÓ ĐỒNG, hàn ở mặt đồng. Theo trang "MẶT LINH KIỆN" để đặt đúng vị trí và đúng chiều.
- Hàn linh kiện thấp trước (điện trở, đi-ốt), cao sau (tụ, hàng chân, board vi điều khiển).
- Chú ý chiều: LED chân dài là (+); đi-ốt có vạch trắng là K; tụ hoá có vạch là (−); transistor theo mặt chữ như nét vẽ đỏ.
- Mạch này còn 1 CẦU NHẢY: hàn đoạn dây đơn ở mặt linh kiện theo nét đỏ gạch trên bản vẽ:
- cầu nhảy net GND: từ SW1.2 sang U2.GND
- Trước khi cấp điện: đo thông mạch giữa chân nguồn và GND — KHÔNG được kêu (không được chập).
Lưu ý với board vi điều khiển
- Board nhái có thể khác thứ tự chân. Đối chiếu tên chân in cạnh từng lỗ trên trang "MẶT LINH KIỆN" với chữ in trên board thật TRƯỚC KHI ăn mòn. Nếu khác, đừng hàn mà báo lại để sửa bản vẽ.
Bản vẽ lỗ khoan
Đo kiểm bằng đồng hồ vạn năng
| Chân VCC và GND của LCD | 4,7 – 5,1V |
| Chân SDA với GND khi đang chạy | khoảng 4,5 – 5V (dao động nhẹ khi có dữ liệu) |
| Chân D2 với GND khi không bấm | khoảng 5V |
Sửa lỗi thường gặp
LCD sáng đèn nhưng không có chữ
Vặn biến trở tương phản phía sau LCD. Code tự dò địa chỉ 0x27 và 0x3F; mở Serial Monitor 115200 để xem địa chỉ tìm thấy.
Màn hình báo "Loi DS3231"
Kiểm tra SDA → A4, SCL → A5 và nguồn của DS3231. Hai dây SDA/SCL bị đảo là lỗi hay gặp nhất.
Rút điện cắm lại bị sai giờ
Pin CR2032 hết hoặc lắp ngược. Thay pin rồi chỉnh lại giờ bằng nút.
Còi không kêu khi tới giờ báo thức
Kiểm tra báo thức đang BẬT (dấu * cuối dòng 2), chiều chân còi và chân E-B-C của S8050.
⚠️ An toàn
- Chỉ dùng điện 5V từ cổng USB/cục sạc điện thoại.
- Không sạc pin CR2032: một số module DS3231 có mạch sạc pin; nếu dùng pin CR2032 (không sạc được) nên gỡ con điện trở 200Ω cạnh đi-ốt trên module hoặc dùng pin LIR2032.