IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series 37 Module Cảm Biến – DS3231 RTC: TCXO ±2ppm, I2C, Alarm Ngắt SQW & Đồng Hồ Thực Với Backup Pin

DS3231 là IC đồng hồ thời gian thực (RTC) chính xác nhất trong tầm giá cho dự án nhúng. Bí quyết là bộ dao động TCXO (Temperature Compensated Crystal Oscillator) tích hợp sẵn trên chip, loại bỏ sai số do nhiệt độ gây ra. Bài này giải thích tại sao DS3231 chính xác hơn DS1307, cách dùng thư viện RTClib Adafruit, và cách cài đặt alarm ngắt qua chân SQW.

Nguyên Lý Hoạt Động

1. Vấn Đề Với Crystal Thông Thường

Mọi RTC đều đếm thời gian dựa trên dao động của thạch anh (crystal):

  • Thạch anh chuẩn: 32.768kHz = 2^15 Hz → dễ chia thành 1Hz
  • Vấn đề: tần số dao động phụ thuộc nhiệt độ
Sai số tần số theo nhiệt độ:

Sai số (ppm)
+20 │ *.
    │    *.
0   │────────*────────  ← Điểm hoàn hảo (~25°C)
    │           *.
-20 │               *.
    ├───────────────────── Nhiệt độ (°C)
    0   10  20  30  40  50

Crystal thường: ±20ppm → ±10 phút/năm
DS3231 TCXO: ±2ppm → ±1 phút/năm

2. TCXO — Temperature Compensated Crystal Oscillator

DS3231 tích hợp:

  1. Crystal 32.768kHz trực tiếp trong package IC (không phải bên ngoài)
  2. Cảm biến nhiệt độ đọc nhiệt độ chip liên tục
  3. Mạch bù (compensation circuit) điều chỉnh tần số dao động theo nhiệt độ
Chuỗi bù nhiệt:

Cảm biến T → Lookup table → Điện áp bù → Varactor → Crystal tune
                                                        ↓
                                              32.768kHz chính xác

Kết quả: ±2ppm toàn dải 0°C–+40°C, ±3.5ppm toàn dải -40°C–+85°C

3. Cấu Trúc Thanh Ghi DS3231

DS3231 lưu thời gian trong các thanh ghi BCD (Binary Coded Decimal) qua I2C:

Địa chỉ  | Nội dung    | Range
0x00      | Seconds     | 00-59
0x01      | Minutes     | 00-59
0x02      | Hours       | 1-12/00-23
0x03      | Day of Week | 1-7
0x04      | Date        | 01-31
0x05      | Month       | 01-12
0x06      | Year        | 00-99 (2000-2099)
0x07-0x0A | Alarm 1     |
0x0B-0x0D | Alarm 2     |
0x11-0x12 | Temperature | signed 10-bit, 0.25°C resolution

BCD format: 27 phút = 0x27 (không phải 0x1B = 27 thập phân)

4. Alarm và Chân SQW

DS3231 có 2 alarm:

  • Alarm 1: phân giải 1 giây (có thể alarm theo giây, phút, giờ, ngày)
  • Alarm 2: phân giải 1 phút

Khi alarm kích hoạt:

  • Bit A1F hoặc A2F trong Status Register được set
  • Chân SQW kéo xuống LOW (open drain, cần pull-up)
  • Có thể kết nối SQW với GPIO interrupt của MCU
SQW pin — open drain output:

VCC
 │
[10kΩ]
 │
 ├───→ GPIO (interrupt)
 │
SQW ──┤
      └─── GND (khi alarm kích hoạt)

5. Backup Pin CR2032

Module DS3231 thường có socket pin CR2032:

  • Khi mất nguồn chính (VCC): DS3231 tự động chuyển sang pin
  • Dòng tiêu thụ chế độ backup: ~3μA
  • Pin CR2032 3V/225mAh: ~225mAh / 3μA = 75,000 giờ ≈ 8.5 năm

Thông Số Kỹ Thuật

Thông sốGiá trị
Độ chính xác±2ppm (0°C–40°C) ≈ ±1 phút/năm
Giao tiếpI2C
Địa chỉ I2C0x68 (cố định, không thay đổi được)
Điện áp VCC2.3V – 5.5V
Dòng chế độ hoạt động~200μA
Dòng chế độ backup~3μA
Cảm biến nhiệt±3°C, độ phân giải 0.25°C
Pin backupCR2032 (3V) — thường kèm theo module
Alarms2 alarm (A1: 1 giây, A2: 1 phút)

