07/12/2025
Dưới đây là một chương trình Arduino hoàn chỉnh (dựng cho Arduino Uno / Mega) để tính số điện (Wh / kWh) hàng tháng, dùng CT dòng (đo dòng) và đo điện áp (bắt buộc để biết chiều công suất — mua hay bán). Mã này:
Đo mẫu dòng (CT) và mẫu điện áp đồng bộ.
Tính công tức thời = v(t) * i(t), tích phân → công trung bình (W).
Cộng dồn thành Wh/kWh; khi công âm → bán điện (export), công dương → mua điện (import).
Lưu tổng năng lượng vào EEPROM để không mất khi mất điện.
Hỗ trợ DS3231 (nếu bạn có) để tự động reset sang tháng mới — nếu không có RTC thì có nút reset thủ công.
Có hướng dẫn nối dây, hiệu chuẩn, và lưu ý an toàn.
LƯU Ý AN TOÀN (rất quan trọng)
Đo điện áp mạng trực tiếp có nguy cơ giật chết. Nếu bạn không quen với điện lưới, hãy dùng biến áp (PT) cách ly cho điện áp hoặc dùng thiết bị thương mại đã cách ly. Luôn tắt nguồn khi đấu nối.
---
Phần cứng cần có
Arduino Uno/ Mega.
CT sensor (ví dụ SCT-013-xx) với mạch burden resistor (hoặc ACS712 kiểu có tín hiệu DC). Ở ví dụ ta dùng CT kiểu output AC (qua điện trở burden) và có mạch dịch offset.
Mạch chia điện áp (voltage divider) để đưa 230VAC xuống < 5V (hoặc dùng biến áp cách ly).
(Tùy chọn) DS3231 RTC module để tự động reset theo tháng.
Nút bấm để reset tổng (nếu không có RTC).
Đi dây, breadboard, tụ lọc, các trở kháng an toàn, v.v.
---
Sơ đồ nối dây (tóm tắt)
CT ngoài dây pha: CT_out -> mạch burden -> analog input A0 (CURRENT). Đồng thời phải tạo bias Vcc/2 (2.5V) để đọc AC.
Voltage divider: lấy từ một biến áp cách ly hoặc qua mạch chia an toàn -> analog input A1 (VOLTAGE). Cần có bias nếu đo trực tiếp AC.
Nút bấm Reset -> chân digital RESET_PIN (với pullup).
I2C cho DS3231: SDA -> A4, SCL -> A5 (Uno).
(Chi tiết phụ thuộc sensor: nếu dùng ACS712 thì chỉ cần đọc A0 và không cần bias.)
---
Ý tưởng thuật toán ngắn
1. Đồng bộ đọc N mẫu nhanh cho cả V và I.
2. Chuyển ADC sang giá trị điện áp thực (Volts) & dòng thực (Amps) bằng hệ số hiệu chuẩn.
3. Tính công tức thời p = v * i (có dương/âm).
4. Tích phân p theo thời gian => Wh: Wh += (p_mean * window_seconds) / 3600.
5. Lưu giá trị vào EEPROM định kỳ.
6. Nếu DS3231 present: kiểm tra ngày 1 của tháng mới -> reset counters tự động.
---
Mã nguồn (Arduino)
// EnergyMeter_Monthly.ino
// Đo công suất (W) và tích phân thành Wh/kWh mỗi tháng.
// Hỗ trợ CT (AC) và đo điện áp (AC). Tính công tức thời để phân biệt import/export.
// Works on Arduino Uno/Mega.
// Author: ChatGPT (việt hóa) - cung cấp như ví dụ, cần hiệu chuẩn thực tế trước khi dùng.
// optional, nếu không có RTC thì vẫn chạy (cần cài thư viện RTClib)
RTC_DS3231 rtc; // nếu không dùng RTC, khối lệnh liên quan sẽ bị bỏ qua
// --------- Cấu hình phần cứng ----------
const int ADC_PIN_I = A0; // đọc CT (dòng)
const int ADC_PIN_V = A1; // đọc voltage divider (điện áp)
const int RESET_PIN = 2; // nút reset tổng tháng (kéo xuống GND khi bấm)
const int LED_PIN = 13;
// Sampling params
const unsigned int NUM_SAMPLES = 2000; // số mẫu trong 1 window (tăng giảm tuỳ hiệu năng)
// với analogRead ~100us -> 2000 mẫu ~0.2s
// Điện áp hệ thống (mặc định VN: 230V rms)
const float VOLTAGE_NOMINAL = 230.0;
// ---------- Hệ số hiệu chuẩn (phải hiệu chỉnh bằng thực nghiệm) ----------
// ADC -> volt/current conversion
// Đối với voltage divider: tính = (ADC_raw / 1023.0) * Vref * (R_total/R_low)
// Bạn cần thay theo mạch của bạn.
const float ADC_REF = 5.0; // Vref của Arduino (5V cho Uno)
const float VOLTAGE_DIVIDER_RATIO = 100.0 / 1.0; // ví dụ: nếu mạch chia 100:1 => multiply lại 100
// Thay giá trị phù hợp
// CT sensor calibration:
// Nếu dùng SCT-013-xx với burden và offset, ta chuyển ADC->Amp bằng hệ số:
const float CT_SENSITIVITY = 0.050; // ví dụ: 50mV/A tại burden -> cần hiệu chỉnh (V per A)
// nếu sensor là ACS712, hệ số khác (ví dụ 185 mV/A)
// Thay bằng giá trị đúng của bạn
// Nếu dùng bias (Vcc/2), trung điểm ADC ~ 512
const int ADC_MID = 512;
// Power factor mặc định nếu không biết: 1.0. Nếu muốn tính chính xác không cần PF cố định.
const float DEFAULT_POWER_FACTOR = 1.0;
// ---------- EEPROM addresses ----------
const int EE_ADDR_TOTAL_WH = 0; // lưu Wh (float 4 bytes)
const int EE_ADDR_LAST_MONTH = 10; // lưu tháng cuối cùng đã ghi (int)
// ---------- Biến chạy ----------
float total_wh = 0.0; // Wh dương = mua (import). Wh âm = bán (export) nếu net export.
int last_saved_month = -1;
// --------- Hàm EEPROM read/write float ----------
void EEPROMWriteFloat(int address, float value) {
byte *p = (byte*)(void*)&value;
for (int i = 0; i < 4; i++) EEPROM.update(address + i, p[i]);
}
float EEPROMReadFloat(int address) {
float value = 0.0;
byte *p = (byte*)(void*)&value;
for (int i = 0; i < 4; i++) p[i] = EEPROM.read(address + i);
return value;
}
void EEPROMWriteInt(int address, int value) {
EEPROM.update(address, (value >> 8) & 0xFF);
EEPROM.update(address + 1, value & 0xFF);
}
int EEPROMReadInt(int address) {
int hi = EEPROM.read(address);
int lo = EEPROM.read(address + 1);
return (hi NUM_SAMPLES có thể điều chỉnh
}
unsigned long endMicros = micros();
double dt_seconds = (endMicros - startMicros) / 1000000.0;
double meanP = sumP / (double)NUM_SAMPLES; // note: mean of instantaneous products in ADC-voltage units
// Chuyển từ đơn vị ADC-voltage * A -> W cần hệ số chuẩn
// Trong ví dụ đơn giản, v_sample & i_amp đã ở đơn vị V và A -> meanP là W
// Nếu không, bạn phải nhân bằng hệ số chỉnh.
return (float)meanP; // W (có thể âm)
}
// ---------- Hàm main loop ----------
unsigned long lastEEPROMsave = 0;
const unsigned long EEPROM_SAVE_INTERVAL_MS = 60000UL; // lưu mỗi 60s
void loop() {
// Nếu nút reset bấm -> reset tổng (thủ công)
if (digitalRead(RESET_PIN) == LOW) {
delay(50); // debounce
if (digitalRead(RESET_PIN) == LOW) {
total_wh = 0.0;
EEPROMWriteFloat(EE_ADDR_TOTAL_WH, total_wh);
Serial.println("RESET MANUAL: total_wh = 0");
// chờ bỏ tay
while (digitalRead(RESET_PIN) == LOW) delay(50);
}
}
// Nếu có RTC: kiểm tra ngày 1 của tháng -> tự reset nếu khác last_saved_month
DateTime now;
bool haveRTC = rtc.begin();
if (haveRTC) {
now = rtc.now();
int m = now.month();
if (last_saved_month == -1) {
last_saved_month = EEPROMReadInt(EE_ADDR_LAST_MONTH);
if (last_saved_month == 0xFFFF) last_saved_month = -1;
}
if (m != last_saved_month) {
// Reset sang tháng mới (buổi sáng của ngày 1)
if (now.day() == 1 && now.hour() == 0 && now.minute() < 5) {
Serial.println("Auto reset tháng mới (RTC).");
total_wh = 0.0;
last_saved_month = m;
EEPROMWriteFloat(EE_ADDR_TOTAL_WH, total_wh);
EEPROMWriteInt(EE_ADDR_LAST_MONTH, last_saved_month);
}
}
}
// Đo 1 window
float meanPower = measurePowerWindow(); // W (có thể âm)
// Thời gian đo xấp xỉ dt_seconds (trong measurePowerWindow)
// Tuy nhiên ta tính tạm bằng: window_time = NUM_SAMPLES * read_time (~)
// Để đơn giản: ta lấy dt_seconds = NUM_SAMPLES * avg_sample_time (approx)
// Ở đây ta ước dt ~ 0.2s (tuỳ NUM_SAMPLES và board). Để chính xác, sửa measurePowerWindow để trả dt.
float window_seconds = 0.2; // sửa nếu cần hoặc tính dt chính xác
// Tính Wh thêm: Wh += P_mean(W) * window_seconds(s) / 3600
float add_wh = meanPower * (window_seconds / 3600.0);
total_wh += add_wh;
// Hiển thị
Serial.print("P_mean (W): ");
Serial.print(meanPower, 3);
Serial.print(" addWh: ");
Serial.print(add_wh, 6);
Serial.print(" totalWh: ");
Serial.println(total_wh, 6);
digitalWrite(LED_PIN, meanPower > 0 ? HIGH : LOW); // LED on khi nhập điện (positive power)
// Lưu EEPROM mỗi khoảng
unsigned long nowMillis = millis();
if (nowMillis - lastEEPROMsave > EEPROM_SAVE_INTERVAL_MS) {
EEPROMWriteFloat(EE_ADDR_TOTAL_WH, total_wh);
if (haveRTC) {
int m = rtc.now().month();
EEPROMWriteInt(EE_ADDR_LAST_MONTH, m);
last_saved_month = m;
}
lastEEPROMsave = nowMillis;
}
// chờ 200ms trước lần đo kế tiếp (điều chỉnh nếu muốn)
delay(200);
}
---
Giải thích và cách hiệu chỉnh (cần thiết)
1. Hiệu chuẩn CT (CT_SENSITIVITY)
Nếu dùng SCT-013 với burden resistor Rburden, và CT có tỉ số ví dụ 1000:1, thì amplitude trên burden = I * Rburden * (turns?) — đọc datasheet CT và tính V/A. Thường manufacturer ghi bao nhiêu mV/A (ví dụ 50 mV/A).
Đo thực tế: cho 1A qua CT, đọc ADC -> tính ra V đọc, từ đó suy ra hệ số V per A, điền vào CT_SENSITIVITY.
2. Hiệu chỉnh voltage divider (VOLTAGE_DIVIDER_RATIO)
Nếu dùng biến áp: đo RMS với đồng hồ để biết tỉ lệ.
Nếu dùng divider trực tiếp (không khuyến khích), tính tỷ lệ = (Rtop + Rbottom)/Rbottom.
3. Đồng bộ V và I
Trong mã ví dụ, ta đọc V và I "gần như" cùng thời điểm (liên tiếp). Để chính xác hơn, cần sampling nhanh và đọc V và I ngay sát nhau. Nếu dùng Arduino Uno, analogRead khá chậm nhưng vẫn có thể cho kết quả chấp nhận nếu NUM_SAMPLES đủ lớn.
4. Xác định chiều (mua/bán)
Vì ta tính công tức thời p = v * i, nếu tích phân p dương => lưới cung cấp năng lượng cho nhà => import (mua). Nếu âm => xuất điện ra lưới => export (bán). Do đó total_wh sẽ tăng khi nhập (dương), giảm khi xuất (add_wh âm) — bạn có thể tách thành 2 biến (import_wh, export_wh) nếu cần báo chi tiết.
5. Lưu và reset tháng
Nếu có RTC DS3231, chương trình tự reset vào ngày 1 giờ 00:xx (cái này bạn có thể điều chỉnh).
Nếu không có RTC, sử dụng nút bấm reset thủ công.
6. Giới hạn và cải tiến
Độ chính xác phụ thuộc calibration, sampling rate, phase shift do bộ lọc/burden, và sai số ADC. Với yêu cầu thương mại phải dùng chip ADC chính xác (ADS1115, v.v) và mạch cách ly.
Có thể cải tiến bằng cách: lấy dt chính xác từ micros() trong measurePowerWindow và trả về meanP và dt để tích phân chính xác.
Thêm màn hình LCD/OLED để hiển thị kWh, import/export hiện tại.
---
Gợi ý kiểm tra hiệu chuẩn nhanh
1. Dùng nguồn AC có dòng ổn định (ví dụ 1A). Đo bằng ampe kẹp để biết chính xác I_rms.
2. Đọc ADC trong chương trình ở raw và tính ra Vread -> điều chỉnh CT_SENSITIVITY cho đúng A đọc.
3. Kiểm tra với tải résistive thuần (PF ~1) để chắc công = V * I.
---
Nếu bạn muốn, tôi có thể:
1. Chỉnh mã để trả về dt_seconds chính xác từ hàm đo và dùng để tích phân chính xác,
2. Thêm lưu tách import_wh và export_wh,
3. Thêm hỗ trợ ADS1115 cho ADC chính xác,
4. Sinh sketch đã chỉnh cho ESP32 (nếu bạn muốn dùng ESP32).
P/s: còn tiếp phần hiệu chỉnh