Skip to main content

Firmware Measurement Model

This page documents how the EMS firmware converts raw ADC samples from the voltage sensor (ZMPT101B) and current sensor (ACS712 hall-effect, or SCT-013 clamp) into electrical quantities: true RMS voltage and current, active power, apparent power, power factor, and an estimated phase angle. It also covers the calibration routine, the accuracy limits of the present approach, and the methods that would improve it.

:::info Notation Throughout, NN is the number of samples per measurement window (bufferSize = 200), xnx_n is a raw ADC count in the range [0,M][0, M] with M=4095M = 4095 (12-bit), and Vref=3.3VV_{ref} = 3.3\,\text{V} is the ADC reference. ff is the line frequency (50 Hz here). :::


1. Signal Chain

Each analog channel follows the same path:

AC quantity  sensor  vin(t)  shift & scale  vadc(t)[0,3.3]V  12-bit SAR ADC  xn[0,4095]\text{AC quantity} \;\overset{\text{sensor}}{\longrightarrow}\; v_{in}(t) \;\overset{\text{shift \& scale}}{\longrightarrow}\; v_{adc}(t) \in [0, 3.3]\,\text{V} \;\overset{\text{12-bit SAR ADC}}{\longrightarrow}\; x_n \in [0, 4095]
  • Voltage: the ZMPT101B outputs an attenuated, level-shifted copy of the mains voltage, biased to roughly mid-rail.
  • Current (ACS712): outputs a voltage centred on 2.5V\approx 2.5\,\text{V} with sensitivity s=0.185V/As = 0.185\,\text{V/A} (5 A module). Because that can swing to 5 V, a resistor divider of ratio D=18.8/6.82.76D = 18.8/6.8 \approx 2.76 brings it into the 3.3 V ADC window.
  • Current (SCT-013): a current transformer whose signal is biased to mid-rail and scaled so that Si=mVperAmp1000=130V/AS_i = \tfrac{mVperAmp}{1000} = \tfrac{1}{30}\,\text{V/A}.

The firmware fills two integer buffers, sampling the channels sequentially inside one loop:

for (int i = 0; i < bufferSize; i++) {
voltageBuffer[i] = analogRead(VOLT_SENSOR_PIN);
currentBuffer[i] = analogRead(CURR_SENSOR_PIN);
delayMicroseconds(sampleDelayUs); // 500 us
}

With two conversions plus the 500 µs delay, the effective sample interval is Δt0.5ms+2tconv\Delta t \approx 0.5\,\text{ms} + 2\,t_{conv}, giving a rate of roughly fs1.52kHzf_s \approx 1.5\text{–}2\,\text{kHz} and a window length

Twin=NΔt200×0.6ms0.12s6 cycles at 50Hz.T_{win} = N\,\Delta t \approx 200 \times 0.6\,\text{ms} \approx 0.12\,\text{s} \approx 6 \text{ cycles at } 50\,\text{Hz}.

2. From ADC Counts to Volts

A raw count maps to the pin voltage by

un=xnVrefMu_n = x_n \cdot \frac{V_{ref}}{M}

The ESP32 uses ADC_11db attenuation, whose true full-scale is closer to 3.1V\sim 3.1\,\text{V} than the nominal 3.3 V and is mildly non-linear near the rails. The voltage gain factor kvk_v (below) absorbs this constant scale error, but the non-linearity itself is not corrected.


3. DC Offset Removal and Calibration

Both channels carry a DC bias that must be removed before computing AC quantities. The firmware treats the two channels differently.

3.1 Voltage offset OvO_v (Step 2 of calibration)

With no special excitation, 1000 reads are averaged. Over 1\approx 1 s (50 cycles) the AC component averages to zero, leaving the DC bias in counts:

Ov=11000n=11000xv,n.O_v = \frac{1}{1000}\sum_{n=1}^{1000} x_{v,n}.

This stored value is then subtracted from every voltage sample at runtime.

3.2 Current offset OiO_i (Step 1 of calibration)

Measured the same way with no load:

Oi=11000n=11000xi,n.O_i = \frac{1}{1000}\sum_{n=1}^{1000} x_{i,n}.