Sơ Đồ Chân (Pinout)

Module DS3231 — 6 chân thông dụng:

┌─────────────────────────────────┐
│  DS3231 RTC Module              │
│  [CR2032 socket]                │
│  32K  SQW  SCL  SDA  VCC  GND  │
└──┬────┬────┬────┬────┬────┬─────┘
   │    │    │    │    │    │
  32K  SQW  SCL  SDA  VCC  GND
ChânMô tả
GNDMass
VCC3.3V hoặc 5V
SDAI2C Data
SCLI2C Clock
SQWSquare Wave / Interrupt output (open drain)
32K32.768kHz clock output

Cài Đặt Thư Viện

Trong Arduino IDE: Sketch → Include Library → Manage Libraries

Cài: RTClib by Adafruit

Kết Nối Phần Cứng

DS3231 với Arduino Uno

Arduino Uno               DS3231 Module
─────────────────────     ──────────────────────────
5V   ─────────────────→  VCC
GND  ─────────────────→  GND
A4 (SDA) ──────────────→  SDA
A5 (SCL) ──────────────→  SCL
Pin 2 (INT0) ─ [10kΩ pullup] ─ SQW  ← Tùy chọn alarm

DS3231 với ESP32 DevKit V1

ESP32 DevKit V1           DS3231 Module
─────────────────────     ──────────────────────────
3.3V ─────────────────→  VCC
GND  ─────────────────→  GND
GPIO21 (SDA) ──────────→  SDA
GPIO22 (SCL) ──────────→  SCL
GPIO4 ─ [10kΩ pullup] ─ SQW   ← Tùy chọn alarm

Code Arduino IDE

Code Đặt Giờ Và Đọc Thời Gian — Arduino Uno

/*
 * DS3231 RTC — Đặt thời gian và đọc liên tục
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, SDA→A4, SCL→A5
 * Thư viện: RTClib by Adafruit
 *
 * LẦN ĐẦU: uncomment dòng adjust() để đặt thời gian
 * SAU KHI ĐẶT: comment lại để không reset mỗi lần khởi động
 */

#include <Wire.h>
#include <RTClib.h>

RTC_DS3231 rtc;

const char* dayNames[] = {"CN", "T2", "T3", "T4", "T5", "T6", "T7"};

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

  if (!rtc.begin()) {
    Serial.println("Không tìm thấy DS3231!");
    while (true);
  }

  // *** Đặt thời gian — CHỈ chạy 1 lần, sau đó comment lại ***
  // Cách 1: Đặt thời gian từ thời điểm biên dịch code
  // rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  // Cách 2: Đặt thời gian cụ thể
  // rtc.adjust(DateTime(2026, 6, 28, 10, 30, 0)); // 2026-06-28 10:30:00

  // Kiểm tra xem RTC có bị mất điện không (power loss detected)
  if (rtc.lostPower()) {
    Serial.println("RTC mất điện! Đặt lại thời gian...");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  Serial.println("DS3231 sẵn sàng");
}

void loop() {
  DateTime now = rtc.now(); // Đọc thời gian hiện tại

  // In theo định dạng: YYYY-MM-DD HH:MM:SS (T7)
  Serial.print(now.year());  Serial.print("-");
  if (now.month()  < 10) Serial.print("0");
  Serial.print(now.month()); Serial.print("-");
  if (now.day()    < 10) Serial.print("0");
  Serial.print(now.day());   Serial.print(" ");
  if (now.hour()   < 10) Serial.print("0");
  Serial.print(now.hour());  Serial.print(":");
  if (now.minute() < 10) Serial.print("0");
  Serial.print(now.minute());Serial.print(":");
  if (now.second() < 10) Serial.print("0");
  Serial.print(now.second());

  // In thứ trong tuần (dayOfTheWeek: 0=CN, 1=T2, ..., 6=T7)
  Serial.print(" ("); Serial.print(dayNames[now.dayOfTheWeek()]); Serial.print(")");

  // In nhiệt độ từ cảm biến TCXO (chính xác ±3°C, dùng để bù nhiệt)
  Serial.print(" | T: "); Serial.print(rtc.getTemperature(), 1); Serial.print("°C");

  Serial.println();
  delay(1000);
}

Code Alarm Ngắt SQW — Arduino Uno

