If you want to move beyond blinking LEDs and build truly awesome electronics projects, you need to bridge fundamental AC circuit theory with modern embedded systems. The most direct way to do this is by building a True-RMS Power and Power Factor Meter. The best approach for measuring AC power factor, real power, and true-RMS voltage/current on a hobbyist bench is pairing an ESP32-S3-DevKitC-1 with the Microchip ATM90E26 energy metering IC via SPI.
This build forces you to understand complex impedance, phase angles, and SPI bus timing, while giving you a genuinely useful tool for auditing the power draw of motors, transformers, and switching power supplies in your shop.
The Theory: True-RMS, Apparent Power, and Power Factor
Before writing a single line of code, you must understand what the hardware is actually calculating. In DC circuits, power is simply $P = V \times I$. In AC circuits, voltage and current are sinusoidal and frequently out of phase due to inductive or capacitive loads (like motors or LED drivers).
This phase shift ($\theta$) creates three distinct power measurements:
- Real Power (Watts): The actual work being done. Calculated as $V_{rms} \times I_{rms} \times \cos(\theta)$.
- Reactive Power (VAR): Power sloshing back and forth between the source and the load's magnetic/electric fields. Calculated as $V_{rms} \times I_{rms} \times \sin(\theta)$.
- Apparent Power (VA): The vector sum of real and reactive power. This is what your breakers and wires must be sized to handle.
The Power Factor (PF) is the ratio of Real Power to Apparent Power ($\cos \theta$). A purely resistive load (like a space heater) has a PF of 1.0. A heavily inductive load (like an unloaded induction motor) might have a PF of 0.6. Standard multimeters only measure average-responding RMS, which fails completely on non-linear loads like switching power supplies. The ATM90E26 calculates True-RMS by sampling the waveform at high speed and performing the root-mean-square math in hardware, yielding accurate readings regardless of waveform distortion.
Decision Path: Choosing Your Sensor IC and Board
When designing awesome electronics projects in the power monitoring space, picking the right metrology IC is critical. Here is the decision framework to select your core components.
| IC Variant | Phases | Interface | Complexity | Verdict |
|---|---|---|---|---|
| ADE7758 (Analog Devices) | 3-Phase | SPI | High (Legacy, obsolete) | Avoid. Hard to source, complex register map. |
| ATM90E32AS (Microchip) | 3-Phase | SPI | Medium-High | Choose only if measuring 3-phase industrial panels. |
| ATM90E26 (Microchip) | 1-Phase | SPI/UART | Low-Medium | Default Pick. Perfect for single-phase 120V/240V mains. |
Final Hardware Pick: Use the ATM90E26 paired with an ESP32-S3-DevKitC-1. The ESP32-S3 is chosen over the original ESP32-WROOM because its improved ADC linearity, native USB-CDC for serial debugging, and dual-core architecture prevent Wi-Fi interrupt latency from stalling your SPI reads.
Hardware Spec Sheet & Pin Mapping
Here is the exact bill of materials and wiring map. Prices reflect typical 2026 market rates for genuine components.
Parts List
- MCU: ESP32-S3-DevKitC-1 (N8R8 variant) — ~$8.00
- Metrology IC: ATM90E26 Breakout Board (with onboard shunt/CT headers and isolation) — ~$14.00
- Current Sensor: SCT-013-000 (100A:50mA Current Transformer) — ~$9.00
- Voltage Sensor: ZMPT101B AC Voltage Transformer Module (or a custom 12V AC wall-wart stepped down via voltage divider) — ~$4.00
- Power Supply: 5V 2A USB-C isolated supply — ~$6.00
Difficulty Rating: Intermediate (Requires mains wiring experience and SPI debugging).
SPI Pin Mapping Table
| ATM90E26 Pin | ESP32-S3-DevKitC-1 Pin | Function | Notes |
|---|---|---|---|
| VCC | 3V3 | Logic Power | Do NOT use 5V. The IC logic is 3.3V. |
| GND | GND | Ground | Common ground required. |
| SCK | GPIO 12 | SPI Clock | Max 200kHz recommended for stability. |
| MOSI (SDI) | GPIO 11 | Master Out, Slave In | Data to IC. |
| MISO (SDO) | GPIO 13 | Master In, Slave Out | Data from IC. |
| CS | GPIO 10 | Chip Select | Active LOW. Must use internal pull-up. |
| WARN | GPIO 4 | Interrupt | Optional. Triggers on over-current. |
Step-by-Step Build & Mains Wiring Procedure
⚠️ DANGER: MAINS VOLTAGE HAZARD
This project interfaces directly with 120V/240V AC mains. Lethal shock and arc flash hazards exist. De-energize the circuit at the breaker panel, apply a lockout/tagout device, and verify the circuit is dead with a CAT III rated multimeter before touching any terminals. If you are not comfortable with NEC-style mains wiring practices, hire a licensed electrician to install the CT and voltage taps. Never bypass fuses or grounding.
- Mount the Sensors: Clamp the SCT-013-000 current transformer around the hot (line) conductor of your target load. Do not clamp it around the entire NM-B cable, or the magnetic fields will cancel out and read zero.
- Wire the Voltage Tap: Connect the primary side of your ZMPT101B or step-down transformer in parallel with the load (Line to Neutral).
- Connect the Breakout: Wire the secondary outputs of the CT and Voltage transformer to the designated analog input headers on the ATM90E26 breakout board.
- Wire the SPI Bus: Connect the ESP32-S3 to the ATM90E26 using the pin mapping table above. Keep SPI traces under 10cm to prevent parasitic capacitance from corrupting the clock edges.
- Power Up: Plug the ESP32-S3 into your PC via USB-C. Ensure the ATM90E26 VCC is reading exactly 3.3V at the breakout header before proceeding.
- Calibrate: Apply a known resistive load (like a 100W incandescent bulb). Use the code below to read the raw registers and apply the calibration multipliers in the software.
Complete C++ Code with SPI Error Handling
The following code targets the ESP32-S3-DevKitC-1 using the Arduino framework. It initializes the SPI bus, reads the System Status register to verify communication, and fetches the True-RMS voltage and active power. Note the explicit error handling for SPI timeouts.
#include <SPI.h>
// --- PIN DEFINITIONS (ESP32-S3-DevKitC-1) ---
#define ATM_CS 10
#define ATM_SCK 12
#define ATM_MISO 13
#define ATM_MOSI 11
#define ATM_WARN 4
// --- ATM90E26 REGISTER MAP (Subset) ---
#define REG_SYS_STATUS 0x09
#define REG_URMS 0x14
#define REG_IRMS 0x15
#define REG_PMEAN 0x1A
// Calibration constants (Determined via known resistive load)
const float VOLTAGE_CAL = 0.152; // Adjust based on ZMPT101B divider
const float CURRENT_CAL = 0.012; // Adjust based on SCT-013 burden resistor
const float POWER_CAL = 0.0018;
SPISettings atmSettings(200000, MSBFIRST, SPI_MODE0);
void setup() {
Serial.begin(115200);
while(!Serial) { delay(10); }
pinMode(ATM_CS, OUTPUT);
digitalWrite(ATM_CS, HIGH); // Deselect IC
pinMode(ATM_WARN, INPUT_PULLUP);
SPI.begin(ATM_SCK, ATM_MISO, ATM_MOSI, ATM_CS);
delay(100); // Wait for IC internal reset
Serial.println("ESP32-S3 ATM90E26 Power Meter Initialized.");
// Verify communication
uint16_t sysStatus = readRegister(REG_SYS_STATUS);
if (sysStatus == 0xFFFF || sysStatus == 0x0000) {
Serial.println("Error: SPI Read returned 0xFFFF or 0x0000. Check CS pin and logic levels.");
while(1) { delay(1000); } // Halt execution
}
Serial.printf("System Status Register: 0x%04X\n", sysStatus);
}
void loop() {
uint16_t rawV = readRegister(REG_URMS);
uint16_t rawI = readRegister(REG_IRMS);
int16_t rawP = (int16_t)readRegister(REG_PMEAN); // Power can be negative (exporting)
float voltage = rawV * VOLTAGE_CAL;
float current = rawI * CURRENT_CAL;
float power = rawP * POWER_CAL;
float pf = (voltage > 0 && current > 0) ? (power / (voltage * current)) : 0.0;
Serial.printf("V: %5.1fV | I: %5.2fA | P: %6.1fW | PF: %4.2f\n",
voltage, current, power, pf);
delay(1000); // 1Hz update rate
}
// --- SPI READ FUNCTION WITH ERROR HANDLING ---
uint16_t readRegister(uint8_t reg) {
uint16_t val = 0xFFFF;
SPI.beginTransaction(atmSettings);
digitalWrite(ATM_CS, LOW);
delayMicroseconds(5); // CS setup time
// Send read command (Bit 15 = 1 for read, bits 14:8 = address)
uint16_t cmd = 0x8000 | ((uint16_t)reg << 8);
SPI.transfer16(cmd);
// Clock out the data
val = SPI.transfer16(0x0000);
delayMicroseconds(5); // CS hold time
digitalWrite(ATM_CS, HIGH);
SPI.endTransaction();
// Basic sanity check for disconnected MISO line
if (val == 0xFFFF) {
Serial.println("Warning: SPI Read returned 0xFFFF. MISO may be floating.");
}
return val;
}
Debugging: "SPI Read Returns 0xFFFF or 0x0000"
When working with SPI metrology ICs, the most common failure mode during initial bring-up is the serial monitor printing: Error: SPI Read returned 0xFFFF or 0x0000. Check CS pin and logic levels.
If you hit this exact error string, do not rewrite your code. The issue is almost always physical or timing-related. Here are the first three things to check:
- Logic Level Mismatch: The ATM90E26 is strictly a 3.3V logic device. If you are using a 5V Arduino clone or forgot to set your ESP32-S3 pins to 3.3V mode, you may have back-powered the IC through the SPI protection diodes. Measure the VCC pin on the IC with a multimeter; it must read 3.3V ± 5%.
- SPI Mode and Clock Speed: The ATM90E26 requires SPI Mode 0 (CPOL=0, CPHA=0). If your library defaults to Mode 3, the IC will ignore the clock. Furthermore, while the datasheet claims higher speeds, parasitic capacitance on breadboards will corrupt signals above 200kHz. Keep the clock at 200kHz until the build is soldered.
- CS Pin Routing: Ensure the Chip Select pin is not being driven by the ESP32's default hardware SPI manager. We are using software-controlled CS (
digitalWrite) to guarantee timing. If you pass the CS pin into theSPI.begin()constructor on some ESP32 Arduino core versions, it overrides your manual toggling.
Ranked Causes for Persistent 0xFFFF Errors
| Rank | Cause | Fix / Measurement Threshold |
|---|---|---|
| 1 | Floating MISO line (broken jumper wire) | Measure continuity from IC SDO to ESP32 GPIO 13. Must read < 1 ohm. |
| 2 | CS pin stuck HIGH | Scope the CS pin. It must drop to < 0.5V during the transaction. |
| 3 | Insufficient IC reset time | Increase delay(100) in setup to delay(500). The IC requires a stable clock before SPI is active. |
| 4 | Missing AGND/DGND bridge | Ensure the breakout board ties Analog and Digital grounds together at a single star point. |
Extending or Simplifying the Build
Depending on your project goals, you may want to alter the scope of this build.
To Simplify: If the raw SPI register mapping is too tedious, swap the ATM90E26 for a pre-calibrated PZEM-004T v3.0 module. The PZEM uses a simpler UART interface and handles all internal calibration, though it sacrifices the raw waveform data and high-speed sampling needed for deep harmonic analysis. You will lose the ability to calculate custom reactive power metrics, but you will get a working True-RMS meter in an afternoon.
To Extend: To turn this into a shop-wide energy dashboard, add the PubSubClient library to the ESP32-S3 code. Publish the voltage, current, power, and pf floats to an MQTT broker (like Mosquitto running on a Raspberry Pi) every 5 seconds. From there, ingest the MQTT topics into Telegraf and visualize the power factor degradation of your shop air compressor over time using a Grafana dashboard. This extension turns a bench instrument into a permanent predictive maintenance sensor.
Building awesome electronics projects requires moving past abstract theory and wrestling with real-world silicon. By calibrating this meter against a known resistive load and then testing it on a heavily inductive motor, you will finally see the math of complex impedance come to life on your serial monitor. Lock in your calibration constants, secure your mains terminals, and start logging real power data.