:::warning Calibrated, but unused calculatePower() does not use OiO_i. It removes the per-window mean xˉi=1Nxi,n\bar{x}_i = \tfrac{1}{N}\sum x_{i,n} instead, and the ACS712 instantaneous branch even subtracts a hard-coded 2.5V2.5\,\text{V} midpoint. So OiO_i is effectively dead state. Real power is largely unaffected (the offset cancels because the voltage term is zero-mean), but absolute instantaneous current is biased. Wiring OiO_i into the current conversion is a recommended fix (Section 9). :::

3.3 Voltage gain kvk_v (Step 3 of calibration)

A single-point linear gain. The firmware measures the raw RMS over 0.5\approx 0.5 s,

Vmeasured=VrefM1Nn(xv,nOv)2,V_{measured} = \frac{V_{ref}}{M}\sqrt{\frac{1}{N}\sum_{n}(x_{v,n} - O_v)^2},

the operator enters the true mains RMS VactualV_{actual} from a multimeter, and

kv=VactualVmeasured.k_v = \frac{V_{actual}}{V_{measured}}.

This assumes linearity and a zero intercept (single point, no offset term).

3.4 Current gain

There is no current gain calibration. Current amplitude relies entirely on the datasheet sensitivity (0.185V/A0.185\,\text{V/A}) and resistor-divider tolerance. Since the ACS712 sensitivity spread is ±510%\pm 5\text{–}10\% and is ratiometric to its 5 V supply, this is the dominant amplitude-error source.


4. True RMS

For a discrete signal of NN samples with the DC component removed, the RMS is

Xrms=1Nn=1N(xnxˉ)2  .X_{rms} = \sqrt{\frac{1}{N}\sum_{n=1}^{N} (x_n - \bar{x})^2}\;.

This is a true RMS: it is exact for any periodic waveform sampled adequately, not just sinusoids.

Voltage RMS (fixed stored offset OvO_v, gain kvk_v):

Vrms=kvVrefM1Nn=1N(xv,nOv)2V_{rms} = k_v\,\frac{V_{ref}}{M}\sqrt{\frac{1}{N}\sum_{n=1}^{N}\big(x_{v,n} - O_v\big)^2}

Current RMS, SCT-013 (per-window mean xˉi\bar{x}_i, scale SiS_i):

Irms=VrefMSi1Nn=1N(xi,nxˉi)2I_{rms} = \frac{V_{ref}}{M\,S_i}\sqrt{\frac{1}{N}\sum_{n=1}^{N}\big(x_{i,n} - \bar{x}_i\big)^2}

Current RMS, ACS712 (divider ratio DD, sensitivity ss):

Irms=VrefDMs1Nn=1N(xi,nxˉi)2I_{rms} = \frac{V_{ref}\,D}{M\,s}\sqrt{\frac{1}{N}\sum_{n=1}^{N}\big(x_{i,n} - \bar{x}_i\big)^2}

A noise dead-band forces Irms=0I_{rms} = 0 below 50 mA to suppress jitter at idle.


5. Active (Real) Power

Real power is the time-average of instantaneous power:

For Discrete Real Power calculation

  P=1Nn=1Nvnin  \boxed{\;P = \frac{1}{N}\sum_{n=1}^{N} v_n\, i_n \;}

For Analog Real Power calculation

1T0Tv(t)i(t)dt  \frac{1}{T}\int_0^T v(t)\,i(t)\,dt\;

where the per-sample physical values are

