If you want to move beyond blinking LEDs and tackle a real-world electrical engineering project, building a True RMS AC power analyzer is the ultimate bridge between embedded firmware and AC circuit theory. This project targets the ESP32-DevKitC V4 (WROOM-32E) module to sample AC waveforms, calculate True RMS voltage and current, and derive apparent vs. real power.

Unlike cheap multimeters that assume a perfect sine wave and just multiply the peak voltage by 0.707, a True RMS meter samples the waveform thousands of times per second, squares the values, averages them, and takes the square root. This is the only way to accurately measure power on modern non-linear loads like LED drivers and switching power supplies.

⚠️ MAINS VOLTAGE HAZARD: This project interfaces with 120V/230V AC mains. Always de-energize the circuit, lock out the breaker, and verify dead with a CAT III multimeter before making connections. The ZMPT101B primary terminals carry lethal voltage. If you are not comfortable with mains wiring, use a 12V AC wall transformer for bench testing.

Sensor Specifications and AC Theory Constraints

The most common mistake in power measurement projects is choosing the wrong sensor. Hall-effect sensors are easy to wire but terrible for precision AC work due to offset drift and bandwidth limitations. Here is the data-dense breakdown of why we chose specific transformers for this build.

Sensor Model Type Ratio / Sensitivity Phase Shift Error Best Application
ZMPT101B Active Voltage Transformer 1000:1000 (with onboard op-amp) < 3° at 50/60Hz True RMS Voltage, Zero-crossing
SCT-013-000 Passive Current Transformer (CT) 2000:1 (50mA at 100A) < 1° at 50/60Hz True RMS Current, Power Factor
ACS712-30A Hall-Effect IC 66 mV/A High (frequency dependent) DC/AC rough estimation only
ZMCT103C Miniature Precision CT 1000:1000 < 2° Compact PCB-mount metering

Source: Sensor phase shift characteristics heavily impact Power Factor (PF) calculations. For deep theory on waveform distortion, refer to the Fluke True RMS measurement guide.

Hardware Wiring and Pin Mapping

The ESP32-WROOM-32E features a 12-bit ADC, but it is notoriously non-linear near the 0V and 3.3V rails. To measure AC (which swings positive and negative), we must bias the signal to the middle of the ADC's linear range (approximately 1.55V) using a voltage divider.

Complete Parts List

  • MCU: ESP32-DevKitC V4 (WROOM-32E variant)
  • Voltage Sensor: ZMPT101B module (adjustable gain)
  • Current Sensor: SCT-013-000 (100A, no internal burden)
  • Burden Resistor: 33Ω 1/4W (for SCT-013-000)
  • Bias Network: Two 10kΩ resistors, one 10µF capacitor
  • Display: 0.96" SSD1306 I2C OLED (128x64)

Pin Mapping Table

Component Module Pin ESP32-DevKitC V4 Pin Notes
ZMPT101B AO (Analog Out) GPIO 34 (ADC1_CH6) Input only, no internal pullup
SCT-013 (via Burden) Signal GPIO 35 (ADC1_CH7) Input only, tie to 1.55V bias
Bias Divider Midpoint GPIO 35 & ZMPT101B GND ref 10k to 3.3V, 10k to GND, 10µF cap
SSD1306 OLED SDA GPIO 21 Default I2C SDA
SSD1306 OLED SCL GPIO 22 Default I2C SCL
Bench Tip: The SCT-013-000 outputs current, not voltage. You must place a 33Ω burden resistor across its output leads to convert the current to a measurable voltage (V = I × R). At 100A primary, secondary is 50mA. 50mA × 33Ω = 1.65V peak, which perfectly fits the ESP32's 11dB attenuation window.

Firmware: True RMS Sampling and Error Handling

The following C++ code targets the ESP32-DevKitC V4. It uses direct ADC polling to capture one full 60Hz cycle (16.6ms), calculates the True RMS, and handles I2C initialization errors gracefully. You will need the Adafruit_SSD1306 and Adafruit_GFX libraries installed via the Arduino Library Manager.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- Pin Definitions ---
#define V_PIN 34
#define I_PIN 35
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C

// --- Calibration & Theory Constants ---
#define ADC_RESOLUTION 4095.0
#define V_CALIBRATION 0.168  // Calibrate against a known True RMS multimeter
#define I_CALIBRATION 28.5   // Amps per volt at the ADC pin (depends on burden)
#define DC_BIAS 1800         // Raw ADC value for 1.55V bias (approx)
#define SAMPLES_PER_CYCLE 300 // ~18kHz sampling rate for 60Hz

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  analogSetAttenuation(ADC_11db); // Crucial: Allows 0-3.1V range

  // Error Handling: I2C OLED Initialization
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("ERROR: SSD1306 allocation failed. Check I2C wiring."));
    // Blink onboard LED to indicate hardware fault without halting entirely
    pinMode(2, OUTPUT);
    while(1) { digitalWrite(2, !digitalRead(2)); delay(250); }
  }
  
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.println("Power Analyzer Ready");
  display.display();
  delay(1000);
}

