PZEM-004T với ESP32: đo điện áp, dòng, công suất, điện năng 220 V qua Modbus không cần thư viện
Cấu tạo PZEM-004T v3, đấu an toàn phía 220 V và vòng CT, nối TTL với ESP32 có cầu phân áp, giải nghĩa khung Modbus và chương trình đọc điện năng không cần thư viện.
Muốn biết tủ lạnh, máy bơm hay cả phòng làm việc tiêu thụ bao nhiêu điện, cần một bộ đo làm việc trực tiếp với điện lưới 220 V. PZEM-004T v3 là module đo điện năng phổ biến cho việc này: nó tự đo điện áp, dòng, công suất, điện năng (kWh), tần số và hệ số công suất, rồi trả số liệu qua cổng nối tiếp TTL theo giao thức Modbus RTU. Bài này giải thích cấu tạo, cách đấu an toàn phía 220 V, cách nối với ESP32, ý nghĩa từng byte trong khung Modbus và chương trình đọc số liệu không cần thư viện.
1. PZEM-004T v3 là gì

Module có hai phần cách ly với nhau:
- Phía 220 V: chip đo điện năng, lấy điện áp từ hai cọc L, N và lấy dòng từ một vòng biến dòng (CT) ôm quanh dây điện. Phần này lấy nguồn nuôi từ chính điện lưới.
- Phía TTL: bốn chân 5V, RX, TX, GND nối với vi điều khiển. Tín hiệu đi qua optocoupler nên mạch ESP32 không nối điện với điện lưới.
Hệ quả quan trọng: khi chưa cấp điện lưới vào L, N thì PZEM không trả lời, dù phía TTL đã có 5 V. Nhiều người nối xong phía TTL, thử trên bàn mà chưa cấp 220 V, rồi nghĩ module hỏng.
| Đại lượng | Dải đo (bản 100 A, dùng CT) | Bước đo |
|---|---|---|
| Điện áp | 80–260 V | 0,1 V |
| Dòng điện | 0–100 A (bản 10 A: 0–10 A, dòng chạy qua module) | 0,001 A |
| Công suất tác dụng | 0–23 kW (bản 10 A: 0–2,3 kW) | 0,1 W |
| Điện năng | 0–9999,99 kWh | 1 Wh |
| Tần số | 45–65 Hz | 0,1 Hz |
| Hệ số công suất | 0,00–1,00 | 0,01 |
Bản 100 A đo dòng bằng vòng CT ôm ngoài dây, không cần cắt dây tải để luồn qua module, nên dễ lắp và an toàn hơn. Bản 10 A cho dòng tải chạy qua cọc vít của module, chỉ hợp với tải nhỏ. Theo datasheet, số điện năng được lưu lại khi mất điện.
2. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân (hoặc loại 38 chân) và dây USB nạp code.
- Module PZEM-004T v3, nên chọn bản 100 A kèm vòng CT.
- Hai điện trở 1 kΩ và 2 kΩ (hoặc 2,2 kΩ) nếu chân TX của PZEM lên 5 V (xem mục 5).
- Dây cắm cho phía TTL.
- Phía 220 V: dây điện đúng tiết diện, cầu dao có CB, hộp nhựa cách điện để đặt PZEM, ổ cắm hoặc cọc đấu nối có nắp che.
3. An toàn khi làm việc với điện 220 V
Điện lưới 220 V có thể gây chết người. Phía TTL của PZEM an toàn khi chạm tay, nhưng cọc L, N và dây đi tới chúng thì không. Trước khi đấu:
- Cắt cầu dao và dùng bút thử điện kiểm tra lại dây đã hết điện. Chỉ cấp điện lại khi mọi mối nối đã siết chặt và đậy kín.
- Đặt PZEM trong hộp nhựa cách điện; mặt dưới module có mối hàn của phía 220 V, không đặt trần trên bàn hay trên tấm kim loại.
- Không chạm vào module khi đang có điện, kể cả khi chỉ muốn nhấn nút reset ESP32: đặt ESP32 ra ngoài hộp, chỉ đưa 4 dây TTL ra.
- Nếu chưa từng đấu điện dân dụng, nhờ người có chuyên môn đấu phía 220 V. Phần ESP32 và chương trình vẫn tự làm được.
4. Đấu phía 220 V

