Learn

Measure AC voltage and energy with an Arduino: the PZEM-004T and the ZMPT101B

Intermediate

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
Arduino Uno: a PZEM-004T energy monitorlive0.000 s 0.00x
Click to open it in the editor
Press Run. The Uno reads the meter every two seconds: 230.00 V, 4.35 A, 1000.50 W, and the kWh counting up. Drag the voltage, current, power factor and frequency sliders on the meter.

Safety first. Both modules on this page are made to be wired to household mains, and mains can kill you or start a fire. Only a qualified person should connect them, inside a closed enclosure. Here in the browser the mains is never wired at all: it is a set of sliders on each module, so you can learn the code with nothing dangerous anywhere near you.

The PZEM-004T v3.0 is a whole energy meter on a board. It measures the mains itself and hands the Arduino finished numbers over a serial line: voltage, current, power, energy in kWh, frequency and power factor. The ZMPT101B is much simpler. It is a small transformer and an op-amp that turn the mains into a little sine wave on an analog pin, and the Arduino does the math.

The circuit above is an Uno with a PZEM-004T on pins 2 and 3. The meter is set to a 1 kW heater on 230 V. Press Run and it prints a block of readings every two seconds. Drag the sliders on the meter and the next block follows.

What you need

  • An Arduino Uno
  • A PZEM-004T v3.0 (the 100 A one comes with a current transformer ring)
  • Or a ZMPT101B AC voltage sensor module
  • Jumper wires
  • For real mains: a qualified person and a closed enclosure

Wiring

Part and pinGoes toNote
PZEM 5V and GND5 V and GNDThe TTL side only; the mains side is on the screw terminals
PZEM TXPin 2Into the Uno: SoftwareSerial’s receive pin
PZEM RXPin 3Out of the Uno: SoftwareSerial’s transmit pin
ZMPT101B VCC and GND5 V and GNDOn 3.3 V its op-amp has almost no room for the wave
ZMPT101B OUTA0The scaled-down mains sine, centered at 2.5 V

The PZEM-004T: a meter that talks Modbus

The PZEM-004T has its own metering chip on the mains side and an optocoupled serial port on the other, so the Arduino never touches the mains. The serial port runs at 9600 baud and speaks Modbus-RTU: the Arduino sends a short request frame, and the meter answers with the contents of its registers.

Every reading is a register. Voltage is register 0 in tenths of a volt, so 230.0 V is 2300. Current takes two registers in thousandths of an amp, power two in tenths of a watt, and energy two in whole watt-hours. Then frequency in tenths of a hertz, power factor in hundredths, and an alarm flag.

The library asks for all ten at once. It sends F8 04 00 00 00 0A 64 64: the general address 0xF8 that every meter answers, function 04 (read input registers), start at register 0, read 10, then a two-byte CRC. The meter answers with 25 bytes, and PZEM004Tv30 turns them into floats.

The PZEM-004T v3.0 input registers, from its manual
RegisterWhat it isUnitThe 1 kW heater
0Voltage0.1 V2300 = 230.0 V
1 and 2Current, low and high word0.001 A4350 = 4.350 A
3 and 4Power, low and high word0.1 W10005 = 1000.5 W
5 and 6Energy, low and high word1 Whcounting up
7Frequency0.1 Hz500 = 50.0 Hz
8Power factor0.01100 = 1.00
9Alarm0xFFFF or 00

The energy monitor sketch

The Uno’s one hardware serial port is busy with the serial monitor, so the meter goes on a SoftwareSerial port on pins 2 and 3. PZEM004Tv30 pzem(pzemSerial) opens it at 9600 baud. Then each of voltage(), current(), power(), energy(), frequency() and pf() returns one reading, or NAN if the meter did not answer.

The library reads the meter at most every 200 ms and hands back the same numbers in between, so the six calls in a row are one Modbus read. It also skips a read in the first 200 ms after reset and returns 0, which is why the sketch waits 250 ms in setup().

The power is volts times amps times the power factor: 230 x 4.35 x 1.00 is 1000.5 W. The energy is that power added up over time, and a 1 kW heater uses 1 Wh every 3.6 seconds, so after 36 seconds the monitor prints "Energy: 0.010 kWh". energy() is in kWh, not Wh.

The PZEM-004T energy monitor
#include <PZEM004Tv30.h>
#include <SoftwareSerial.h>

SoftwareSerial pzemSerial(2, 3);  // RX (the meter's TX), TX (the meter's RX)
PZEM004Tv30 pzem(pzemSerial);

