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:
- Crystal 32.768kHz trực tiếp trong package IC (không phải bên ngoài)
- Cảm biến nhiệt độ đọc nhiệt độ chip liên tục
- 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ếp | I2C |
| Địa chỉ I2C | 0x68 (cố định, không thay đổi được) |
| Điện áp VCC | 2.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 backup | CR2032 (3V) — thường kèm theo module |
| Alarms | 2 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ân | Mô tả |
|---|---|
| GND | Mass |
| VCC | 3.3V hoặc 5V |
| SDA | I2C Data |
| SCL | I2C Clock |
| SQW | Square Wave / Interrupt output (open drain) |
| 32K | 32.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ụng | Mô tả |
|---|---|
| Data logger chính xác | Timestamp thực cho log cảm biến dài hạn |
| Hệ thống tưới tự động | Bật relay đúng 6:00 sáng mỗi ngày |
| Đồng hồ nhúng | Hiển thị giờ trên LCD/OLED khi mất kết nối NTP |
| Ghi sự kiện | Log thời điểm cửa mở, chuyển động PIR |
| Lập lịch bật tắt | Bậ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.
📚 Series 37 Module Cảm Biến
⬅️ Bài trước: SD Card Module SPI: FAT32, Ghi Log CSV Cảm Biến Đọc Config File Không Hardcode