- Đo điện áp: hai cọc L, N của PZEM nối song song với điện lưới, giống như cắm thêm một thiết bị. Dây nhánh này chỉ mang dòng rất nhỏ nên dây mảnh cũng được, nhưng phải có vỏ cách điện tốt.
- Đo dòng (bản 100 A): luồn một dây (thường là dây L) đi tới tải qua vòng CT, hai dây của CT vào hai cọc CT. Luồn cả L lẫn N qua vòng thì từ trường hai dây triệt tiêu nhau và PZEM báo 0 A.
- Đo dòng (bản 10 A): dây tải phải đi qua cọc dòng của module; không dùng cho tải lớn hơn 10 A như bình nóng lạnh, máy lạnh.
Vòng CT chỉ đo dòng qua những dây nằm trong vòng. Muốn đo cả nhà thì CT ôm dây L tổng sau công tơ; muốn đo một thiết bị thì CT ôm dây L của riêng thiết bị đó.
5. Nối phía TTL với ESP32

| PZEM-004T (phía TTL) | ESP32 DevKit V1 | Ghi chú |
|---|---|---|
| 5V | VIN | VIN có 5 V khi cắm USB |
| RX | TX2 (GPIO17) | ESP32 gửi khung hỏi |
| TX | RX2 (GPIO16) | PZEM trả lời; thêm cầu phân áp nếu chân này lên 5 V |
| GND | GND |
Chân 5V của PZEM nuôi phần optocoupler phía TTL. Mạch kéo lên của chân TX thường nối về chính chân 5V này, nên khi nghỉ chân TX có thể lên khoảng 5 V, vượt mức chịu của chân ESP32 (3,3 V). Cách làm an toàn:
- Cấp 5 V cho PZEM (chưa nối TX vào ESP32), đo chân TX so với GND bằng đồng hồ.
- Nếu đo được khoảng 5 V: nối TX → 1 kΩ → RX2, và RX2 → 2 kΩ → GND. Chân RX2 khi đó chỉ nhận 5 × 2 / 3 ≈ 3,3 V.
- Nếu đo được khoảng 3,3 V hoặc thấp hơn: nối thẳng TX vào RX2.
Chiều ngược lại (TX2 của ESP32 vào RX của PZEM) không cần chuyển mức: 3,3 V đủ để bật LED của optocoupler. Một số người cấp 3,3 V thay cho 5 V để tránh cầu phân áp; cách này hay chạy được nhưng nằm ngoài thông số của module, nên bài dùng 5 V.
6. Modbus của PZEM: hỏi gì, trả lời ra sao
PZEM là thiết bị tớ Modbus RTU trên UART 9600 baud, 8 bit dữ liệu, không chẵn lẻ, 1 bit dừng. ESP32 làm thiết bị chủ: gửi khung hỏi, PZEM trả khung chứa số đo. Nếu chưa quen Modbus, đọc trước bài Modbus RTU (phần 1): khung tin, mã hàm, CRC.

