Đồ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.

⏱ 90 phút🔧 Biết hàn💰 Linh kiện ~180.600đ🧠 Arduino Nano⚡ Nguồn USB 5V

✅ Đã kiểm thử

Luật điệnKhông lỗiSai chân, chập nguồn, quá áp 5V vào chân 3,3V, thiếu đi-ốt, thiếu trở kéo…
Mô phỏng SPICE2/2 kịch bản · 10 phép đo đạtDòng/áp từng linh kiện ở trường hợp xấu nhất (ngspice)
Biên dịch codeĐạt · dùng 41% bộ nhớarduino:avr:nano
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 ✓
Kiểm lần cuối: 12:02:53 12/11/2026

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.

5V5VGNDGNDSDASDASCLSCLBTN_SETBTN_SETBTN_UPBTN_UPBZ_DRVBZ_DRVBZ_BBZ_BBZ_CBZ_CU1 · Arduino Nano5VA4A5D2D3D8GNDU2 · thời gian thực DS3231VCCSCLSDAGNDU3 · Màn hình LCD 1602 kèm mạch I2CVCCSCLSDAGNDSW1 · Nút nhấn12SW2 · Nút nhấn12R1 · Điện trở 1kΩ12Q1 · Transistor NPN S8050B (gốc)C (góp)E (phát)BZ1 · Còi chip chủ động 5V-+
#DâyNối các điểm
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)
3vàng
SDA
U1 A4 ⟷ U2 SDA ⟷ U3 SDA
4xanh lá
SCL
U1 A5 ⟷ U2 SCL ⟷ U3 SCL
5xanh dương
BTN_SET
U1 D2 ⟷ SW1 1
6tím
BTN_UP
U1 D3 ⟷ SW2 1
7xanh ngọc
BZ_DRV
U1 D8 ⟷ R1 1
8nâu
BZ_B
R1 2 ⟷ Q1 B (gốc)
9hồng
BZ_C
BZ1 - ⟷ Q1 C (góp)

Danh sách linh kiện

Ký hiệuLinh kiệnGiá tham khảo
U1Arduino Nano V3 (ATmega328P, chip nạp CH340) kèm cáp USB85.000đTìm mua
U2Module thời gian thực DS3231 (loại có hộc pin CR2032) + pin CR203235.000đTìm mua
U3Màn hình LCD 1602 nền xanh kèm mạch chuyển I2C (PCF8574) hàn sẵn55.000đTìm mua
SW1Nút nhấn 4 chân 6×6mm (nút CHỈNH)1.000đTìm mua
SW2Nút nhấn 4 chân 6×6mm (nút TĂNG)1.000đTìm mua
BZ1Còi chip chủ động 5V (loại có dán tem, tự kêu khi cấp điện)3.000đTìm mua
Q1Transistor 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);
}

Đo kiểm bằng đồng hồ vạn năng

Chân VCC và GND của LCD4,7 – 5,1V
Chân SDA với GND khi đang chạykhoảng 4,5 – 5V (dao động nhẹ khi có dữ liệu)
Chân D2 với GND khi không bấmkhoả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.