The Core Theory: Why Measure True Power and Power Factor?

When evaluating foundational projects of electrical engineering, bridging AC circuit theory with embedded digital measurement is the ultimate test of practical competence. A true power and power factor (PF) meter forces you to confront the difference between apparent power (VA) and true power (W).

In a purely resistive load, voltage and current are in phase. But introduce an inductive load—like an AC motor or a transformer—and the current waveform lags the voltage waveform. This phase shift ($\theta$) means the grid is supplying reactive power that does no real work. The relationship is defined as:

True Power (W) = V_rms × I_rms × cos(θ)

Worked Example: If your multimeter reads 120V RMS and your clamp meter reads 5A RMS, the apparent power is 600 VA. But if the load is a motor with a 0.75 power factor (a 41.4° phase lag), the true power consumed is only 120 × 5 × 0.75 = 450W. The remaining 150 VAR (volt-amps reactive) just sloshes back and forth, heating up your wires. Measuring this accurately requires sampling both waveforms simultaneously to detect the zero-crossing time delta.

Hardware Spec Sheet & Pin Mapping

To build this, we are stepping away from abstract schematics and using real, bench-proven modules. Total BOM cost is roughly $18 USD.

Component Exact Variant / Model Key Spec Approx. Cost
Microcontroller ESP32-WROOM-32 DevKit V1 (30-pin) 12-bit ADC, 240MHz dual-core $6.00
Voltage Sensor ZMPT101B Active AC Voltage Module 0-250V AC, analog out, onboard op-amp $4.50
Current Sensor ACS712-20A Hall Effect Module 100mV/A sensitivity, 20A max $3.50
Passive Components 10kΩ Trimpot, 4.7kΩ Resistors, 100nF Caps For offset tuning and RC filtering $1.00
Power Supply Hi-Link HLK-PM01 120/230V AC to 5V DC isolated $3.00
⚠️ CRITICAL SAFETY WARNING: This project interfaces directly with mains voltage (>50V AC). Before making any connections, de-energize the circuit at the breaker panel, apply Lockout/Tagout (LOTO), and verify the circuit is dead using a CAT III or CAT IV non-contact voltage tester and a multimeter. Local electrical codes (NEC/IEC) may require this work to be performed or inspected by a licensed electrician.

ESP32 Pin Mapping Table

The ESP32-WROOM-32 ADC is notoriously non-linear near the 0V and 3.3V rails. We map our analog signals to ADC1 pins (which do not conflict with WiFi) and ensure the AC waveform is centered at 1.65V with a peak-to-peak swing no greater than 2.0V.

Sensor Module Pin ESP32-WROOM-32 Pin Function / Notes
ZMPT101B VCC 3V3 Requires clean 3.3V; add 100nF decoupling cap
ZMPT101B GND GND Common ground with ACS712
ZMPT101B OUT GPIO 34 (ADC1_CH6) Input only; no internal pull-up needed
ACS712 VCC 5V (VIN) Needs 5V for internal Hall sensor, output swings around 2.5V
ACS712 GND GND Common ground
ACS712 OUT GPIO 35 (ADC1_CH7) Requires voltage divider (10k/10k) to drop 2.5V center to 1.65V for ESP32

