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ì

Sơ đồ module PZEM-004T v3: bên trái phía 220 V có cọc L, N và hai dây vòng CT; giữa là optocoupler cách ly; bên phải phía TTL có 4 chân 5V, RX, TX, GND
Phía 220 V và phía TTL cách ly nhau bằng optocoupler (hình minh họa).

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ượngDải đo (bản 100 A, dùng CT)Bước đo
Điện áp80–260 V0,1 V
Dòng điện0–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ụng0–23 kW (bản 10 A: 0–2,3 kW)0,1 W
Điện năng0–9999,99 kWh1 Wh
Tần số45–65 Hz0,1 Hz
Hệ số công suất0,00–1,000,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

Sơ đồ đấu phía 220 V: dây L và N từ cầu dao đi tới tải; hai dây rẽ nhánh từ L và N vào cọc L, N của PZEM; vòng CT ôm quanh dây L, hai dây của CT vào cọc CT
Điện áp lấy song song ở L, N; vòng CT ôm một dây duy nhất (hình minh họa).
  • Đ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

Sơ đồ nối PZEM-004T với ESP32 DevKit V1: 5V vào VIN, GND vào GND, chân TX của PZEM vào RX2 GPIO16, chân RX của PZEM vào TX2 GPIO17
5V vào VIN; TX của PZEM vào RX2 (GPIO16), RX của PZEM vào TX2 (GPIO17) (hình minh họa).
PZEM-004T (phía TTL)ESP32 DevKit V1Ghi chú
5VVINVIN có 5 V khi cắm USB
RXTX2 (GPIO17)ESP32 gửi khung hỏi
TXRX2 (GPIO16)PZEM trả lời; thêm cầu phân áp nếu chân này lên 5 V
GNDGND

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:

  1. 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ồ.
  2. 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.
  3. 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 F8 04 00 00 00 0A 64 64 và khung trả lời 25 byte của PZEM-004T, chia nhóm điện áp 08 EE, dòng 01 C4 00 00, công suất 03 D8 00 00, điện năng 04 E2 00 00, tần số 01 F4, hệ số công suất 00 5F, cảnh báo 00 00 và CRC
Một lần hỏi và trả lời; CRC tính theo Modbus RTU (số liệu ví dụ).

Khung hỏi dùng hàm 0x04 (đọc thanh ghi input), đọc 10 thanh ghi bắt đầu từ 0x0000:

Thanh ghiNội dungĐơn vị
0x0000Điện áp0,1 V
0x0001 (thấp), 0x0002 (cao)Dòng điện, 32 bit0,001 A
0x0003 (thấp), 0x0004 (cao)Công suất, 32 bit0,1 W
0x0005 (thấp), 0x0006 (cao)Điện năng, 32 bit1 Wh
0x0007Tần số0,1 Hz
0x0008Hệ số công suất0,01
0x0009Cảnh báo quá công suất0xFFFF: đ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ằng 64 64 chí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

Serial Monitor in các dòng 228.6 V, 0.452 A, 98.4 W, 1.250 kWh, 50.0 Hz, PF 0.95; khi bật thêm ấm đun nước dòng tăng lên 6.654 A, 1501.2 W; khi chưa có điện lưới in thông báo PZEM không trả lời
Tải khoảng 100 W, rồi bật thêm ấm đun nước; khung dưới là khi chưa có điện lưới (số liệu ví dụ).

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ượngNguyên nhân thường gặpCách xử lý
"PZEM khong tra loi" liên tụcChưa cấp 220 V vào L, N; đảo TX/RX; thiếu 5V hoặc GND phía TTLCấ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 đọcDây TTL dài, nhiễu; khung bị cắt giữa chừngDâ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 đúngCT ôm cả L và N; CT chưa cắm vào cọc; tải chưa chạyChỉ 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óngTX của PZEM đưa 5 V thẳng vào GPIO16Thêm cầu phân áp 1 kΩ / 2 kΩ
"Loi xoa bo dem" khi gõ RPZEM chưa có điện lưới hoặc khung trả lời sai CRCCấp điện lưới, gõ lại

11. Xem thêm


Tin tức liên quan

ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt
ESP32 điều khiển động cơ bước NEMA 17 với A4988: vi bước, chỉnh dòng VREF, tăng tốc mượt

Hướng dẫn điều khiển động cơ bước NEMA 17 bằng ESP32 qua driver A4988: nối dây, chỉnh dòng VREF, chọn vi bước 1/16, tăng tốc giảm tốc hình thang và thí nghiệm STEM.

Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng
Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng

Hướng dẫn các thao tác Wi-Fi cần dùng nhất trên ESP32 với Arduino IDE: quét mạng, kết nối có thời gian chờ, xem IP và cường độ sóng, tự kết nối lại, đặt IP tĩnh, phát Wi-Fi riêng.

ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ
ESP32-CAM là gì: sơ đồ chân, cách nạp code, xem video trên trình duyệt, chụp ảnh lưu thẻ nhớ

Giới thiệu mạch ESP32-CAM AI Thinker, sơ đồ chân, nạp code bằng đế MB hoặc USB-TTL, chương trình xem video MJPEG trên trình duyệt, chụp ảnh có đèn flash và lưu vào thẻ microSD.

ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay
ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay

ESP32 dùng Firebase Realtime Database không cần thư viện Firebase: đăng nhập ẩn danh, tự làm mới token, đọc lệnh relay, ghi trạng thái và lịch sử, Rules theo uid.

ESP32 điều khiển động cơ DC với driver L298N: đảo chiều, chỉnh tốc độ bằng PWM
ESP32 điều khiển động cơ DC với driver L298N: đảo chiều, chỉnh tốc độ bằng PWM

Hướng dẫn nối ESP32 với driver L298N và động cơ DC giảm tốc: cầu H, đảo chiều, phanh, chỉnh tốc độ bằng PWM, điều khiển bằng lệnh trên Serial Monitor và thí nghiệm STEM.

ESP32 điều khiển động cơ không chổi than qua ESC: xung 1000–2000 µs, hiệu chuẩn dải ga, thử an toàn
ESP32 điều khiển động cơ không chổi than qua ESC: xung 1000–2000 µs, hiệu chuẩn dải ga, thử an toàn

Dùng ESP32 phát xung ga 1000–2000 µs bằng LEDC để điều khiển ESC và động cơ không chổi than: nối dây với pin LiPo, hiệu chuẩn dải ga, tăng ga từ từ, khóa động cơ và an toàn khi thử.

Hướng dẫn dùng màn hình LCD 2004 I2C với ESP32
Hướng dẫn dùng màn hình LCD 2004 I2C với ESP32

Hướng dẫn nối màn hình LCD 2004 I2C (mạch PCF8574) với ESP32 DevKit V1, quét địa chỉ I2C, hiển thị 4 dòng chữ, tạo ký tự riêng, làm bộ đếm thời gian và đổi sang LCD 1602.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.


Bình luận
Đã thêm vào giỏ hàng