void loop() {
  float sumV = 0, sumI = 0;
  int rawV, rawI;
  
  // Sample one full AC cycle
  unsigned long startTime = micros();
  for(int i = 0; i < SAMPLES_PER_CYCLE; i++) {
    rawV = analogRead(V_PIN) - DC_BIAS;
    rawI = analogRead(I_PIN) - DC_BIAS;
    
    // Sum of squares for True RMS
    sumV += (float)rawV * rawV;
    sumI += (float)rawI * rawI;
    
    // Prevent Watchdog Timer (WDT) resets during tight loops
    if(i % 50 == 0) yield(); 
  }
  unsigned long duration = micros() - startTime;
  
  // Calculate RMS (Root Mean Square)
  float rmsV_raw = sqrt(sumV / SAMPLES_PER_CYCLE);
  float rmsI_raw = sqrt(sumI / SAMPLES_PER_CYCLE);
  
  // Convert to real-world units
  float voltage_rms = rmsV_raw * V_CALIBRATION;
  float current_rms = rmsI_raw * I_CALIBRATION;
  float apparent_power = voltage_rms * current_rms;
  
  // Output to OLED
  display.clearDisplay();
  display.setCursor(0,0);
  display.printf("Vrms: %5.1f V", voltage_rms);
  display.setCursor(0,16);
  display.printf("Irms: %5.2f A", current_rms);
  display.setCursor(0,32);
  display.printf("App Pwr: %4.0f VA", apparent_power);
  display.setCursor(0,48);
  display.printf("Sample: %lu us", duration);
  display.display();
  
  delay(500);
}

Debugging: First Three Checks and Common Faults

When working with high-speed ADC sampling on the ESP32, things will go wrong. If your serial monitor spits out the dreaded Guru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1), it means your sampling loop took too long and starved the RTOS background tasks.

Ranked Causes for WDT and ADC Faults

  1. Missing yield() in tight loops: The ESP32 runs FreeRTOS. If you lock Core 1 in a for loop for more than a few milliseconds without yielding, the Watchdog Timer (WDT) assumes the system is frozen and reboots it. (Fixed in the code above).
  2. Incorrect ADC Attenuation: If you forget analogSetAttenuation(ADC_11db), the ESP32 defaults to 0dB, clipping any input above 1.0V. Your AC waveform will look like a square wave, and your RMS math will be wildly inflated.
  3. DC Bias Drift: If your 10kΩ bias resistors are low tolerance (5%), your midpoint might be 1.4V instead of 1.55V. This pushes the negative half of the AC wave into the non-linear bottom 10% of the ESP32 ADC curve, causing massive current reading errors at low loads.
The First 3 Things to Check When It Fails:
  1. Measure the Bias: Put your multimeter on the SCT-013 signal pin with the AC load disconnected. It should read exactly 1.55V DC (±0.05V).
  2. Verify I2C Address: Run an I2C scanner sketch. Many cheap SSD1306 displays use 0x3C, but some use 0x3D. If it fails to init, check the hex address.
  3. Check Burden Heating: Touch the 33Ω burden resistor. If it's hot, your primary current is exceeding 100A, or you have a short. A 1/4W resistor will burn up at sustained 80A+ loads; upgrade to a 1W metal film resistor for heavy continuous loads.

For deeper architectural understanding of the ESP32's ADC non-linearities and hardware limitations, consult the official Espressif ESP32 ADC API Reference.

Extending and Simplifying the Build

Not every application requires raw ADC sampling. Here is how to scale this electrical engineering project up or down based on your actual deployment needs.

How to Simplify (The Pragmatic Route)

If you don't need to log high-frequency harmonics and just want reliable billing-grade power data, ditch the raw sensors and use a PZEM-004T v3 module. It contains a dedicated metrology chip (V9821S) that handles True RMS, PF, and energy accumulation internally, communicating via UART. You lose the embedded signal-processing learning experience, but you gain rock-solid reliability and isolation.

How to Extend (The Advanced Route)

  • Calculate Real Power & PF: To get Real Power (Watts) instead of just Apparent Power (VA), you must multiply the instantaneous voltage and current samples together inside the loop, average those products, and then divide by the apparent power. This requires precise phase-matching.
  • Add MQTT Telemetry: Swap the OLED for an ESP32-S3 with native WiFi, push the JSON payload to an MQTT broker (like Mosquitto), and graph the harmonics in Grafana.
  • 3-Phase Tracking: Use an ESP32 with three separate ADC1 channels (GPIO 32, 33, 34) to monitor all three legs of a 208V/480V service, ensuring you use hardware timers to trigger simultaneous ADC sampling across all pins to prevent phase-skew errors.