Step-by-Step Assembly & Calibration

  1. Prepare the Voltage Divider: The ACS712 outputs an analog signal centered at VCC/2. If powered by 5V, the center is 2.5V. The ESP32 ADC saturates above 2.5V. Solder a voltage divider using two 10kΩ 1% resistors to shift the 2.5V offset down to 1.65V (matching the ESP32's 3.3V VCC/2).
  2. Wire the Mains Side: Connect the AC Live wire through the IP (Input Positive) and IP- (Input Negative) screw terminals of the ACS712. Connect the ZMPT101B AC input terminals in parallel with your load. Double-check that no low-voltage DC wires cross the mains terminal block.
  3. Calibrate the ZMPT101B Offset: Power up the low-voltage DC side only (keep mains off). Use a multimeter to measure the DC voltage between the ZMPT101B OUT pin and GND. Adjust the blue trimpot on the module until the multimeter reads exactly 1.65V. This is your zero-crossing baseline.
  4. Verify Signal Swing: If you have an oscilloscope, probe the OUT pins. Apply mains power safely. The ZMPT101B should output a clean sine wave centered at 1.65V, peaking no higher than 2.4V and no lower than 0.9V. If it clips, adjust the trimpot to reduce the gain.

Complete ESP32 Firmware with Error Handling

This code targets the ESP32 Dev Module (ESP32-WROOM-32) board variant in the Arduino IDE. It utilizes a high-frequency sampling loop to calculate RMS values and detect the time delta between voltage and current zero-crossings to determine the phase angle. It includes Watchdog Timer (WDT) feeding and NaN (Not a Number) error handling to prevent silent data corruption.


#include 
#include 
#include "esp_task_wdt.h"

// --- PIN DEFINITIONS ---
#define VOLTAGE_PIN 34
#define CURRENT_PIN 35

// --- CALIBRATION CONSTANTS ---
// Adjust these based on your specific module gain and voltage divider
const float V_CAL = 1.25;   // Voltage calibration factor
const float I_CAL = 0.075;  // Current calibration factor (ACS712-20A = 100mV/A)
const float ADC_VREF = 3.3;
const int ADC_RESOLUTION = 4095;

// --- SAMPLING PARAMETERS ---
const int SAMPLES = 1000;
const int GRID_FREQ = 60; // 60Hz for North America, 50Hz for EU/UK
const float CYCLE_TIME_US = 1000000.0 / GRID_FREQ;

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  analogReadResolution(12);
  analogSetAttenuation(ADC_11db); // Full 3.3V range
  
  // Initialize Task Watchdog Timer to catch infinite loops
  esp_task_wdt_init(3, true); // 3 second timeout
  esp_task_wdt_add(NULL);
  
  Serial.println("ESP32 True Power & PF Meter Initialized.");
}

void loop() {
  long sumV = 0, sumI = 0;
  int lastV_sign = 0, lastI_sign = 0;
  unsigned long V_zero_cross_time = 0, I_zero_cross_time = 0;
  bool V_crossed = false, I_crossed = false;
  
  unsigned long start_micros = micros();
  
  for (int i = 0; i < SAMPLES; i++) {
    int rawV = analogRead(VOLTAGE_PIN);
    int rawI = analogRead(CURRENT_PIN);
    
    // Convert to signed values centered around 0 (assuming 1.65V offset = 2048 ADC)
    double instV = (rawV - 2048) * (ADC_VREF / ADC_RESOLUTION) * V_CAL;
    double instI = (rawI - 2048) * (ADC_VREF / ADC_RESOLUTION) * I_CAL;
    
    sumV += (long)(instV * instV * 10000); // Scale up to avoid float math in loop
    sumI += (long)(instI * instI * 10000);
    
    // Zero-crossing detection for Phase Angle
    int currentV_sign = (instV > 0) ? 1 : -1;
    int currentI_sign = (instI > 0) ? 1 : -1;
    
    if (lastV_sign != 0 && currentV_sign != lastV_sign && !V_crossed) {
      V_zero_cross_time = micros();
      V_crossed = true;
    }
    if (lastI_sign != 0 && currentI_sign != lastI_sign && !I_crossed) {
      I_zero_cross_time = micros();
      I_crossed = true;
    }
    
    lastV_sign = currentV_sign;
    lastI_sign = currentI_sign;
    
    // Feed the watchdog to prevent panic during long sampling
    esp_task_wdt_reset();
  }
  
  // Calculate RMS
  double V_rms = sqrt((double)sumV / (SAMPLES * 10000));
  double I_rms = sqrt((double)sumI / (SAMPLES * 10000));
  double apparent_power = V_rms * I_rms;
  
  // Calculate Phase Angle and Power Factor
  double pf = 1.0;
  double phase_angle = 0.0;
  
  if (V_crossed && I_crossed && apparent_power > 1.0) {
    long time_delta_us = (long)(I_zero_cross_time - V_zero_cross_time);
    // Handle micros() overflow or negative delta
    if (time_delta_us < 0) time_delta_us += CYCLE_TIME_US; 
    
    phase_angle = (time_delta_us / CYCLE_TIME_US) * 360.0;
    if (phase_angle > 180.0) phase_angle -= 360.0;
    
    pf = cos(phase_angle * (M_PI / 180.0));
  }
  
  double true_power = apparent_power * pf;
  
  // Error Handling: Check for NaN or disconnected sensors
  if (isnan(V_rms) || isnan(I_rms) || V_rms > 300.0 || I_rms > 25.0) {
    Serial.println("ERROR: Sensor read out of bounds or NaN. Check wiring and offsets.");
  } else {
    Serial.printf("V: %.1f V | I: %.2f A | Apparent: %.1f VA | True: %.1f W | PF: %.2f | Phase: %.1f deg\n", 
                  V_rms, I_rms, apparent_power, true_power, pf, phase_angle);
  }
  
  // Yield to RTOS background tasks (WiFi/BT stack)
  yield();
  delay(500);
}