Khung hỏi dùng hàm 0x04 (đọc thanh ghi input), đọc 10 thanh ghi bắt đầu từ 0x0000:
| Thanh ghi | Nội dung | Đơn vị |
|---|---|---|
| 0x0000 | Điện áp | 0,1 V |
| 0x0001 (thấp), 0x0002 (cao) | Dòng điện, 32 bit | 0,001 A |
| 0x0003 (thấp), 0x0004 (cao) | Công suất, 32 bit | 0,1 W |
| 0x0005 (thấp), 0x0006 (cao) | Điện năng, 32 bit | 1 Wh |
| 0x0007 | Tần số | 0,1 Hz |
| 0x0008 | Hệ số công suất | 0,01 |
| 0x0009 | Cảnh báo quá công suất | 0xFFFF: đang cảnh báo; 0x0000: không |
Điểm dễ nhầm: số 32 bit được gửi thanh ghi thấp trước, nhưng trong mỗi thanh ghi thì byte cao vẫn đi trước. Bốn byte dòng điện 01 C4 00 00 nghĩa là thanh ghi thấp 0x01C4, thanh ghi cao 0x0000, ghép lại 0x000001C4 = 452, tức 0,452 A. Đọc nhầm thứ tự sẽ ra 0x01C40000, một con số vô lý.
Địa chỉ 0xF8 là địa chỉ chung mà PZEM nào cũng trả lời, chỉ dùng khi trên đường dây có một PZEM. Ngoài ra PZEM còn hàm riêng 0x42 để xóa bộ đếm điện năng: khung gửi chỉ có địa chỉ, mã hàm và CRC, PZEM trả lời lặp lại đúng khung đó.
7. Chương trình đọc PZEM bằng ESP32
Chương trình dùng HardwareSerial có sẵn trong gói esp32, tự dựng khung Modbus và tính CRC, không cần thư viện ngoài. Cứ 2 giây đọc một lần và in ra Serial Monitor; gõ R để xóa bộ đếm điện năng.
// PYWORLD.VN - Đọc PZEM-004T v3 bằng ESP32 qua Modbus RTU: điện áp, dòng, công suất, điện năng, tần số, hệ số công suất
// Phía TTL: 5V vào VIN, GND chung, TX của PZEM vào RX2 (GPIO16, qua cầu phân áp nếu TX lên 5 V), RX của PZEM vào TX2 (GPIO17)
// Phía 220 V: chỉ đấu khi đã cắt điện, có vỏ cách điện, nhờ người có chuyên môn. Gõ R trên Serial Monitor để xóa bộ đếm điện năng
HardwareSerial pzem(2);
const int CHAN_RX2 = 16;
const int CHAN_TX2 = 17;
const uint8_t DIA_CHI = 0xF8; // địa chỉ chung: dùng khi chỉ có một PZEM trên đường dây (mặc định máy là 0x01)
struct SoDo {
float dienAp, dong, congSuat, tanSo, heSoCongSuat;
uint32_t dienNangWh;
};
uint16_t crcModbus(const uint8_t *du, size_t n) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < n; i++) {
crc ^= du[i];
for (int b = 0; b < 8; b++) crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc;
}
// Gửi khung, chờ đủ `canNhan` byte trả lời (tối đa 200 ms) và kiểm CRC.
bool traoDoi(uint8_t *gui, size_t n, uint8_t *nhan, size_t canNhan) {
uint16_t crc = crcModbus(gui, n);
gui[n] = crc & 0xFF; // Modbus gửi byte thấp của CRC trước
gui[n + 1] = crc >> 8;
while (pzem.available()) pzem.read();
pzem.write(gui, n + 2);
size_t da = 0;
uint32_t batDau = millis();
while (da < canNhan && millis() - batDau < 200) {
if (pzem.available()) nhan[da++] = pzem.read();
}
if (da < canNhan) return false;
return crcModbus(nhan, canNhan - 2) == (uint16_t)(nhan[canNhan - 2] | (nhan[canNhan - 1] << 8));
}
uint16_t tu(const uint8_t *p) { return (p[0] << 8) | p[1]; } // thanh ghi 16 bit, byte cao trước
uint32_t tu32(const uint8_t *p) { return tu(p) | ((uint32_t)tu(p + 2) << 16); } // 32 bit: thanh ghi thấp trước
// Hàm 0x04 đọc 10 thanh ghi input từ 0x0000.
bool docPzem(SoDo &s) {
uint8_t gui[8] = {DIA_CHI, 0x04, 0x00, 0x00, 0x00, 0x0A};
uint8_t nhan[25]; // địa chỉ, hàm, số byte (20), 20 byte dữ liệu, 2 byte CRC
if (!traoDoi(gui, 6, nhan, 25) || nhan[1] != 0x04 || nhan[2] != 20) return false;
const uint8_t *d = nhan + 3;
s.dienAp = tu(d) * 0.1f; // 0x0000: 0,1 V
s.dong = tu32(d + 2) * 0.001f; // 0x0001-0x0002: 0,001 A
s.congSuat = tu32(d + 6) * 0.1f; // 0x0003-0x0004: 0,1 W
s.dienNangWh = tu32(d + 10); // 0x0005-0x0006: 1 Wh
s.tanSo = tu(d + 14) * 0.1f; // 0x0007: 0,1 Hz
s.heSoCongSuat = tu(d + 16) * 0.01f; // 0x0008: 0,01
return true;
}
bool xoaDienNang() { // hàm riêng 0x42 của PZEM, trả lời lặp lại khung gửi
uint8_t gui[4] = {DIA_CHI, 0x42};
uint8_t nhan[4];
return traoDoi(gui, 2, nhan, 4) && nhan[1] == 0x42;
}
void setup() {
Serial.begin(115200);
pzem.begin(9600, SERIAL_8N1, CHAN_RX2, CHAN_TX2); // PZEM-004T v3: 9600 baud, 8N1
Serial.println("Doc PZEM-004T moi 2 giay. Go R de xoa bo dem dien nang");
}
void loop() {
if (Serial.available()) {
char c = Serial.read();
if (c == 'R' || c == 'r') Serial.println(xoaDienNang() ? "Da xoa bo dem dien nang" : "Loi xoa bo dem");
}
static uint32_t lanDoc = 0;
if (millis() - lanDoc < 2000) return;
lanDoc = millis();
SoDo s;
if (!docPzem(s)) {
Serial.println("PZEM khong tra loi: kiem tra TX/RX, nguon 5V, PZEM phai co dien 220V moi tra loi");
return;
}
Serial.printf("%.1f V | %.3f A | %.1f W | %.3f kWh | %.1f Hz | PF %.2f\n", s.dienAp, s.dong, s.congSuat,
s.dienNangWh / 1000.0f, s.tanSo, s.heSoCongSuat);
}
Các phần chính:
crcModbus()tính CRC-16 Modbus (đa thức đảo 0xA001, giá trị đầu 0xFFFF). CRC gửi byte thấp trước. Khung hỏi ở hình trên kết thúc bằng64 64chính là kết quả của hàm này.traoDoi()xóa dữ liệu cũ trong bộ đệm, gửi khung, chờ đủ số byte trả lời trong tối đa 200 ms rồi kiểm tra CRC. Thiếu byte hay sai CRC đều coi là lỗi, không dùng số liệu đó.tu()ghép hai byte thành thanh ghi 16 bit (byte cao trước);tu32()ghép hai thanh ghi thành số 32 bit (thanh ghi thấp trước), đúng như mục 6.docPzem()kiểm tra mã hàm và số byte (20) trước khi đổi đơn vị. Nếu PZEM trả lời mã lỗi (hàm 0x84), khung chỉ dài 5 byte nên hàm cũng trả về lỗi.
8. Kết quả mong đợi