void setup() {
  Serial.begin(115200);
  Serial.println("PZEM-004T energy monitor");
  // The library skips any read in the first 200 ms after reset (it returns
  // 0), so give it that long before the first one.
  delay(250);
  Serial.print("Meter address: 0x");
  Serial.println(pzem.readAddress(), HEX);
}

void loop() {
  float voltage = pzem.voltage();
  float current = pzem.current();
  float power = pzem.power();
  float energy = pzem.energy();
  float frequency = pzem.frequency();
  float pf = pzem.pf();

  if (isnan(voltage) || isnan(current) || isnan(power) || isnan(energy) || isnan(frequency) || isnan(pf)) {
    Serial.println("Error reading the meter: is it on the mains?");
  } else {
    Serial.print("Voltage: ");   Serial.print(voltage);      Serial.println(" V");
    Serial.print("Current: ");   Serial.print(current);      Serial.println(" A");
    Serial.print("Power: ");     Serial.print(power);        Serial.println(" W");
    Serial.print("Energy: ");    Serial.print(energy, 3);    Serial.println(" kWh");
    Serial.print("Frequency: "); Serial.print(frequency, 1); Serial.println(" Hz");
    Serial.print("PF: ");        Serial.println(pf);
  }
  Serial.println();
  delay(2000);
}

What the readings do when you drag the sliders

Set the meter to 240 V and 8.5 A, a 2 kW kettle, and the next block reads "Voltage: 240.00 V", "Current: 8.50 A" and "Power: 2040.00 W". Drop the power factor to 0.50 and the power halves, because a motor or a power supply with a poor power factor draws current that does no work.

Drag the voltage to 0 and the sketch prints "Error reading the meter: is it on the mains?" with "Meter address: 0x0" at the start. That is real: the PZEM-004T’s metering side is powered from the mains it measures, so with no mains it does not answer at all, and the library returns NAN. A PZEM on your desk with only USB power reads NAN for the same reason.

Under 0.02 A the 100 A model reads 0 A, 0 W and a power factor of 0. That is its starting current from the manual: a phone charger on standby can be too small to register.

The ZMPT101B: the mains as a small sine wave

The ZMPT101B module has a tiny voltage transformer, an LM358 op-amp and a blue multi-turn trimmer. OUT rests at half the supply, 2.5 V on an Uno, which analogRead() reads as about 512, and the mains sine rides on top of that, made small. The trimmer sets how small.

One analogRead() is one point on the wave, so it is not the voltage. The voltage is the RMS, the root of the mean of the squares, taken over whole cycles and measured from the middle of the wave. The sketch below finds the middle first, the mean over five 50 Hz cycles, then the RMS about it, then multiplies by the trimmer’s ratio: how many mains volts make one volt at OUT.

On 230 V with the trimmer at a ratio of 600, OUT is a 0.383 V rms sine, and the sketch prints "middle 511.5 OUT 0.384 V rms mains 230.3 V". It takes about 880 readings in each 100 ms window, one every 114 microseconds.

The AC voltmeter, plain code
const int SENSOR = A0;
const float RATIO = 600.0;             // mains volts per volt at OUT, rms
const unsigned long WINDOW_US = 100000; // five 50 Hz cycles

void setup() {
  Serial.begin(115200);
  Serial.println("AC voltmeter");
}

void loop() {
  // The middle of the wave: the mean over whole cycles.
  unsigned long start = micros();
  long sum = 0;
  unsigned int n = 0;
  while (micros() - start < WINDOW_US) {
    sum += analogRead(SENSOR);
    n++;
  }
  float middle = (float)sum / n;

  // The RMS about it.
  start = micros();
  float squares = 0;
  unsigned int m = 0;
  while (micros() - start < WINDOW_US) {
    float d = analogRead(SENSOR) - middle;
    squares += d * d;
    m++;
  }
  float rmsCounts = sqrt(squares / m);
  float outVolts = rmsCounts * 5.0 / 1023.0;
  float mains = outVolts * RATIO;

  Serial.print("middle ");
  Serial.print(middle, 1);
  Serial.print("  OUT ");
  Serial.print(outVolts, 3);
  Serial.print(" V rms  mains ");
  Serial.print(mains, 1);
  Serial.println(" V");
  delay(1000);
}

The ZMPT101B library, and calibrating it

The ZMPT101B library by Abdurraziq Bachmid does the same in one call. ZMPT101B voltageSensor(A0, 50.0) names the pin and the mains frequency, setSensitivity() takes the trimmer’s ratio, and getRmsVoltage() measures for two mains periods, 40 ms at 50 Hz, and returns the volts. With the sensitivity at 600 it prints "Voltage: 230 V".