vn=(xv,nOv)VrefMkv,in={VrefMSi(xi,nxˉi)SCT-0131s ⁣(xi,nMVrefD2.5)ACS712v_n = (x_{v,n} - O_v)\frac{V_{ref}}{M}k_v, \qquad i_n = \begin{cases} \dfrac{V_{ref}}{M\,S_i}\,(x_{i,n} - \bar{x}_i) & \text{SCT-013}\\[2ex] \dfrac{1}{s}\!\left(\dfrac{x_{i,n}}{M}V_{ref}\,D - 2.5\right) & \text{ACS712} \end{cases}

The instantaneous-product formulation is waveform-agnostic: it captures the real power of distorted loads automatically, because power is computed before any sinusoidal assumption is made. A small dead-band zeroes PP when P<0.5W|P| < 0.5\,\text{W}.

:::note ACS712 branch inconsistency The ACS712 instantaneous current uses a fixed 2.5V2.5\,\text{V} reference, while its RMS uses the dynamic mean xˉi\bar{x}_i. The AC gain is identical in both, so only the DC reference differs; since vnv_n is zero-mean, the difference largely cancels in PP. It is still cleaner to use the same mean-removed value in both (Section 9). :::


6. Apparent power, power factor, phase angle

S=VrmsIrms,PF=PS,φ=arccos(PF)180π.S = V_{rms}\,I_{rms}, \qquad PF = \frac{P}{S}, \qquad \varphi = \arccos(PF)\cdot\frac{180}{\pi}.

Because PP comes from instantaneous products and SS from true RMS, PF=P/SPF = P/S is the true power factor, already including any distortion component. This is correct.

The phase angle, however, is only an equivalent angle:

φ=arccos(PF).\varphi = \arccos(PF).

It is valid as a displacement angle only for sinusoidal signals, and it is always unsignedcos(+φ)=cos(φ)\cos(+\varphi) = \cos(-\varphi), so it cannot distinguish leading (capacitive) from lagging (inductive) loads. Section 8 shows why it diverges from the true angle under distortion, and Section 9 gives the proper method.


7. The sinusoidal case (where the model is exact)

Let v(t)=2Vcos(ωt)v(t) = \sqrt{2}\,V\cos(\omega t) and i(t)=2Icos(ωtφ)i(t) = \sqrt{2}\,I\cos(\omega t - \varphi). Then

P=1T0Tvidt=VIcosφ,S=VI,PF=cosφ,arccos(PF)=φ.P = \frac{1}{T}\int_0^T v\,i\,dt = V I \cos\varphi,\qquad S = V I,\qquad PF = \cos\varphi,\qquad \arccos(PF) = \varphi.

So for a clean sine wave, every quantity the firmware reports — VrmsV_{rms}, IrmsI_{rms}, PP, SS, PFPF, and φ\varphi — is correct, limited only by sampling, ADC, and calibration error.


8. The non-sinusoidal case

Expand both signals in a Fourier series:

v(t)=k2Vkcos(kωt+αk),i(t)=k2Ikcos(kωt+βk).v(t) = \sum_{k} \sqrt{2}\,V_k\cos(k\omega t + \alpha_k),\qquad i(t) = \sum_{k} \sqrt{2}\,I_k\cos(k\omega t + \beta_k).

Because harmonics of different orders are orthogonal over a period, only matched orders contribute to real power:

P=kVkIkcos(αkβk)P = \sum_{k} V_k I_k \cos(\alpha_k - \beta_k)

and the RMS values aggregate all harmonics:

Vrms=kVk2,Irms=kIk2,S=VrmsIrms.V_{rms} = \sqrt{\sum_k V_k^2},\qquad I_{rms} = \sqrt{\sum_k I_k^2},\qquad S = V_{rms} I_{rms}.

The firmware's time-domain sums compute exactly these quantities — provided the sampling resolves the harmonics (Section 10). So VrmsV_{rms}, IrmsI_{rms}, PP, SS, and the true PFPF remain correct for distorted loads.

8.1 Why arccos(PF)\arccos(PF) breaks

Total harmonic distortion of the current is

THDi=k2Ik2I1.THD_i = \frac{\sqrt{\sum_{k\geq 2} I_k^2}}{I_1}.

For a clean (fundamental-only) voltage feeding a distorted current, the true power factor factors into a displacement part and a distortion part:

PF=cosφ1displacement11+THDi2distortion.PF = \underbrace{\cos\varphi_1}_{\text{displacement}} \cdot \underbrace{\frac{1}{\sqrt{1 + THD_i^2}}}_{\text{distortion}}.

Hence arccos(PF)φ1\arccos(PF) \neq \varphi_1 whenever THDi0THD_i \neq 0: the reported angle folds distortion into displacement and overstates the true phase shift. The firmware's φ\varphi should therefore be read as a "sinusoid-equivalent angle," not as the fundamental displacement angle.

8.2 Dead-band artifact

When the dead-bands force Irms=0I_{rms} = 0 or P=0P = 0 near idle, PFPF collapses to 0 and φ\varphi reports 9090^\circ. This is a cosmetic artifact of the thresholds, not a real reading.


9. Sampling considerations and error sources

EffectCauseConsequence
Aliasingfs1.52kHzf_s \approx 1.5\text{–}2\,\text{kHz}, no anti-alias filterHarmonics above fs/2\sim f_s/2 fold back into the band, corrupting RMS and PP for sharp-edged waveforms
Spectral leakageWindow is 6\approx 6 cycles, non-integer, not zero-cross lockedSmall RMS / PP error
Inter-channel skewV and I sampled sequentially, not simultaneouslyPhase error δ=360fΔts\delta = 360^\circ f \,\Delta t_s; power error ΔPPδtanφ\frac{\Delta P}{P}\approx \delta\tan\varphi (negligible near PF=1PF=1, ~2–3% at PF=0.5PF=0.5 for a 50 µs gap)
Timing jitterdelayMicroseconds + variable ADC timeNon-uniform Δt\Delta t smears any frequency-dependent result
ADC non-linearityESP32 SAR ADC, worse near railsAbsolute amplitude error
Software floating pointESP32-C3 has no FPU; classic ESP32 has only single-precisionPer-sample double math is slow, limiting achievable fsf_s

A useful skew estimate: at 50 Hz, one ADC conversion of 50μs\sim 50\,\mu\text{s} between channels is

δ=360×50Hz×50μs0.9.\delta = 360^\circ \times 50\,\text{Hz} \times 50\,\mu\text{s} \approx 0.9^\circ.

10. Accuracy summary

QuantitySinusoidalNon-sinusoidalLimiting factors
VrmsV_{rms}, IrmsI_{rms}AccurateAccurate (if harmonics within Nyquist)ADC cal, aliasing
PP (real power)AccurateAccurate (instantaneous product)Skew, aliasing, offsets
SS (apparent)AccurateAccurateRMS accuracy
PF=P/SPF = P/SAccurateAccurate (true PF)PP and SS accuracy
φ=arccos(PF)\varphi = \arccos(PF)Accurate (unsigned)InaccurateDistortion folded in; sign lost
Lead / lag directionNot availableNot availablearccos\arccos is symmetric

11.1 Use the calibrated current offset

Replace the per-window mean and the fixed 2.5V2.5\,\text{V} reference with the stored OiO_i for a consistent, drift-aware zero:

in=(xi,nOi)VrefMGi,i_n = (x_{i,n} - O_i)\cdot\frac{V_{ref}}{M}\cdot G_i,

with GiG_i the (newly calibrated) current gain.

11.2 Add a current gain calibration

Apply a known resistive load IknownI_{known}, measure ImeasuredI_{measured}, and store

Gi\*=IknownImeasured.Gi=IknownImeasured.G_i \mathrel{\*}= \frac{I_{known}}{I_{measured}}. G_i \mathrel{\ast}= \frac{I_{known}}{I_{measured}}.

This removes the dominant amplitude error from sensitivity/divider tolerance.

11.3 Anti-aliasing filter

A first-order RC low-pass on each analog input with a corner around 300500Hz300\text{–}500\,\text{Hz} attenuates content above Nyquist before it can fold back.

11.4 Deterministic, timer-driven sampling

Drive the ADC from a hardware timer (or the ESP32 continuous / DMA ADC mode) at a fixed, known fsf_s instead of delayMicroseconds. This removes jitter and makes the window length exact.

11.5 Simultaneous V/I sampling

Eliminate inter-channel skew by sampling both channels at the same instant (sample-and-hold front end, or a simultaneous-sampling ADC). A software-only stopgap is to linearly interpolate one channel to the other's timestamp:

i^n=in+ΔtsΔt(in+1in).\hat{i}_n = i_n + \frac{\Delta t_s}{\Delta t}\,(i_{n+1} - i_n).

11.6 Synchronous / whole-cycle windowing

Detect zero crossings (or lock a PLL to the line) and integrate over an integer number of cycles to eliminate leakage.

11.7 Signed displacement angle and reactive power

Extract the fundamental of each channel with the Goertzel algorithm at f=50Hzf = 50\,\text{Hz} to obtain V1α1V_1\angle\alpha_1 and I1β1I_1\angle\beta_1, then

φ1=α1β1(signed),PFdisp=cosφ1,Q1=V1I1sinφ1.\varphi_1 = \alpha_1 - \beta_1 \quad(\text{signed}),\qquad PF_{disp} = \cos\varphi_1,\qquad Q_1 = V_1 I_1 \sin\varphi_1.

The sign of Q1Q_1 distinguishes inductive (Q1>0Q_1 > 0, lagging) from capacitive (Q1<0Q_1 < 0, leading). A total non-active power consistent with IEEE Std 1459 is

N=S2P2.\mathcal{N} = \sqrt{S^2 - P^2}.

11.8 Report THD

With the harmonic magnitudes from a DFT/Goertzel bank,

THDi=k2Ik2I1,THDv=k2Vk2V1.THD_i = \frac{\sqrt{\sum_{k\geq 2} I_k^2}}{I_1},\qquad THD_v = \frac{\sqrt{\sum_{k\geq 2} V_k^2}}{V_1}.

11.9 Energy accumulation

Integrate power over time for kWh:

E=PdtmPmΔTm.E = \int P\,dt \approx \sum_m P_m\,\Delta T_m.

11.10 Numerical performance

On the FPU-less C3, accumulate integer sums of squares and products in int64_t inside the loop and convert to float once at the end; precompute constant scale factors outside the loop. Prefer float over double everywhere the extra precision is not needed.

11.11 Robust offset and ADC calibration

Use a median (not mean) for offset estimation to reject spikes, and apply the ESP32 eFuse Vref characterization (esp_adc_cal) to linearize the ADC.


12. Alternative measurement methods

  • Dedicated metering AFEs. ICs such as the ADE7953, ADE7758, ATM90E26/E32, or CS5490 perform simultaneous sampling, harmonic-aware power, signed reactive power, and energy accumulation in calibrated hardware, communicating over SPI/I²C/UART. This is the most accurate path and offloads the MCU entirely.
  • External simultaneous-sampling ADC. A delta-sigma ADC like the ADS131M02 samples both channels at the same instant with high resolution, removing skew and ADC-linearity concerns while keeping the power math on the MCU.
  • Frequency-domain processing. Run a windowed FFT (e.g. Hann window) on each channel to obtain per-harmonic magnitude and phase, then compute PP, QQ, and THD spectrally. Heavier on the MCU but gives a full harmonic picture.
  • Goertzel single-bin extraction. Cheaper than a full FFT when only the fundamental (and a few harmonics) are needed for displacement PF and angle.

13. Equation ↔ code reference

SymbolMeaningCode variable
NNsamples per windowbufferSize
Δt\Delta tsample intervalsampleDelayUs (+ conversion time)
VrefV_{ref}ADC referencereferenceVoltage
MMADC full scaleadcMax
OvO_vvoltage DC offsetvoltageOffset
OiO_icurrent DC offset (unused)currentOffset
kvk_vvoltage gainvoltageCalibration
SiS_iSCT scale (V/A)mVperAmp / 1000
DDACS712 divider ratiovoltageDividerRatio
ssACS712 sensitivityhall_sensitivity
VrmsV_{rms}RMS voltagevoltageRMS
IrmsI_{rms}RMS currentcurrentRMS
PPactive powerrealPower
SSapparent powerapparentPower
PFPFpower factorpowerFactor
φ\varphiphase anglephaseAngle

:::caution Mains safety Calibration Step 3 requires measuring live mains with a multimeter, and the sensors connect to mains during normal operation. Perform these steps only with the circuit properly enclosed and isolated, and only if you are competent to work around live AC. :::