Có thể tự kiểm tra số liệu: công suất xấp xỉ điện áp × dòng × hệ số công suất. Với dòng đầu tiên: 228,6 × 0,452 × 0,95 ≈ 98,2 W, khớp với 98,4 W trong giới hạn làm tròn. Tải thuần trở như ấm đun nước có hệ số công suất gần 1; nguồn adapter, đèn LED giá rẻ có hệ số thấp hơn nhiều.
9. Mở rộng
- Gửi số liệu đi xa: thay phần in Serial bằng gửi lên MQTT hay trang web, xem bài dùng MQTT với ESP32. Điện năng nên lưu theo ngày (lấy số Wh đầu ngày trừ số Wh cuối ngày), không cần xóa bộ đếm.
- Nhiều PZEM: mỗi PZEM nên đặt một địa chỉ riêng (thanh ghi giữ 0x0002, ghi bằng hàm 0x06, mỗi lần chỉ nối một module) rồi đọc theo địa chỉ thay cho 0xF8. Cách đơn giản nhất về điện là mỗi PZEM một UART: ESP32 còn UART1, gán được vào chân tùy chọn.
- Cảnh báo quá công suất: thanh ghi giữ 0x0001 đặt ngưỡng (đơn vị 1 W), thanh ghi 0x0009 báo đã vượt ngưỡng. Muốn ngắt tải khi quá công suất, đọc bài ESP32 điều khiển module relay an toàn và chọn relay, dây có dòng định mức lớn hơn dòng tải. Relay không thay cho cầu dao, CB bảo vệ.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| "PZEM khong tra loi" liên tục | Chưa cấp 220 V vào L, N; đảo TX/RX; thiếu 5V hoặc GND phía TTL | Cấp điện lưới; TX của PZEM vào RX2, RX vào TX2; đủ 4 dây |
| Thỉnh thoảng mất một lần đọc | Dây TTL dài, nhiễu; khung bị cắt giữa chừng | Dây ngắn, xoắn đôi; bỏ qua lần lỗi, không dùng số liệu cũ |
| Dòng luôn 0 A, điện áp vẫn đúng | CT ôm cả L và N; CT chưa cắm vào cọc; tải chưa chạy | Chỉ luồn một dây qua CT, kiểm tra hai dây CT |
| Dòng, công suất nhỏ hơn thực tế | CT ôm dây nhánh khác, không phải dây cấp cho tải cần đo | Đặt CT trên đúng dây L của tải |
| Dòng ra số rất lớn, vô lý | Ghép hai thanh ghi 32 bit sai thứ tự | Thanh ghi thấp trước, như hàm tu32() |
| ESP32 hoạt động chập chờn, chân RX2 nóng | TX của PZEM đưa 5 V thẳng vào GPIO16 | Thêm cầu phân áp 1 kΩ / 2 kΩ |
| "Loi xoa bo dem" khi gõ R | PZEM chưa có điện lưới hoặc khung trả lời sai CRC | Cấp điện lưới, gõ lại |
11. Xem thêm
- UART là gì: khung truyền, tốc độ baud và Serial2 trên ESP32.
- Kiểm tra lỗi truyền dữ liệu: checksum và CRC-16 của Modbus.
- Modbus RTU (phần 2): ESP32 làm thiết bị tớ: hiểu PZEM từ phía bên kia đường dây.
- Bài đo dòng một chiều bằng INA219 và ACS712 trong cùng loạt module phổ biến: dùng cho mạch pin, động cơ nhỏ thay vì điện lưới.
Xem thêm