Nobody knows the ratio of a real board until they calibrate it. Read the mains with a meter you trust, then multiply the sensitivity by the meter’s reading over the sketch’s. The library starts at 500, and left there this board reads "Voltage: 192 V": 230 x 500 / 600.

Give it the right frequency too. On 60 Hz mains the constructor needs 60.0, because the library measures for one period of the frequency it was given. Told 50.0 on 120 V, 60 Hz mains, it averages over a cycle and a fifth and prints "Voltage: 125 V".

The ZMPT101B library
#include <ZMPT101B.h>

// Find this by calibrating against a meter: here, the part's trimmer setting.
#define SENSITIVITY 600.0f

// The module's OUT on A0, on 50 Hz mains.
ZMPT101B voltageSensor(A0, 50.0);

void setup() {
  Serial.begin(115200);
  voltageSensor.setSensitivity(SENSITIVITY);
}

void loop() {
  float voltage = voltageSensor.getRmsVoltage();
  Serial.print("Voltage: ");
  Serial.print(voltage, 0);
  Serial.println(" V");
  delay(1000);
}

The trimmer turned too far, and 3.3 V boards

The LM358 on the ZMPT101B cannot swing its output all the way to its supply. It tops out about 1.5 V short, at 3.5 V on 5 V, which analogRead() reads as 716. Turn the trimmer up to a ratio of 150 on 230 V and the wave wants to reach 4.67 V: its tops are cut off flat, the middle drops to 457, and the plain sketch reads "mains 184.4 V" instead of 230. If the highest reading sits at the same number cycle after cycle, the wave is clipped: turn the trimmer down.

On a 3.3 V board the middle is 1.65 V and the top is 1.8 V, only 0.15 V of room. That is why a ZMPT101B is usually run from 5 V, with its trimmer turned well down for a 3.3 V ADC.

What the simulation leaves out

The PZEM-004T’s registers read exactly what the sliders say, where a real one is within half a percent. Its answer comes 20 ms after the request here; the manual gives no figure. The ZMPT101B’s wave here is clean, where a real module has some noise and reads a few volts with no mains at all, and its phase shift and the mains’ own distortion are not modeled.

On the classic ESP32 the meter goes on Serial2, a real UART: PZEM004Tv30 pzem(Serial2, 16, 17) with the meter’s TX on GPIO 16 and its TTL side on 3.3 V. The ESP32-S3, C3 and C6 boards’ second UART is not wired to pins yet, so on those use an Uno, a Mega with Serial2, an ESP8266 with SoftwareSerial or a classic ESP32.

Common mistakes

TX and RX swapped. The meter’s TX goes to the Uno’s receive pin, pin 2 here.

A PZEM-004T on USB alone. The meter only answers when its mains side is powered, so on the bench it reads NAN until it is on the mains.

Reading a ZMPT101B once and calling it the voltage, or forgetting to take off the middle before squaring. Either way the number is mostly the 2.5 V bias.

Leaving the ZMPT101B library’s sensitivity at 500. Calibrate it against a meter.

Wiring a module to mains yourself. If you are not qualified to work on mains, do not: use the simulator, or a plug-in step-down transformer for the ZMPT101B.

Questions

How do I connect a PZEM-004T to an Arduino Uno?

Its 5V and GND to the Uno’s 5 V and GND, its TX to pin 2 and its RX to pin 3, then SoftwareSerial pzemSerial(2, 3) and PZEM004Tv30 pzem(pzemSerial). The mains side is a job for a qualified person.

Why does my PZEM-004T read NAN?

Most often it is not on the mains: its metering side is powered from the mains it measures, so with only USB power it does not answer. Otherwise check TX and RX, and that the sketch talks at 9600 baud.

Is PZEM-004T energy in kWh or Wh?

The register counts whole watt-hours, and PZEM004Tv30’s energy() divides by 1000, so energy() is kWh. A 1 kW load adds 0.001 every 3.6 seconds.

How do I calibrate a ZMPT101B?

Read the mains with a meter you trust, then set the sensitivity (or the ratio in plain code) to the old value times the meter’s reading over the sketch’s. Check that the wave is not clipped first.

Can I use a ZMPT101B on an ESP32?

The library builds on the ESP32 and reads its 12-bit ADC on 3.3 V. Powered from 3.3 V, the module’s LM358 leaves only 0.15 V of room over the 1.65 V middle, so turn the trimmer well down and check that the wave is not clipped.

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.