The PZEM-004T v3.0 measures a 230 V circuit — voltage, current, power, energy, frequency and power factor — and reports it over an isolated serial port. The Pico 2 W reads it on UART0 (GP0/GP1) with a few lines of Modbus-RTU, ready to log or send over Wi-Fi.
This guide covers only the low-voltage side: 5 V from VBUS, RX/TX crossed, and a 1 kΩ / 2 kΩ divider on the PZEM’s TX. Connecting L, N and the current transformer to the mains is electrician-only work.
Wiring
| Raspberry Pi Pico 2 W | PZEM-004T v3.0 AC energy meter (100 A with CT) | Wire | Notes |
|---|---|---|---|
| — | L | — | To Mains live — electrician only, via MCB (not drawn) |
| — | N | — | To Mains neutral — electrician only (not drawn) |
| — | CT1 | — | To Current transformer (clamped on the live wire only) (not drawn) |
| — | CT2 | — | To Current transformer (other lead) (not drawn) |
| VBUS | 5V | red | VBUS (pin 40), USB 5 V for the opto-isolated TTL side. |
| GP0 | RX | green | GP0 = UART0 TX, physical pin 1 → PZEM RX. |
| GP1 | TX | yellow | Via 1 kΩ / 2 kΩ divider — PZEM TX is pulled up to 5 V. 1 kΩ from TX to GP1, 2 kΩ from GP1 to GND. GP1 = UART0 RX, physical pin 2. |
| GND | GND | black | TTL-side ground only — it is isolated from the mains side. |
TTL side 5 V from your board; measuring side powered from the 230 V it measures part, 3.3 V board. The TTL side is opto-isolated and powered from your board. Its TX line is pulled up to whatever you feed the 5V pin: on ESP32/Pico, keep 5 V and put a 1 kΩ / 2 kΩ divider on PZEM TX → board RX (many v3.0 units also work with 3.3 V on the 5V pin). On Uno connect directly.
Board quirks
- Mains safety: L, N and the CT are on 230 V. Wiring them is a job for a licensed electrician, with the supply off at the MCB, behind an RCCB, inside a closed enclosure, with proper earthing. Never touch the PZEM board while L/N are live.
- You only wire the TTL side: 5V, GND, RX and TX. It is opto-isolated from the mains side — keep it that way, and never join its GND to mains earth or neutral.
- PZEM TX is pulled up to its 5V pin. The Pico 2 W’s inputs tolerate 5 V only while the board is powered, so the divider stays in.
- No library: the script sends one Modbus-RTU “read input registers” request (address 0xF8 reaches a single PZEM) and checks the CRC-16 of the reply.
- The CT must clamp the live wire only. Clamping the whole cable (L and N together) reads zero.
Code
# PZEM-004T v3.0 energy meter on a Raspberry Pi Pico 2 W (MicroPython, Modbus-RTU, no library).
# Low-voltage side only: 5V -> VBUS (pin 40), GND -> GND, PZEM RX <- GP0 (pin 1, UART0 TX),
# PZEM TX -> 1 kOhm -> GP1 (pin 2, UART0 RX), and 2 kOhm from GP1 to GND.
# The L/N terminals and the CT are on 230 V mains: wiring them is a job for a licensed electrician,
# inside a closed enclosure, behind an MCB/RCCB, with the supply switched off.
from machine import UART, Pin
import struct
import time
uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1), timeout=200)
led = Pin("LED", Pin.OUT)
def crc16(data): # Modbus CRC-16: init 0xFFFF, reflected polynomial 0xA001
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc
def read_all(addr=0xF8):
"""Read the 10 input registers. 0xF8 = 'whoever is listening' (one PZEM on the bus)."""
req = bytes([addr, 0x04, 0x00, 0x00, 0x00, 0x0A])
req += struct.pack("<H", crc16(req))
uart.read() # drop anything stale
uart.write(req)
resp = uart.read(25) # addr, func, count(20), 20 data bytes, CRC
if not resp or len(resp) != 25:
return None
if crc16(resp[:23]) != struct.unpack("<H", resp[23:])[0] or resp[1] != 0x04:
return None
r = struct.unpack(">10H", resp[3:23])
return {
"volts": r[0] / 10,
"amps": (r[1] | (r[2] << 16)) / 1000,
"watts": (r[3] | (r[4] << 16)) / 10,
"kwh": (r[5] | (r[6] << 16)) / 1000,
"hz": r[7] / 10,
"pf": r[8] / 100,
}
print("PZEM-004T on UART0 (GP0/GP1) - the module only answers while its L/N side has mains")
while True:
m = read_all()
if m is None:
led.off()
print("No answer - mains off on L/N, TX/RX not crossed, or divider missing")
else:
led.toggle()
print("{volts:.1f} V {amps:.3f} A {watts:.1f} W PF {pf:.2f} {hz:.1f} Hz total {kwh:.3f} kWh".format(**m))
time.sleep(2)# PZEM-004T v3.0 energy meter on a Raspberry Pi Pico 2 W (MicroPython, Modbus-RTU, no library).
# Low-voltage side only: 5V -> VBUS (pin 40), GND -> GND, PZEM RX <- GP0 (pin 1, UART0 TX),
# PZEM TX -> 1 kOhm -> GP1 (pin 2, UART0 RX), and 2 kOhm from GP1 to GND.
# The L/N terminals and the CT are on 230 V mains: wiring them is a job for a licensed electrician,
# inside a closed enclosure, behind an MCB/RCCB, with the supply switched off.
from machine import UART, Pin
import struct
import time
uart = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1), timeout=200)
led = Pin("LED", Pin.OUT)
def crc16(data): # Modbus CRC-16: init 0xFFFF, reflected polynomial 0xA001
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc
def read_all(addr=0xF8):
"""Read the 10 input registers. 0xF8 = 'whoever is listening' (one PZEM on the bus)."""
req = bytes([addr, 0x04, 0x00, 0x00, 0x00, 0x0A])
req += struct.pack("<H", crc16(req))
uart.read() # drop anything stale
uart.write(req)
resp = uart.read(25) # addr, func, count(20), 20 data bytes, CRC
if not resp or len(resp) != 25:
return None
if crc16(resp[:23]) != struct.unpack("<H", resp[23:])[0] or resp[1] != 0x04:
return None
r = struct.unpack(">10H", resp[3:23])
return {
"volts": r[0] / 10,
"amps": (r[1] | (r[2] << 16)) / 1000,
"watts": (r[3] | (r[4] << 16)) / 10,
"kwh": (r[5] | (r[6] << 16)) / 1000,
"hz": r[7] / 10,
"pf": r[8] / 100,
}
print("PZEM-004T on UART0 (GP0/GP1) - the module only answers while its L/N side has mains")
while True:
m = read_all()
if m is None:
led.off()
print("No answer - mains off on L/N, TX/RX not crossed, or divider missing")
else:
led.toggle()
print("{volts:.1f} V {amps:.3f} A {watts:.1f} W PF {pf:.2f} {hz:.1f} Hz total {kwh:.3f} kWh".format(**m))
time.sleep(2)Expected serial output
PZEM-004T on UART0 (GP0/GP1) - the module only answers while its L/N side has mains
231.4 V 0.412 A 84.6 W PF 0.89 50.0 Hz total 12.345 kWh
230.9 V 1.873 A 421.2 W PF 0.97 50.0 Hz total 12.346 kWhPZEM-004T on UART0 (GP0/GP1) - the module only answers while its L/N side has mains 231.4 V 0.412 A 84.6 W PF 0.89 50.0 Hz total 12.345 kWh 230.9 V 1.873 A 421.2 W PF 0.97 50.0 Hz total 12.346 kWh
Common errors
No answer - mains off on L/N…
The measuring side is powered from the mains it measures, so with no 230 V on L/N it never replies. Also check TX/RX are crossed and the divider is in place.
Voltage OK, current and power 0
The CT is around both conductors or not closed properly. An electrician should clamp it around the live wire alone.
kWh differs from the electricity meter
Expect about 1 % difference from the DISCOM meter. The energy count lives in the PZEM and survives power cuts.