If you want to bridge the gap between high-voltage AC theory and low-voltage embedded DSP, building a true RMS and power factor meter is where it’s at. While many cool electrical engineering projects stop at blinking LEDs or reading basic DC sensors, measuring AC phase angles forces you to deal with real-world signal conditioning, discrete integration, and interrupt-driven sampling. In this guide, we are building a benchtop AC Power Factor Meter targeting the ESP32-DevKitC V4 (ESP32-WROOM-32U variant). You will measure Voltage (RMS), Current (RMS), Real Power (Watts), Apparent Power (VA), and the Power Factor (PF) of any AC load up to 100A.
Project Spec Sheet & Parts List
To get accurate zero-crossing detection and RMS calculations, your analog front-end must be stable. Do not substitute the ZMPT101B with a simple resistor divider; the isolation and op-amp buffering are mandatory for safety and signal integrity.
| Component | Exact Variant / Model | Purpose | Est. Cost (2026) |
|---|---|---|---|
| Microcontroller | ESP32-DevKitC V4 (WROOM-32U, 38-pin) | Dual-core DSP, 12-bit ADC, WiFi | $6.50 |
| Voltage Sensor | ZMPT101B Module (with LM393 comparator) | Isolated AC step-down & biasing | $3.20 |
| Current Sensor | SCT-013-000 (100A / 50mA output) | Split-core non-invasive CT | $8.00 |
| Burden Resistor | 33Ω 1/4W Metal Film (1% tolerance) | Converts CT current to voltage | $0.10 |
| DC Bias Network | 2x 10kΩ resistors + 10µF electrolytic cap | Biases AC signal to 1.65V for ESP32 ADC | $0.25 |
| Display | SSD1306 128x64 I2C OLED (0x3C address) | Real-time telemetry readout | $4.50 |
Pin Mapping & Mains Safety Wiring
| Sensor / Module | Sensor Pin | ESP32-WROOM-32U Pin | Notes |
|---|---|---|---|
| ZMPT101B | VCC | 5V (VIN) | Requires 5V for internal op-amp headroom |
| ZMPT101B | GND | GND | Common ground with ESP32 |
| ZMPT101B | AO (Analog Out) | GPIO 34 (ADC1_CH6) | Input only pin, no internal pull-up |
| SCT-013-000 | Tip (via 33Ω burden) | GPIO 35 (ADC1_CH7) | Must be biased to 1.65V via 10k/10k divider |
| SCT-013-000 | Sleeve | GND | Connect to the bottom of the bias divider |
| SSD1306 OLED | SDA | GPIO 21 | I2C Data (add 4.7k pull-up if missing on module) |
| SSD1306 OLED | SCL | GPIO 22 | I2C Clock |
Complete ESP32 Power Factor Code
The code below targets the Arduino framework for the ESP32. It uses a discrete integration method over 10 AC cycles (166.6ms for 60Hz) to calculate True RMS and Real Power. Apparent power is simply V_rms * I_rms, and Power Factor is Real Power / Apparent Power. For a deeper dive into the math behind this sampling, refer to the OpenEnergyMonitor CT/AC Theory documentation.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// --- PIN DEFINITIONS ---
#define PIN_VOLTAGE 34
#define PIN_CURRENT 35
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
// --- CALIBRATION CONSTANTS ---
// Adjust these based on your multimeter readings
const float V_CAL = 138.5; // Maps ADC raw to Volts
const float I_CAL = 28.4; // Maps ADC raw to Amps
const float PHASE_CAL = 1.7; // Phase shift compensation (microseconds)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
unsigned long lastCalcTime = 0;
const int SAMPLES_PER_CYCLE = 100;
const int NUM_CYCLES = 10; // 10 cycles at 60Hz = 166.6ms
void setup() {
Serial.begin(115200);
analogReadResolution(12); // ESP32 native 12-bit ADC
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Halt execution
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0,0);
display.println("AC PF Meter Ready");
display.display();
delay(1000);
}
void loop() {
unsigned long startMicros = micros();
double sumV_sq = 0, sumI_sq = 0, sumP = 0;
int lastV = 0, lastI = 0;
// Sample over 10 full AC cycles
for(int i = 0; i < SAMPLES_PER_CYCLE * NUM_CYCLES; i++) {
int rawV = analogRead(PIN_VOLTAGE);
int rawI = analogRead(PIN_CURRENT);
// Remove DC bias (assuming 1.65V bias = 2048 on 12-bit scale)
int offsetV = rawV - 2048;
int offsetI = rawI - 2048;
// Phase shift compensation (simple linear extrapolation)
int shiftedI = offsetI + PHASE_CAL * (offsetI - lastI);
sumV_sq += (double)offsetV * offsetV;
sumI_sq += (double)shiftedI * shiftedI;
sumP += (double)offsetV * shiftedI;
lastV = offsetV;
lastI = offsetI;
// Delay to space samples evenly across the 166.6ms window
delayMicroseconds(15);
}
unsigned long endMicros = micros();
// Calculate RMS and Power
double V_rms = V_CAL * sqrt(sumV_sq / (SAMPLES_PER_CYCLE * NUM_CYCLES));
double I_rms = I_CAL * sqrt(sumI_sq / (SAMPLES_PER_CYCLE * NUM_CYCLES));
double RealPower = V_CAL * I_CAL * (sumP / (SAMPLES_PER_CYCLE * NUM_CYCLES));
double ApparentPower = V_rms * I_rms;
double PowerFactor = 0.0;
if(ApparentPower > 5.0) { // Avoid division by zero / noise at no load
PowerFactor = abs(RealPower / ApparentPower);
}
// Update Display
display.clearDisplay();
display.setCursor(0,0);
display.printf("V: %5.1f V\n", V_rms);
display.printf("I: %5.2f A\n", I_rms);
display.printf("P: %5.1f W\n", RealPower);
display.printf("S: %5.1f VA\n", ApparentPower);
display.printf("PF: %5.3f\n", PowerFactor);
display.display();
// Serial output for logging
Serial.printf("V:%.1f I:%.2f W:%.1f PF:%.3f\n", V_rms, I_rms, RealPower, PowerFactor);
}
Debugging: First Three Checks & Common Errors
When working with high-impedance analog sensors and the ESP32’s notoriously noisy ADC, things will go wrong. If your serial monitor spits out garbage or the screen stays black, run through these first three checks:
- Verify the DC Bias Voltage: Unplug the SCT-013 and ZMPT101B signal wires. Use your multimeter to measure the voltage at the ESP32 GPIO 35 and GPIO 34 pins. You must read exactly 1.65V (half of 3.3V). If it reads 0V or 3.3V, your 10kΩ voltage divider is wired backward or the op-amp on the ZMPT101B isn't getting 5V on its VCC pin.
- Check I2C Pull-up Resistors: Many cheap SSD1306 modules lack the required 4.7kΩ pull-up resistors on the SDA/SCL lines. The ESP32’s internal pull-ups are too weak for reliable I2C at 400kHz.
- Isolate the Ground Loop: If your current readings fluctuate wildly when a compressor kicks on, your sensor ground is sharing a return path with a noisy digital load. Star-ground your analog sensors directly to the ESP32 GND pin, not through a breadboard power rail.
[E][Wire.cpp:499] requestFrom(): i2cWriteReadNonStop returned Error 263 (ESP_ERR_TIMEOUT)Ranked Causes:
1. Missing or blown I2C pull-up resistors on the OLED module.
2. SDA/SCL jumper wire has an internal break (very common with cheap dupont cables).
3. The OLED module is actually addressed at 0x3D instead of 0x3C. Run an I2C scanner sketch to verify.
Extending and Simplifying the Build
Depending on your bench space and end goal, you can easily modify this architecture:
- To Simplify: Drop the SSD1306 OLED entirely. Remove the
Wire.handAdafruitincludes, delete the display update block in the loop, and rely purely on the Serial Plotter. This frees up CPU cycles and eliminates I2C bus lockups, allowing you to increase the sampling rate for higher resolution zero-crossing detection. - To Extend (IoT Integration): Add the
PubSubClientlibrary to push the Real Power and PF data to an MQTT broker (like Mosquitto or Home Assistant) every 5 seconds. This turns the project into a whole-home energy monitor. Just ensure you use the ESP32's core 0 for WiFi tasks and core 1 for ADC sampling to prevent the FreeRTOS Watchdog from triggering during WiFi transmission delays. - To Extend (Power Factor Correction): Wire a 5V relay module to GPIO 26. If the PF drops below 0.85 (indicating a heavy inductive load like an unwound motor), trigger the relay to switch in a parallel run capacitor bank, actively correcting the phase angle in real-time.
FAQ: Cool Electrical Engineering Projects
What are some cool electrical engineering projects for beginners that teach AC theory?
Before tackling true RMS power meters, beginners should start with an AC zero-crossing detector using an H11AA1 optocoupler. It safely steps down mains voltage and outputs a clean 3.3V logic pulse every time the AC sine wave crosses 0V. This teaches the fundamental concept of phase timing and isolation without requiring complex analog biasing or high-speed ADC sampling. Once you understand zero-crossing, stepping up to a full power factor meter becomes much more intuitive.
How do cool electrical engineering projects like this handle AC safety and isolation?
Professional and educational projects rely on galvanic isolation to protect the low-voltage microcontroller (and the user) from lethal mains faults. In this build, the ZMPT101B uses a miniature transformer to magnetically couple the voltage signal, meaning there is no direct electrical path between the 120V/240V mains and the ESP32. The SCT-013 current transformer operates on the same principle; it clamps around the insulated wire, never making bare-metal contact with the live conductor. Never use direct-connected resistor dividers for mains voltage measurement on a hobbyist bench.
Can I use an Arduino Uno instead of an ESP32 for these cool electrical engineering projects?
You can, but you will hit severe hardware limitations. The Arduino Uno’s ATmega328P features a 10-bit ADC (compared to the ESP32’s 12-bit), which drastically reduces your current measurement resolution at low loads. More importantly, the Uno only has one core. If you try to write data to an SD card or transmit over WiFi via an ESP-01 shield while simultaneously sampling the AC waveform at 6kHz, the sampling loop will stall, destroying your phase angle calculations and resulting in wildly inaccurate Power Factor readings. The ESP32’s dual-core architecture and native 12-bit SAR ADC make it the undisputed choice for embedded AC DSP.