/*
 * DS3231 RTC — Alarm 1: báo thức mỗi 30 giây, ngắt qua SQW
 * Board: Arduino Uno
 * Kết nối: VCC→5V, GND→GND, SDA→A4, SCL→A5
 *          SQW → Pin2 (INT0) qua pull-up 10kΩ lên 5V
 * Thư viện: RTClib by Adafruit
 */

#include <Wire.h>
#include <RTClib.h>

RTC_DS3231 rtc;
const int SQW_PIN = 2;            // INT0 trên Arduino Uno

volatile bool alarmTriggered = false; // Cờ ngắt (dùng volatile!)

void IRAM_ATTR alarmISR() {
  alarmTriggered = true; // Ghi cờ trong ISR, xử lý nặng ở loop()
}

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

  if (!rtc.begin()) {
    Serial.println("DS3231 không tìm thấy!"); while (true);
  }

  // Xóa alarm cũ còn sót
  rtc.clearAlarm(1);
  rtc.clearAlarm(2);
  rtc.disableAlarm(2);

  // Alarm 1: kích hoạt khi giây = 0 hoặc giây = 30 (mỗi 30 giây)
  // DS3231Alarm1 modes: A1_PerSecond, A1_Second, A1_Minute, A1_Hour, A1_Date, A1_Day
  DateTime now = rtc.now();
  // Set alarm vào giây 0 của mỗi phút (mỗi 60 giây)
  rtc.setAlarm1(DateTime(0, 0, 0, 0, 0, 0), DS3231_A1_Second); // Alarm khi giây = 0

  // Kích hoạt ngắt trên SQW khi alarm
  pinMode(SQW_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SQW_PIN), alarmISR, FALLING);

  Serial.println("Alarm đặt: báo mỗi phút (giây = 0)");
}

void loop() {
  if (alarmTriggered) {
    alarmTriggered = false;       // Xóa cờ
    rtc.clearAlarm(1);            // Xóa flag alarm trong DS3231 (BẮT BUỘC để SQW lên HIGH)

    DateTime now = rtc.now();
    Serial.print("⏰ ALARM! Thời gian: ");
    Serial.printf("%02d:%02d:%02d\n", now.hour(), now.minute(), now.second());

    // Thêm xử lý alarm tại đây: ghi log, gửi dữ liệu, v.v.
  }
}

Code Data Logger Có Timestamp — Arduino Uno (DS3231 + SD Card)

/*
 * DS3231 + SD Card — Data logger với timestamp thực
 * Board: Arduino Uno
 * DS3231: SDA→A4, SCL→A5, VCC→5V
 * SD Card: CS→10, MOSI→11, MISO→12, SCK→13, VCC→5V
 *
 * Ghi log: YYYY-MM-DD HH:MM:SS,nhiệt độ vào file LOG.CSV
 */

#include <Wire.h>
#include <RTClib.h>
#include <SPI.h>
#include <SD.h>

RTC_DS3231 rtc;
const int CS_PIN = 10;
const char LOG_FILE[] = "LOG.CSV";
const unsigned long LOG_INTERVAL = 60000; // Log mỗi 1 phút
unsigned long lastLog = 0;

float readTempC() {
  int raw = analogRead(A0); // LM35 trên A0
  return raw * 5000.0 / 1023.0 / 10.0;
}

String timestampStr(DateTime dt) {
  char buf[20];
  sprintf(buf, "%04d-%02d-%02d %02d:%02d:%02d",
    dt.year(), dt.month(), dt.day(),
    dt.hour(), dt.minute(), dt.second());
  return String(buf);
}

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

  if (!rtc.begin()) { Serial.println("RTC fail"); while (true); }
  if (!SD.begin(CS_PIN)) { Serial.println("SD fail"); while (true); }

  if (rtc.lostPower()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  // Tạo header nếu file mới
  if (!SD.exists(LOG_FILE)) {
    File f = SD.open(LOG_FILE, FILE_WRITE);
    f.println("timestamp,temperature_C"); // Header
    f.close();
  }

  Serial.println("Logger sẵn sàng");
}

void loop() {
  if (millis() - lastLog >= LOG_INTERVAL) {
    lastLog = millis();

    DateTime now = rtc.now();
    float temp = readTempC();
    String ts = timestampStr(now);

    // Ghi vào SD
    File f = SD.open(LOG_FILE, FILE_WRITE);
    if (f) {
      f.print(ts); f.print(","); f.println(temp, 2);
      f.close();
    }

    // In Serial
    Serial.print(ts); Serial.print(" | "); Serial.print(temp, 1); Serial.println("°C");
  }
}