Debugging: Sensor Drift and Core Panics

When working with high-frequency ADC sampling on the ESP32, the most common catastrophic failure is the RTOS Watchdog triggering because your sampling loop hogs the CPU. If your serial monitor spits out the following exact error string:

Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)

Ranked Causes:

  1. Blocking ADC Loop: You removed the esp_task_wdt_reset() or yield() calls from the sampling for loop, starving the RTOS idle task.
  2. Heavy Floating-Point Math in ISR: If you moved the RMS calculation into an interrupt service routine (ISR) triggered by a timer, the ESP32's FPU overhead will exceed the WDT timeout. Keep math in the main loop().
  3. Serial Print Bottleneck: Calling Serial.printf() inside the high-speed sampling loop fills the UART buffer, blocking execution.

The First Three Things to Check When It Fails

  1. Measure the DC Offset: If your RMS readings are wildly incorrect (e.g., 400V on a 120V line), your ZMPT101B trimpot has drifted. Disconnect mains, measure the OUT pin to GND, and re-tune to exactly 1.65V.
  2. Verify the Voltage Divider: If the current reading is pegged at maximum or zero, check the 10k/10k voltage divider on the ACS712 output. The ESP32 cannot read the 2.5V center point accurately without stepping it down to 1.65V.
  3. Check for Ground Loops: If the ADC readings are noisy (PF jumping from 0.4 to 0.9 randomly), ensure the HLK-PM01 DC ground is tied directly to the ESP32 GND pin, and that no high-current AC wires are running parallel to your analog sensor traces.

For deeper RTOS debugging, consult the Espressif Watchdog Timer Documentation.

Extending and Simplifying the Build

How to Simplify: If you only need to monitor energy consumption for a smart plug and don't care about power factor or phase angle, drop the ZMPT101B entirely. Assume a fixed grid voltage (e.g., 120V), measure only the RMS current with the ACS712, and multiply. This cuts your code complexity and BOM cost in half.

How to Extend: For a safer, non-invasive build, replace the ACS712 with an SCT-013-000 split-core current transformer. Because the SCT-013 outputs an AC current, you must run it through a burden resistor (e.g., 22Ω) and an op-amp summing circuit (like the LM358) to create a 1.65V DC offset. To make it IoT-ready, add the PubSubClient library and publish the True Power and PF metrics via MQTT to a Home Assistant broker. For advanced AC theory on apparent vs. reactive power, see the All About Circuits AC Power Guide.

FAQ: Common Questions on Electrical Engineering Projects

What are the best beginner projects of electrical engineering for AC circuits?

The best beginner projects isolate one variable at a time. Before building a full power factor meter, start with a simple zero-crossing detector using an H11AA1 optocoupler to safely trigger an interrupt on the ESP32. This teaches you mains isolation and AC frequency measurement without the complexity of analog waveform sampling. Once you can accurately measure 60Hz, move on to RMS current measurement using a split-core CT.

How do university projects of electrical engineering differ from DIY embedded builds?

University projects of electrical engineering often focus on theoretical simulation (using MATLAB/Simulink or LTspice) and PCB-level design using raw ICs rather than pre-built modules. A DIY embedded build uses modules like the ZMPT101B, which already includes the operational amplifiers and filtering. To bridge the gap, DIY builders should eventually design their own custom PCB with isolated power supplies and dedicated ADC driver chips (like the Texas Instruments ADS1115) to bypass the ESP32's internal ADC non-linearities.

Can I use an Arduino Uno instead of an ESP32 for power measurement projects?

Yes, but with severe limitations. The Arduino Uno (ATmega328P) features a 10-bit ADC (1024 steps) compared to the ESP32's 12-bit ADC (4096 steps), resulting in much lower resolution for small current measurements. Furthermore, the Uno lacks a true RTOS and WiFi. While the Uno is perfectly fine for a basic RMS ammeter, calculating power factor requires precise microsecond timing between two analog reads; the ESP32's 240MHz dual-core processor handles the simultaneous sampling and floating-point math far more reliably than the Uno's 16MHz single-core chip.