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.
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 |
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
- Missing
yield()in tight loops: The ESP32 runs FreeRTOS. If you lock Core 1 in aforloop 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). - 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. - 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.
- 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).
- 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.
- 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.