Code ESP32 — Đồng Hồ OLED Với RTC

/*
 * DS3231 + OLED SSD1306 — Đồng hồ thực hiển thị màn hình
 * Board: ESP32 DevKit V1
 * DS3231: VCC→3.3V, GND→GND, SDA→GPIO21, SCL→GPIO22
 * OLED: VCC→3.3V, GND→GND, SDA→GPIO21, SCL→GPIO22 (chung I2C)
 *
 * Cả DS3231 (0x68) và OLED (0x3C) trên cùng bus I2C — OK!
 */

#include <Wire.h>
#include <RTClib.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1

RTC_DS3231 rtc;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const char* dayVN[] = {"CN","T2","T3","T4","T5","T6","T7"};

void drawClock(DateTime now) {
  display.clearDisplay();

  // --- Giờ:Phút lớn ---
  display.setTextSize(3);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(14, 5);
  char timeStr[6];
  sprintf(timeStr, "%02d:%02d", now.hour(), now.minute());
  display.print(timeStr);

  // --- Giây nhỏ bên phải ---
  display.setTextSize(2);
  display.setCursor(98, 16);
  char secStr[3];
  sprintf(secStr, "%02d", now.second());
  display.print(secStr);

  // Đường kẻ phân cách
  display.drawLine(0, 36, 127, 36, SSD1306_WHITE);

  // --- Ngày/tháng/năm + thứ ---
  display.setTextSize(1);
  display.setCursor(0, 42);
  char dateStr[22];
  sprintf(dateStr, "%s %02d/%02d/%04d",
    dayVN[now.dayOfTheWeek()],
    now.day(), now.month(), now.year());
  display.print(dateStr);

  // --- Nhiệt độ từ DS3231 ---
  display.setCursor(0, 54);
  display.print("Nhiet: ");
  display.print(rtc.getTemperature(), 1);
  display.print((char)247); // °
  display.print("C");

  display.display();
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);

  if (!rtc.begin()) { Serial.println("RTC fail!"); while (true); }
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("OLED fail!"); while (true); }

  if (rtc.lostPower()) {
    Serial.println("RTC mất nguồn, đặt lại giờ...");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }
}

void loop() {
  DateTime now = rtc.now();
  drawClock(now);
  delay(1000);
}

Kết Quả Mong Đợi

Serial Monitor:
DS3231 sẵn sàng
2026-06-28 10:30:00 (CN) | T: 27.8°C
2026-06-28 10:30:01 (CN) | T: 27.8°C
2026-06-28 10:30:02 (CN) | T: 27.8°C

Ứng Dụng Thực Tế

Ứng dụngMô tả
Data logger chính xácTimestamp thực cho log cảm biến dài hạn
Hệ thống tưới tự độngBật relay đúng 6:00 sáng mỗi ngày
Đồng hồ nhúngHiển thị giờ trên LCD/OLED khi mất kết nối NTP
Ghi sự kiệnLog thời điểm cửa mở, chuyển động PIR
Lập lịch bật tắtBật thiết bị đúng lịch không cần WiFi

Lưu Ý Khi Sử Dụng

1. Đặt lại thời gian đúng cách

Dùng rtc.adjust(DateTime(F(__DATE__), F(__TIME__))) để đặt thời gian từ thời điểm biên dịch code. Sau khi upload và set xong: phải comment lại dòng này — mỗi lần reset MCU sẽ đặt lại thời gian cũ bằng thời điểm upload.

2. Luôn xóa Alarm Flag sau khi xử lý

rtc.clearAlarm(1) bắt buộc phải gọi sau khi alarm kích hoạt. Nếu không: chân SQW giữ nguyên LOW → ISR gọi liên tục → hệ thống loop vô tận.

3. DS3231 vs DS1307

DS1307 (RTC rẻ hơn): dùng crystal ngoài, sai số ±20ppm = ±10 phút/năm. DS3231: TCXO tích hợp, ±2ppm = ±1 phút/năm. Nếu cần độ chính xác: luôn dùng DS3231.

4. Pin CR2032 cần thay định kỳ

Pin mới: lưu giờ ~8 năm. Pin cũ hoặc xả → rtc.lostPower() trả về true → thời gian reset về 2000-01-01 00:00:00. Kiểm tra pin khi dự án quan trọng về thời gian.

Xem thêm: